容器框架

Java容器是必须掌握的Java内置工具,非常非常非常重要,而且必须熟悉使用方法和原理(源码),容器原理必须会,总之就是超级重要。每种容器都有各自的特点,要掌握这些容器的特点以及实现的原理。虽然容器有很多,但是常用的就那么几个,本文对不太常用的或者已经淘汰的容器只做简单介绍,重点介绍常用容器。

java容器组件在下方加入按钮_List

前置知识

容器涉及到的基础知识点特别多,想学透容器必须得会以下的前置知识,不会的话估计也只能学个使用方法,尤其是前三点必须要会的,不然根本没法学,不会的先去补完之后再学容器!

  • 扎实的数据结构功底,数组、链表、栈、队列、哈希表、红黑树都得掌握
  • Java的比较器接口Comparable接口和Comparator接口
  • Java面向对象的基础知识和Object类中的equals、hashCode方法
  • 匿名内部类(非必须)
  • 流Stream(非必须)
  • 函数式接口(非必须)

Collection

Collection是单列容器的父类,分别介绍List、Set和Deque

List

List特点是支持索引下标,类似于数组。

ArrayList与LinkedList比较

ArrayList和LinkedList的优缺点都是相辅相成的,作为互相的补充。

  • 数据结构方面
  • ArrayList底层通过数组实现,物理存储空间连续
  • LinkedList底层是双向链表,物理存储不连续
  • 优缺点
  • ArrayList是固定容器,有初始容量为10,当容器满了之后需要扩容,每次扩容为原来1.5倍,但是LinkedList没有这个问题,因为它是链表,按需分配长度。
  • LinkedList不连续存储,又要满足List下标访问的特点,因此每个节点都一个index字段,每次查询都要遍历链表,源码中是通过双向链表前后同时向中间靠拢来遍历的,提高查询速度。
  • 使用技巧
  • ArrayList查询性能更高,LinkedList插入删除性能更高,因此可以按照实际需求选择容器。
  • ArrayList创建时支持指定初始容量,如果对数据规模有个预估,最好指定,避免触发自动扩容,因为自动扩容会影响代码性能。
ArrayList使用

ArrayList基本方法,当然还有很多有用的方法,需要大家自己去探索。
boolean add(E e); // 加入元素e
public E get(int index) // 获取下标为index的元素
boolean remove(int index); // 移除下标为index的元素
int size(); // 返回元素个数
boolean contains(Object o); // 是否包含元素o
Iterator iterator(); // 返回迭代器

示例代码ArrayList的增删改查和遍历,ArrayList遍历通常可以有4中方法

public class Demo01ArrayList {
    public static void main(String[] args) {
        List<Student> arraylist = new ArrayList<>();
        // List增
        arraylist.add(new Student(1,"zzz","beijing"));
        arraylist.add(new Student(2,"yasuo","japan"));
        arraylist.add(new Student(3,"yongen","japan"));
        arraylist.add(new Student(4,"cat","germany"));

        // List删
        arraylist.remove(1); // 这个方法可以传入下标也可以传入要删除的对象

        // List改 就是在相应的位置重新加入一个对象就覆盖了
        // List查
        arraylist.get(2); // 返回相应下标对应的值

        // 遍历
        // 1.直接for循环通过下标访问
        for (int i = 0; i < arraylist.size(); i++) {
            System.out.println(arraylist.get(i));
        }
        // 2.Iterator迭代器
        Iterator<Student> iterator = arraylist.iterator();
        while (iterator.hasNext()){
            System.out.println(iterator.next());
        }
        // 3.增强for循环
        for(Student student : arraylist){
            System.out.println(student);
        }
        // 4.流 *** 实际开发一般都用这个
        arraylist.stream().forEach(System.out::println);
    }
}

Student类

public class Student {
    int id;
    String name;
    String address;

    public Student() {
    }

    public Student(int id, String name, String address) {
        this.id = id;
        this.name = name;
        this.address = address;
    }

    public int getId() {
        return id;
    }

    public void setId(int id) {
        this.id = id;
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public String getAddress() {
        return address;
    }

    public void setAddress(String address) {
        this.address = address;
    }

    @Override
    public String toString() {
        return  id + "\t\t" + name + "\t\t" + address;
    }
}
LinkedList使用

LinkedList基本方法和ArrayList几乎一样,使用上也一样,示例代码中ArrayList改成LinkedList即可,为节省篇幅不贴代码了。虽然操作都一样,但是根据使用场景不同,一定要区分两种List,一定要掌握两种List的优缺点对比。
boolean add(E e); // 加入元素e
public E get(int index) // 获取下标为index的元素
boolean remove(int index); // 移除下标为index的元素
int size(); // 返回元素个数
boolean contains(Object o); // 是否包含元素o
Iterator iterator(); // 返回迭代器


Set

Set是一种没有重复元素的单列集合,它是底层数据结构是基于Map来实现的,对你没有听错,是基于Map实现的,Map是双列的集合key-value,其key也具有不重复的特性,只保存key,所有的value都设置成一个空的Object就可以了。这个数据结构不理解的话,还需要补一下数据结构。

Set的使用
public class Demo01Set {
    public static void main(String[] args) {
        Set<Integer> set = new HashSet<>(); // 以HashSet为例

        // 1.增
        set.add(0);
        set.add(1);
        set.add(2);
        set.add(3);
        set.add(4);
        set.add(0);
        System.out.println(set); // [0, 1, 2, 3, 4] 重复元素0只出现一次

        // 2.删
        set.remove(2);
        System.out.println(set); // [0, 1, 3, 4] 删掉了2

        // 3.查 只能遍历
        // 遍历方法1.迭代器
        Iterator<Integer> iterator = set.iterator();
        while(iterator.hasNext()){
            System.out.println(iterator.next());
        }
        // 遍历方法2.增强for循环
        for(Integer i : set){
            System.out.println(i);
        }
        // 遍历方法3.流
        set.stream().forEach(System.out::println);
    }
}
HashSet与TreeSet比较
  • 数据结构
  • HashSet也是数组+链表(链表长度>8时转化红黑树)
  • TreeSet是红黑树
  • 唯一性实现原理
  • HashSet是先通过哈希值定位到哈希表,再根据equals判定元素是否存在
  • TreeSet是根据排序器定义的排序规则判定元素是否相同,同时进行排序
  • 特点
  • HashSet无序存储,存取速度快
  • TreeSet存储有顺序,存取速度不如HashSet
  • 使用技巧
  • 对于不需要有序存储的元素,使用HashSet
  • 需要有序存储的选择TreeSet
  • 使用HashSet必须要重写equals和hashCode方法
  • 使用TreeSet必须要实现Comparable接口重写compareTo方法或者是对Set传入一个实现Comparator接口并重写compare方法的外部比较器
两个使用到的类

第一个是没有重写equals、hashCode、compareTo方法的Student
第二个是重写了的Student,通过使用HashSet与TreeSet体会重写不重写的区别

public class StudentNonOverride {
    int id;
    String name;
    int age;

    public StudentNonOverride() {
    }

    public StudentNonOverride(int id, String name, int age) {
        this.id = id;
        this.name = name;
        this.age = age;
    }

    public int getId() {
        return id;
    }

    public void setId(int id) {
        this.id = id;
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public int getAge() {
        return age;
    }

    public void setAge(int age) {
        this.age = age;
    }

    @Override
    public String toString() {
        return "{" +
                "id=" + id +
                ", name='" + name + '\'' +
                ", age=" + age +
                '}';
    }
}
public class StudentWithOverride implements Comparable{
    int id;
    String name;
    int age;

    public StudentWithOverride() {
    }

    public StudentWithOverride(int id, String name, int age) {
        this.id = id;
        this.name = name;
        this.age = age;
    }

    public int getId() {
        return id;
    }

    public void setId(int id) {
        this.id = id;
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public int getAge() {
        return age;
    }

    public void setAge(int age) {
        this.age = age;
    }

    @Override
    public String toString() {
        return "{" +
                "id=" + id +
                ", name='" + name + '\'' +
                ", age=" + age +
                '}';
    }

    @Override
    public boolean equals(Object o) {
        if (this == o) {
            return true;
        }
        if (o == null || getClass() != o.getClass()) {
            return false;
        }
        StudentWithOverride that = (StudentWithOverride) o;
        return id == that.id && age == that.age && Objects.equals(name, that.name);
    }

    @Override
    public int hashCode() {
        return Objects.hash(id, name, age);
    }

    @Override
    public int compareTo(Object o) {
        if(o instanceof StudentWithOverride){
            int diff = this.age - ((StudentWithOverride) o).getAge();
            return  diff != 0 ? diff : this.getId() - ((StudentWithOverride) o).getId();
        }
        return 0;
    }
}

StudentWithOverride中重写compareTo方法定义的比较规则是先按年龄比较,如果年龄相同,则按id比较。

HashSet示例代码

下面是HashSet的使用

public class Demo02HashSet {
    public static void main(String[] args) {
        hashSetDemo();
    }

    public static void hashSetDemo(){
        String s1 = "张三";
        String s2 = "李四";
        String s3 = "王二麻子";
        String s4 = "李四";
        Set<String> set1 = new HashSet<>();
        set1.add(s1);
        set1.add(s2);
        set1.add(s3);
        set1.add(s4);
        System.out.println(set1); // [李四, 张三, 王二麻子] 结果正常

        StudentNonOverride sn1 = new StudentNonOverride(1,"张三",23);
        StudentNonOverride sn2 = new StudentNonOverride(2,"李四",18);
        StudentNonOverride sn3 = new StudentNonOverride(3,"王二麻子",29);
        StudentNonOverride sn4 = new StudentNonOverride(2,"李四",18);
        Set<StudentNonOverride> set2 = new HashSet<>();
        set2.add(sn1);
        set2.add(sn2);
        set2.add(sn3);
        set2.add(sn4); // [{id=2, name='李四', age=18}, {id=2, name='李四', age=18}, {id=1, name='张三', age=23}, {id=3, name='王二麻子', age=29}]
        System.out.println(set2); // 输出结果看到有2个李四,不满足Set的唯一性,因为调用了默认的hashCode方法

        StudentWithOverride sw1 = new StudentWithOverride(1,"张三",23);
        StudentWithOverride sw2 = new StudentWithOverride(2,"李四",18);
        StudentWithOverride sw3 = new StudentWithOverride(3,"王二麻子",29);
        StudentWithOverride sw4 = new StudentWithOverride(2,"李四",18);
        Set<StudentWithOverride> set3 = new HashSet<>();
        set3.add(sw1);
        set3.add(sw2);
        set3.add(sw3);
        set3.add(sw4); // [{id=3, name='王二麻子', age=29}, {id=1, name='张三', age=23}, {id=2, name='李四', age=18}]
        System.out.println(set3); // 输出结果正常
    }
}

分析输出结果可以发现:
String的Set和StudentWithOverride的Set是正常的,因为他们都重写了equals和hashCode方法!StudentNonOverride的Set是不对的,没有重写equals和hashCode根本无法区分哪些元素是相同的,之所以没报错,是因为它的父类Object中有默认的equals和hashCode的方法,但是不管用,因为从父类继承来的方法只是根据地址值来区分元素是否相同,所以当然判定sn2和sn4是不同的元素。

TreeSet示例代码
public class Demo03TreeSet {
    public static void main(String[] args) {
        TreeSetComparableDemo();
        TreeSetComparatorDemo();
    }

    public static void TreeSetComparableDemo(){
        String s1 = "张三";
        String s2 = "李四";
        String s3 = "王二麻子";
        String s4 = "李四";
        Set<String> set1 = new TreeSet<>();
        set1.add(s1);
        set1.add(s2);
        set1.add(s3);
        set1.add(s4);
        System.out.println(set1); // [李四, 张三, 王二麻子] 结果正常

     /* StudentNonOverride sn1 = new StudentNonOverride(1,"张三",23);
        StudentNonOverride sn2 = new StudentNonOverride(2,"李四",18);
        StudentNonOverride sn3 = new StudentNonOverride(3,"王二麻子",29);
        StudentNonOverride sn4 = new StudentNonOverride(2,"李四",18);
        Set<StudentNonOverride> set2 = new TreeSet<>(); // 抛出异常java.lang.ClassCastException 因为类StudentNonOverride没有实现比较器
        set2.add(sn1);
        set2.add(sn2);
        set2.add(sn3);
        set2.add(sn4);
        System.out.println(set2);*/

        StudentWithOverride sw1 = new StudentWithOverride(1,"张三",23);
        StudentWithOverride sw2 = new StudentWithOverride(2,"李四",18);
        StudentWithOverride sw3 = new StudentWithOverride(3,"王二麻子",18);
        StudentWithOverride sw4 = new StudentWithOverride(2,"李四",18);
        Set<StudentWithOverride> set3 = new TreeSet<>();
        set3.add(sw1);
        set3.add(sw2);
        set3.add(sw3);
        set3.add(sw4); // [{id=2, name='李四', age=18}, {id=3, name='王二麻子', age=18}, {id=1, name='张三', age=23}]
        System.out.println(set3); // 输出结果符合预期
    }

    public static void TreeSetComparatorDemo(){
        StudentNonOverride sn1 = new StudentNonOverride(1,"张三",23);
        StudentNonOverride sn2 = new StudentNonOverride(2,"李四",18);
        StudentNonOverride sn3 = new StudentNonOverride(3,"王二麻子",18);
        StudentNonOverride sn4 = new StudentNonOverride(2,"李四",18);
        Set<StudentNonOverride> set2 = new TreeSet<>(new Comparator<StudentNonOverride>() {
            @Override
            public int compare(StudentNonOverride o1, StudentNonOverride o2) {
                int diff = o1.getAge() - o2.getAge();
                return diff != 0 ? diff : o1.getId() - o2.getId();
            }
        }); // 匿名内部类的方式实现comparator接口中的compare方法
        set2.add(sn1);
        set2.add(sn2);
        set2.add(sn3);
        set2.add(sn4);
        System.out.println(set2); // [{id=2, name='李四', age=18}, {id=3, name='王二麻子', age=18}, {id=1, name='张三', age=23}]
    }
}

分析TreeSetComparableDemo结果可知:
String和StudentWithOverride使用TreeSet结果正常,符合预期,StudentNonOverride使用TreeSet会直接抛异常,StudentNonOverride不具备比较能力,之所以这个会抛异常,其实因为类的比较能力不像equals和hashCode方法那样可以从父类继承,这个不写的话干脆就没有,当然会报异常。

为了使得类具备“比较能力”有两种方式,第一种是类实现Comparable接口,重写comparaTo方法,使得类本身具备排序能力,叫“自然排序”,就像类StudentWithOverride那样;第二种是给TreeSet传入一个比较器,叫“外部比较器”,将一个实现了Comparator接口并重写compara方法的类作为参数传入到TreeSet中,就像方法TreeSetComparatorDemo中。但是我是用了匿名内部类,这块不会的额外再补补吧。

Deque

Deque是一种队列容器,数据结构是一个双端队列,内置了支持队列和栈的基本操作方法,还有一些高级方法,一般使用这个容器就是为了使用栈或队列。

ArrayDeque和LinkedList比较
  • 数据结构
  • ArrayDeque是一个支持队列的数组,有一个front和一个rear指针,分别指向队头和队尾,本质上是一个循环队列,当front == rear时队列为空,当(rear+1)%maxsize=front时队列为满。
  • LinkedList就是相比于数组的,是个双端队列,其实和List里面的LinkedList一样,刚好可以复用。
  • 优缺点
  • 其实说到数组和链表,无非就是增删和查找的性能优劣势,但是如果这个容器仅作为栈和队列,插入删除都是在栈顶或队头队尾,不存在说在中间某个位置插入的情况 ,选择哪个性能或许没什么影响?或者有什么我想不到的场景,求高手指点。
常用使用方法

还有很多其他方法,需要的时候自查Api文档就好
boolean offer(E e)将e添加到末尾
E poll() 检索但是删除列表的头
E pop()当列表当栈使用时,出栈
void push(E e)将元素入栈

public class DequeDemo {
    public static void main(String[] args) {
        Deque<Integer> dequeDemo01 = new ArrayDeque<>(); // Deque当栈使用
        dequeDemo01.push(0);
        dequeDemo01.push(1);
        dequeDemo01.push(2);
        System.out.println(dequeDemo01); // [2, 1, 0]
        System.out.println(dequeDemo01.pop()); // 2
        System.out.println(dequeDemo01); // [1, 0]

        Deque<Integer> dequeDemo02 = new ArrayDeque<>(); // Deque当队列用
        dequeDemo02.offer(0);
        dequeDemo02.offer(1);
        dequeDemo02.offer(2);
        System.out.println(dequeDemo02); // [0, 1, 2]
        System.out.println(dequeDemo02.poll()); // 0
        System.out.println(dequeDemo02); // [1, 2]

        // 这里只列举ArrayDeque,因为从操作上和LinkedList()没有任何区别,这个容器还是比较简单的
    }
}

Map

Map是一种双列容器,由key-value键值对组成,一个键对应一个值,特点是key不会重复,和Set一样,这也是为什么Set底层可以通过Map实现的原因之一。

HashMap与TreeMap的区别

  • 数据结构
  • HashMap底层是一个数组(哈希表)+链表(长度>8转化为红黑树)
  • TreeMap底层就是一颗红黑树
  • 实现原理
  • HashMap先通过key的hashCode定位到哈希表中,再根据key的equals从链表中搜索对应的key,如果没搜索到,则将key-value插入到链表中,如果搜到,则替换原来的key-value
  • TreeMap是通过key的比较器结果判定值大小以及是否相同元素的
  • 使用技巧
  • HashMap有扩容问题,在可预知元素个数的情况下,初始化时最好指定hashMap大小
  • HashMap装的类一定要重写equals和hashCode方法,TreeMap一定要实现比较器
  • 对于需要指定存储顺序的类,使用TreeMap,否则使用HashMap

Map使用

public class Demo01Map {
    public static void main(String[] args) {
        Map<String,Integer> map = new HashMap<>(); // 以HashMap为例

        // 1.增
        map.put("first", 1);
        map.put("second", 2);
        map.put("third", 3);
        map.put("fourth", 4);

        // 2.删
        map.remove("third");

        // 3.改
        map.put("fourth", 3);

        // 4.查
        System.out.println(map.get("fourth"));

        // 5.遍历 都是通过set间接的取值
        // 遍历方法1.取出keySet即键的集合,再遍历keySet,根据key取出value
        Set<String> strings = map.keySet();
        for(String s : strings){
            System.out.print(map.get(s) + " "); // 3 1 2
        }
        System.out.println();
        // 遍历方法2.取出键值对的集合entrySet,再遍历entrySet集合,通过entrySet.getValue获取值
        Set<Entry<String, Integer>> entries = map.entrySet();
        for (Entry<String, Integer> entry : entries){
            System.out.print(entry.getValue() + " "); // 3 1 2
        }
        System.out.println();
        // 遍历方法3.流
        map.entrySet().stream().forEach(entry -> System.out.print(entry.getValue() + " ")); // 3 1 2
    }
}

HashMap示例代码

HashMap重中之重,它的数据结构还有源码都很重要,基本上面试必问。
下面的两段代码中的StudentNonOverride和StudentNonOverride与Set中的两个使用到的类相同,直接复制过来就好。

public class Demo02HashMap {
    public static void main(String[] args) {
        hashMapDemo();
    }

    public static void hashMapDemo(){
        String s1 = "张三";
        String s2 = "李四";
        String s3 = "王二麻子";
        String s4 = "李四";
        Map<String, String> map1 = new HashMap<>();
        map1.put(s1, "s1");
        map1.put(s2, "s2");
        map1.put(s3, "s3");
        map1.put(s4, "s4");
        System.out.println(map1); // {李四=s4, 张三=s1, 王二麻子=s3}

        SetDemo.StudentNonOverride sn1 = new SetDemo.StudentNonOverride(1,"张三",23);
        SetDemo.StudentNonOverride sn2 = new SetDemo.StudentNonOverride(2,"李四",18);
        SetDemo.StudentNonOverride sn3 = new SetDemo.StudentNonOverride(3,"王二麻子",29);
        SetDemo.StudentNonOverride sn4 = new SetDemo.StudentNonOverride(2,"李四",18);
        Map<StudentNonOverride, String> map2 = new HashMap<>();
        map2.put(sn1, "sn1");
        map2.put(sn2, "sn2");
        map2.put(sn3, "sn3");
        map2.put(sn4, "sn4");
        System.out.println(map2);
        // {{id=2, name='李四', age=18}=sn4, {id=2, name='李四', age=18}=sn2, {id=1, name='张三', age=23}=sn1, {id=3, name='王二麻子', age=29}=sn3}
        // 观察StudentNonOverride没有重写equals和hashCode导致sn2和sn4相同的key,都存到了map里,不符合map的唯一性
        SetDemo.StudentWithOverride sw1 = new SetDemo.StudentWithOverride(1,"张三",23);
        SetDemo.StudentWithOverride sw2 = new SetDemo.StudentWithOverride(2,"李四",18);
        SetDemo.StudentWithOverride sw3 = new SetDemo.StudentWithOverride(3,"王二麻子",29);
        SetDemo.StudentWithOverride sw4 = new SetDemo.StudentWithOverride(2,"李四",18);
        Map<StudentWithOverride, String> map3 = new HashMap<>();
        map3.put(sw1, "sw1");
        map3.put(sw2, "sw2");
        map3.put(sw3, "sw3");
        map3.put(sw4, "sw4");
        System.out.println(map3);
        // {{id=3, name='王二麻子', age=29}=sw3, {id=1, name='张三', age=23}=sw1, {id=2, name='李四', age=18}=sw4}
        // StudentWithOverride重写equals和hashCode使得sw2和sw4重复的key不能存到同一个map里
    }
}

从结果中可以分析得出:
String和StudentWithOverride都重写了equals和hashCode方法,所以使用HashMap是输出的结果是正常的,StudentNonOverride输出的结果是错误的,因为有重复的key出现。

TreeMap示例代码

public class Demo03TreeMap {
    public static void main(String[] args) {
        treeMapDemo();
    }

    public static void treeMapDemo(){
        String s1 = "张三";
        String s2 = "李四";
        String s3 = "王二麻子";
        String s4 = "李四";
        Map<String, String> map1 = new TreeMap<>();
        map1.put(s1, "s1");
        map1.put(s2, "s2");
        map1.put(s3, "s3");
        map1.put(s4, "s4");
        System.out.println(map1); // {张三=sn1, 李四=s4, 王二麻子=s3} 排除重复key

        /*SetDemo.StudentNonOverride sn1 = new SetDemo.StudentNonOverride(1,"张三",23);
        SetDemo.StudentNonOverride sn2 = new SetDemo.StudentNonOverride(2,"李四",18);
        SetDemo.StudentNonOverride sn3 = new SetDemo.StudentNonOverride(3,"王二麻子",29);
        SetDemo.StudentNonOverride sn4 = new SetDemo.StudentNonOverride(2,"李四",18);
        Map<StudentNonOverride, String> map2 = new TreeMap<>(); // 抛异常java.lang.ClassCastException 因为没有排序器
        map2.put(sn1, "sn1"); // 类StudentNonOverride没有实现排序接口,因此可以通过传入一个排序器来解决
        map2.put(sn2, "sn2");
        map2.put(sn3, "sn3");
        map2.put(sn4, "sn4");
        System.out.println(map2);*/

        SetDemo.StudentNonOverride sn1 = new SetDemo.StudentNonOverride(1,"张三",23);
        SetDemo.StudentNonOverride sn2 = new SetDemo.StudentNonOverride(2,"李四",18);
        SetDemo.StudentNonOverride sn3 = new SetDemo.StudentNonOverride(3,"王二麻子",29);
        SetDemo.StudentNonOverride sn4 = new SetDemo.StudentNonOverride(2,"李四",18);
        Map<StudentNonOverride, String> map2 = new TreeMap<>(new Comparator<StudentNonOverride>() {
            @Override
            public int compare(StudentNonOverride o1, StudentNonOverride o2) {
                return o1.getAge() - o2.getAge(); // 根据年龄排序
            }
        });
        map2.put(sn1, "sn1");
        map2.put(sn2, "sn2");
        map2.put(sn3, "sn3");
        map2.put(sn4, "sn4");
        System.out.println(map2);
        // {{id=2, name='李四', age=18}=sn4, {id=1, name='张三', age=23}=sn1, {id=3, name='王二麻子', age=29}=sn3}

        SetDemo.StudentWithOverride sw1 = new SetDemo.StudentWithOverride(1,"张三",23);
        SetDemo.StudentWithOverride sw2 = new SetDemo.StudentWithOverride(2,"李四",18);
        SetDemo.StudentWithOverride sw3 = new SetDemo.StudentWithOverride(3,"王二麻子",29);
        SetDemo.StudentWithOverride sw4 = new SetDemo.StudentWithOverride(2,"李四",18);
        Map<StudentWithOverride, String> map3 = new TreeMap<>();
        map3.put(sw1, "sw1");
        map3.put(sw2, "sw2");
        map3.put(sw3, "sw3");
        map3.put(sw4, "sw4");
        System.out.println(map3);
        // {{id=2, name='李四', age=18}=sw4, {id=1, name='张三', age=23}=sw1, {id=3, name='王二麻子', age=29}=sw3}
        // 可以看到只有3个元素,相同的key被排除了,因为StudentWithOverride实现了Comparable接口并重写了compareTo方法
    }
}

结果分析:
String和StudentWithOverride都实现了接口Comparable并重写了compareTo方法,所以使用TreeMap是正常的,至于StudentNonOverride没有实现排序器类,直接会抛异常,因此下面我又列举了通过匿名内部类Comparator接口重写compare方法传入Set中的方法定义了比较器,之后TreeMap就正常了

总结

线程安全问题

上面介绍的这些容器都不是同步的,即不是线程安全的,如果在多线程中使用,可以通过Collections工具类中的synchronizedCollection()方法使得线程安全,但是不推荐这种方法,因为现在有更高级更简便的线程安全容器类,封装在java.util.concurrent包内,每一种容器都有对应的线程安全容器。之后学到多线程时,可以再深入研究。

不常用容器简介

继承与List的Vector和它的子类Stack,Vector本来也是一种数组容器,但是它是同步的线程安全的,内置了锁,效率比较低,因此已经弃用了,同时它的子类stack自然也就弃用了,现在我们使用Deque实现栈。
同样比较容易被提到的是HashTable,这个也是个哈希Map其实,但是它是线程安全带的,而且是通过锁机制来实现,所以也不推荐用了。

常见容器的横向纵向比较
  • List、Set、Map都什么区别?
  • ArrayList和LinkedList什么区别?实现原理分别是什么?
  • HashSet和TreeSet什么异同点?实现原理分别是什么?
  • HashMap和TreeMap什么异同点?实现原理分别是什么?
  • HashSet和HashMap什么异同点?实现原理分别是什么?
  • TreeSet和TreeMap什么异同点?实现原理分别是什么?
  • Comparable和Comparator区别是什么?有什么用?
  • compareTo和compare什么区别?有什么用?
  • ArrayList默认容量?什么时候扩容?扩容几倍?
  • LinkedList有默认大小吗?需要扩容吗?为什么它可以通过下标index访问?
  • HashMap的默认大小是多少?什么情况下会扩容?装填因子是多少?如何设置装填因子才合理?出现哈希冲突怎么解决的?
  • HashMap装填因子为什么选0.75?