Java集合进阶

目录

一、体系结构

1.集合体系结构

​编辑

2.单列集合体系结构

二、单列集合

1.Collection集合的使用

[(1) add 方法](#(1) add 方法)

[(2) clear方法](#(2) clear方法)

(3)remove方法

[(4)contains 方法](#(4)contains 方法)

(5)isEmpty方法

(6)size方法

2.Collection集合的通用遍历方式

(1)迭代器遍历

[(2)增强 for 遍历](#(2)增强 for 遍历)

(3)Lambda表达式遍历

3.List集合的使用

(1)add方法

(2)remove方法

(3)set方法

(4)get方法

4.List集合的遍历方式

[(1)普通 for 循环遍历](#(1)普通 for 循环遍历)

(2)列表迭代器遍历

[① hasPrevious 和 previous](#① hasPrevious 和 previous)

[② add](#② add)

(3)五种遍历方式对比

5.ArrayList的底层源码分析

6.LinkedList集合的底层源码分析

7.Iterator迭代器的底层源码分分析

8.Set集合的使用

9.HashSet集合的底层原理

(1)哈希值

(2)底层原理

​编辑

(3)三个问题

10.LinkedHashSet的底层原理

11.TreeSet集合的底层原理

12.单列集合的使用场景

三、双列集合

1.Map集合的使用

(1)put方法

(2)remove方法

(3)clear方法

(4)containsKey和containsValue方法

(5)isEmpty方法

(6)size方法

2.Map集合的遍历方式

(1)键找值

(2)键值对

(3)Lambda表达式

3.HashMap集合的使用

4.linkedHashMap集合的使用

5.TreeMap集合的使用

[7. 双列集合的使用场景](#7. 双列集合的使用场景)

四、可变参数

1.引言

2.定义

3.细节

五、Collections集合工具类

1.定义

2.方法

(1)addAll和shuffle方法

(2)其他方法

六、不可变集合

1.定义

2.书写格式

[(1)不可变的 List 集合](#(1)不可变的 List 集合)

[(2)不可变的 Set 集合](#(2)不可变的 Set 集合)

[(3)不可变的 Map 集合](#(3)不可变的 Map 集合)


一、体系结构

1.集合体系结构

在java中,集合分为两种:单列集合 和 多列集合

单列集合:每次只能添加一个数据

双列集合:每次添加的是一对数据

2.单列集合体系结构

单列集合分为两种:List系列 和 Set系列

List系列集合: 添加的元素是有序,可重复,有索引的

Set系列集合: 添加的元素是无序,不可重复,无索引的

二、单列集合

1.Collection集合的使用

Collection是单列集合的顶层接口,它的功能是全部单列集合都可以使用的(共性的)。

由于Collection是一个接口, 我们不能直接创建他的对象。

所以,调用方法时,只能创建他的实现类对象。

(1) add 方法
public class Demo {
    public static void main(String[] args) {
        //实现类:ArrayList
        Collection<String> coll = new ArrayList<>();

        //1.添加元素
        coll.add("aaa");
        coll.add("bbb");
        coll.add("ccc");
        System.out.println(coll);//[aaa, bbb, ccc]

    }
}

细节:

① 添加元素时,如果是往 List 系列集合中添加元素,那么返回值永远为true,因为List系列是允许重复的。

② 添加元素时,如果是往 Set 系列集合中添加元素:

如果要添加的元素不存在,返回值为 true;

如果要添加的元素已经存在,返回值为false。

因为 Set 系列集合中的元素是不可重复的。

(2) clear方法
public class Demo {
    public static void main(String[] args) {
        //实现类:ArrayList
        Collection<String> coll = new ArrayList<>();

        //1.添加元素
        coll.add("aaa");
        coll.add("bbb");
        coll.add("ccc");
        System.out.println(coll);//[aaa, bbb, ccc]

        //2.清空元素
        coll.clear();
        System.out.println(coll);//[]

    }
}
(3)remove方法
public class Demo {
    public static void main(String[] args) {
        //实现类:ArrayList
        Collection<String> coll = new ArrayList<>();

        //1.添加元素
        coll.add("aaa");
        coll.add("bbb");
        coll.add("ccc");
        System.out.println(coll);//[aaa, bbb, ccc]


        //3.删除
        System.out.println(coll.remove("aaa"));//true
        System.out.println(coll);//[bbb, ccc]
    }
}

细节:

① 删除元素时,由于 Collection 是顶层接口,只能定义共性的方法。所以删除方法只能通过元素的对象删除,不能通过索引删除,因为Set 集合是无索引的。

虽然 ArrayList 中也有remove的重载方法,可以根据索引删除。

但是该实现类对象是由接口多态创建的,遵循"编译看左边,运行看右边",所以不能调用实现类的独有方法。

② 删除元素时,如果要删除的元素存在,则删除成功,返回true;

如果要删除的元素不存在,则删除失败返回false。

(4)contains 方法
public class Demo {
    public static void main(String[] args) {
        //实现类:ArrayList
        Collection<String> coll = new ArrayList<>();

        //1.添加元素
        coll.add("aaa");
        coll.add("bbb");
        coll.add("ccc");
        System.out.println(coll);//[aaa, bbb, ccc]

        //4.判断元素是否包含
        System.out.println(coll.contains("aaa"));//true
        System.out.println(coll.contains("ddd"));//false

    }
}

细节:

底层是通过for循环依次遍历,依赖 equals 方法进行判断是否存在的。

所以,如果集合中存储的对象是自定义类,想要通过 contains 方法判断是否包含,那么必须在该自定义类中,重写 equals 方法,使其判断的不是地址值,而是内部的属性值。

这里可以正常判断的原因是,String 类本身就已经重写好了 equals 方法,使其比较的是字符串的值,而不是地址值。

Student类:

public class Student {
    private String name;
    private int age;
    public Student() {
    }

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

    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;
    }
}

测试类:

public class Demo2 {
    public static void main(String[] args) {
        Collection<Student> coll = new ArrayList<>();

        Student s1=new Student("zhangsan",23);
        Student s2=new Student("lisi",24);

        coll.add(s1);
        coll.add(s2);

        //创建一个一模一样的对象,判断是否存在集合中
        Student s3=new Student("zhangsan",23);
        System.out.println(coll.contains(s3));//false
    }
}

由于Student类中并没有重写equals方法,所以默认使用父类Object 类中的 equals 方法。

而Object 类中的 equals 方法比较的是地址值,由于s1和s3的地址值不相等,所以结果为false。

**************************************************************************************************************

重写后的Student类:

public class Student {
    private String name;
    private int age;
    public Student() {
    }

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

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

    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;
    }
}

这时,由于重写了Student类中equals方法,改为比较内部属性值,结果才为true。

(5)isEmpty方法
public class Demo {
    public static void main(String[] args) {
        //实现类:ArrayList
        Collection<String> coll = new ArrayList<>();

        //1.添加元素
        coll.add("aaa");
        coll.add("bbb");
        coll.add("ccc");

        //5.判断集合是否为空
        boolean result=coll.isEmpty();
        System.out.println(result);//false;

    }
}

细节:

isEmpty方法的底层事实上就是判断集合的长度是否为0。

(6)size方法
public class Demo {
    public static void main(String[] args) {
        //实现类:ArrayList
        Collection<String> coll = new ArrayList<>();

        //1.添加元素
        coll.add("aaa");
        coll.add("bbb");
        coll.add("ccc");

        //6.获取集合的长度
        int size = coll.size();
        System.out.println(size);//3

    }
}

底层直接返回 ArrayList 类的成员变量 size即可

2.Collection集合的通用遍历方式

由于Collection是单列集合的顶级接口,所以遍历得兼容 List系列和 Set 系列。

而 Set集合是不含索引的,所以不能直接用 for 循环进行遍历。

需要采用一种通用的方式,使得 List 和 Set 都能给够进行遍历。

(1)迭代器遍历

迭代器在Java当中的接口是 Iterator,迭代器是集合专门的遍历方式,是不依赖索引的。

步骤:

① 创建迭代器对象后,迭代器对象默认指向集合的 0 索引。

② 然后调用 hasNext 方法判断当前位置是否有元素。

③ 最后调用 next 方法获取当前的元素,迭代器对象向后移动,指向下一个位置。

public class IteratorDemo {
    public static void main(String[] args) {
        Collection<String> coll = new ArrayList<>();

        coll.add("aaa");
        coll.add("bbb");
        coll.add("ccc");

        //1.获取迭代器对象(类似指针,默认指向0索引)
        Iterator<String> it = coll.iterator();
        //2.利用循环不断地获取集合中的每一个元素
        while (it.hasNext()) {
            //3.next方法做了两件事情:获取元素 + 移动指针
            String str = it.next();
            System.out.println(str);
        }
    }
}

注意点:

① 如果 hasNext 方法已经返回 false,则不能再调用 next 方法,否则报错 NoSuchElementException

② 迭代器遍历完毕后,指针仍指向结束位置,不会复位。

如果想要二次遍历集合,只能重新创建新的迭代器对象。

public class IteratorDemo {
    public static void main(String[] args) {
        Collection<String> coll = new ArrayList<>();

        coll.add("aaa");
        coll.add("bbb");
        coll.add("ccc");

        Iterator<String> it1 = coll.iterator();
        while (it1.hasNext()) {
            String str = it1.next();
            System.out.println(str);
        }

        //当上述代码执行完毕后,迭代器的指针已经指向结束位置,不存在任何元素
        System.out.println(it1.hasNext());//false

        //二次遍历
        Iterator<String> it2 = coll.iterator();
        while (it2.hasNext()) {
            String str = it2.next();
            System.out.println(str);
        }
    }
}

③ hasNext 方法要和 next 方法配套使用,即循环中只能使用一次 next 方法。

public class IteratorDemo2 {
    public static void main(String[] args) {
        Collection<String> coll = new ArrayList<>();

        coll.add("aaa");
        coll.add("bbb");
        coll.add("ccc");

        Iterator<String> it = coll.iterator();
        while (it.hasNext()) {
            System.out.println(it.next());
            System.out.println(it.next());
        }
    }
}

集合中只有三个元素,每次循环却调用两次 next 。

在第二次循环的第一次 next 后,指针就已经移到最终位置了。

这时再 next 就会报错 NoSuchElementException。

**结论:**hasNext 方法要和 next 方法配套使用,即一次 hasNext,后面跟着一次 next。

**************************************************************************************************************

Tips:因为迭代器遍历时是不依赖索引的,如果想要反复使用某一元素,需要使用变量保存下来。

因为 next 获取元素之后,指针就后移了,已经不会指向该元素了。

public class IteratorDemo2 {
    public static void main(String[] args) {
        Collection<String> coll = new ArrayList<>();

        coll.add("aaa");
        coll.add("bbb");
        coll.add("ccc");

        Iterator<String> it = coll.iterator();
        while (it.hasNext()) {
            String str = it.next();
            System.out.println(str);//aaa bbb ccc
            System.out.println(str);//aaa bbb ccc
            System.out.println(str);//aaa bbb ccc
        }
    }
}

④ 迭代器遍历时,不能用集合的方法进行增加或者删除,会导致并发修改异常。遍历结束时,可以正常修改集合。

解决办法:

对于删除,可以使用迭代器 Iterator 接口提供的 remove 方法进行删除。

对于添加,"暂时" 没有办法。

public class IteratorDemo2 {
    public static void main(String[] args) {
        Collection<String> coll = new ArrayList<>();

        coll.add("aaa");
        coll.add("bbb");
        coll.add("ccc");
        coll.add("ddd");

        Iterator<String> it = coll.iterator();
        while (it.hasNext()) {
            String str = it.next();

            if ("bbb".equals(str)) {
                it.remove();
            }
        }
        System.out.println(coll);//[aaa, ccc, ddd]
    }
}
(2)增强 for 遍历

增强 for 的底层实质上就是一个迭代器,是为了简化迭代器的代码而书写的。

JDK5以后诞生,其内部原理就是一个 Iterator 迭代器。

所有的单列集合和数组才能用增强 for 进行遍历。

public class ForDemo {
    public static void main(String[] args) {
        Collection<String> coll = new ArrayList<>();

        coll.add("aaa");
        coll.add("bbb");
        coll.add("ccc");
        coll.add("ddd");

        for (String s : coll) {
            System.out.println(s);
        }
    }
}

细节:

增强 for 中的变量 s,只是一个临时变量,表示集合中的每一个数据,相当于 String s = it.next();

所以,修改增强 for 中的变量,是不会对集合本身的数据造成影响的。

public class ForDemo {
    public static void main(String[] args) {
        Collection<String> coll = new ArrayList<>();

        coll.add("aaa");
        coll.add("bbb");
        coll.add("ccc");
        coll.add("ddd");

        for (String s : coll) {
            s = "qqq";
        }

        System.out.println(coll);//[aaa, bbb, ccc, ddd]
    }
}
(3)Lambda表达式遍历

JDK8开始后,诞生了Lambda表达式,从而提供了一种更简单,更直接的遍历集合的方式。

该种遍历方式需要依赖 forEach 方法。

通过源码可以发现,forEach 底层是通过 for 循环进行遍历的。

然后遍历集合,根据索引调用 element(es,i) 方法得到的集合中的每一个元素,交给 accept 方法。

forEach 方法的形参是一个 Consumer 类型。

我们发现,Consumer 是一个接口,而且是一个函数式接口,表明它可以使用 Lambda 表达式 。

public class ForEachDemo {
    public static void main(String[] args) {
        Collection<String> coll = new ArrayList<>();

        coll.add("aaa");
        coll.add("bbb");
        coll.add("ccc");
        coll.add("ddd");

        //1.使用匿名内部类的形式
        coll.forEach(new Consumer<String>() {
            @Override
            //s 表示集合中遍历得到的每一个元素
            public void accept(String s) {
                System.out.println(s);
            }
        });

        //2.使用Lambda表达式的形式
        coll.forEach(s -> System.out.println(s));
    }
}

可以发现,使用匿名内部类时,重写的正是accept 方法。

而 accept 方法的形参 s,正是 for 循环中通过 element(es,i) 方法得到的,表示集合中的每一个元素。


**question1:**可能有人会奇怪,不是说 Collection 中定义的都是共性的方法吗?怎么 forEach 方法底层使用的是普通 for 。Set集合不是没有索引,不能使用普通 for 吗?

回答: 请注意,由于我们是使用接口多态的方式创建的对象,所以查看源码要看实现类。

多态的运行规则遵循 "编译看左边,运行看右边"。

所以上面我们实现类是 ArrayList,查看的也是 ArrayList 类中 forEach 方法。

对于 ArrayList 的它是有索引的,所以可以采用普通 for 进行遍历。


**question2:**那如果这样的话,Set 集合又没有索引,怎么使用 forEach 进行遍历呢?而且对于 Set 集合的实现类,例如 HashSet 中,也没有重写这个 forEach 方法。

回答:

刚刚也说了,编译看左边,运行看右边。

在已知右边可能并不存在 forEach 的重写方法的情况下,相信应该也能猜到。

左边定义的 forEach 方法可能并不是一个抽象方法,而是一个有方法体的方法。

当右边(实现类)没有重写左边(接口)中的某个方法,那么在实现类对象调用这个方法时,就会执行接口中原来已经实现的那个方法。

可能很快又有人发现,左边 Collection 接口中,也没有 forEach 方法呀,这是为什么?

不要忘了,接口与接口之间,是有继承关系的,可以单继承,也可以多继承。

而 Collection 接口继承于 Iterable 接口。

在 Iterable 接口中,定义了一个默认方法 forEach,不强制实现类进行重写。

这个 forEach 采用增强 for 进行遍历,同样也将遍历到的每个元素 t ,交给 accpet 方法进行处理。

这样对于没有索引的Set 集合,也可以进行遍历了。

所以,具体的继承和实现关系,如下图所示。

结论: 以后在看源码时,如果是利用多态创建的对象,要去看其实现类的源码。

如果实现类中没有,再顺着实现/继承关系继续往上找。

3.List集合的使用

List集合的特点:

① 有序:存和取的元素顺序一致

② 有索引:可以通过索引操作元素

③ 可重复:存储的元素可以重复

由于 Collection 是单列集合的顶层接口,所以 Collection 中的方法,List都继承了。

而 List 集合是有索引的,所以多了很多索引操作的方法。

由于 List 本身也是一个接口,所以不能直接创建对象,需要创建其实现类对象。

(1)add方法
public class Demo {
    public static void main(String[] args) {
        List<String> list=new ArrayList<>();

        list.add("aaa");
        list.add("bbb");
        list.add("ccc");

        //1.add  在指定位置插入元素
        list.add(1,"ddd");
        System.out.println(list);//[aaa, ddd, bbb, ccc]
    }
}

细节:

add 方法如果不加索引的话,调用的是继承自 Collection 重写的 add 方法,默认将元素加在末尾。

add 方法形参中有索引的话,调用的是重载的add方法。

(2)remove方法
public class Demo {
    public static void main(String[] args) {
        List<String> list=new ArrayList<>();

        list.add("aaa");
        list.add("bbb");
        list.add("ccc");

        //2.remove  删除指定索引处的元素,并返回被删除的元素
        String s=list.remove(0);
        System.out.println(s);//aaa
    }
}

同理,如果 remove 中参数是一个对象,则调用的是继承自 Collection 重写的 remove 方法。

如果 remove 中参数是一个索引,则调用的是重载的 remove 方法。

**************************************************************************************************************

**Test:**如果集合中的元素是 Integer整数类型,那么调用 remove 方法,即下列结果如何?是删除了 1 这个元素,还是删除了索引为 1 的元素。

public class RemoveDemo {
    public static void main(String[] args) {
        List<Integer> list=new ArrayList<>();

        list.add(1);
        list.add(2);
        list.add(3);

        list.remove(1);
        System.out.println(list);
    }
}

运行结果:

原因:

在方法调用的时候,如果方法出现了重载现象,那么会优先调用实参和形参一致的那个方法。

remove(1)中的实参 1 是 int 类型,所以会调用重载的 remove 方法,删除索引为1的元素。

而继承自 Collection 重写的 remove 方法的形参是一个 Integer 类型,所以不会优先调用。

**************************************************************************************************************

**question:**那如果我就想删除元素1呢?如何实现?

方法一:list.remove(0);

方法二:手动装箱,把基本数据类型的 1 ,变成 Integer 类型.

public class RemoveDemo {
    public static void main(String[] args) {
        List<Integer> list = new ArrayList<>();

        list.add(1);
        list.add(2);
        list.add(3);

        Integer i = Integer.valueOf(1);
        list.remove(i);
        System.out.println(list);//[2, 3]
    }
}
(3)set方法
public class Demo {
    public static void main(String[] args) {
        List<String> list = new ArrayList<>();

        list.add("aaa");
        list.add("bbb");
        list.add("ccc");

        //1.add  在指定位置插入元素
        list.add(1, "ddd");
        System.out.println(list);//[aaa, ddd, bbb, ccc]

        //3.set  修改指定索引处的元素,返回被修改的元素
        String resullt = list.set(0, "qqq");
        System.out.println(list);//[qqq, ddd, bbb, ccc]
    }
}
(4)get方法
public class Demo {
    public static void main(String[] args) {
        List<String> list = new ArrayList<>();

        list.add("aaa");
        list.add("bbb");
        list.add("ccc");

        //1.add  在指定位置插入元素
        list.add(1, "ddd");
        System.out.println(list);//[aaa, ddd, bbb, ccc]

        //4.get  返回指定索引处的值
        String s = list.get(0);
        System.out.println(s);//aaa
    }
}

4.List集合的遍历方式

List 集合继承自 Collection 集合,所以之前 Collection 的三种通用遍历方式均可使用。

除此之外,List 集合还可以使用 列表迭代器遍历 和 普通 for 循环遍历 。

(1)普通 for 循环遍历
public class Demo2 {
    public static void main(String[] args) {
        List<String> list = new ArrayList<>();

        list.add("aaa");
        list.add("bbb");
        list.add("ccc");

        //1.普通for循环遍历
        for (int i = 0; i < list.size(); i++) {
            String s = list.get(i);
            System.out.println(s);
        }
    }
}
(2)列表迭代器遍历

列表迭代器在Java当中的接口是 ListIterator,继承自迭代器接口 Iterator,所以其方法也能够使用。

此外,ListIterator 还新增了几个方法。

① hasPrevious 和 previous

该两个方法和之前的 hasNext 和 next 正好相反,前面是向下一个移动,这个是向前一个移动。

但是,一开始也是默认指向 0 位置,这时不能直接调用 previous,否则报错。

② add

之前提到,迭代器遍历时,不能用集合的方法进行增加或者删除,会导致并发修改异常。

对于删除,可以使用迭代器 Iterator 接口提供的 remove 方法进行删除,这里 ListIterator 同样也继承了该方法。

对于添加,之前是不行的。但现在 ListIterator 新增了 add 方法,是可以做到添加元素的。

public class Demo2 {
    public static void main(String[] args) {
        List<String> list = new ArrayList<>();

        list.add("aaa");
        list.add("bbb");
        list.add("ccc");

        //2.列表迭代器
        ListIterator<String> it = list.listIterator();
        while (it.hasNext()) {
            String s = it.next();
            if ("bbb".equals(s)) {
                it.add("qqq");
            }
        }
        System.out.println(list);//[aaa, bbb, qqq, ccc]
    }
}
(3)五种遍历方式对比

5.ArrayList的底层源码分析

① 最初情况下,空参创建了一个集合,在底层创建了一个默认长度为 0的数组。

② 添加第一个元素时,底层会创建一个长度为 10 的新数组,默认初始化为 null。

注意:size不光表示数组中元素的个数,也表示下次元素要存入的位置。

③ 存满时,会扩容 1.5 倍。

④ 如果一次添加多个元素,会调用 addAll 方法,1.5 倍还放不下,则新创建的数组长度以实际为准。

注意:

如果只说 ArrayList 底层的扩容机制是 自动扩容为1.5倍,这样是不严谨的。

在插入多个数据的时候,1.5倍可能不够,此时会以实际长度为准。

ArrayList 进行数组拷贝的时候调用的是 Arrays 类的 copyof 方法,但该方法底层实质上调用的是System 类的 arraycopy 方法。

6.LinkedList集合的底层源码分析

LinkedList底层数据结构是双链表,查询慢,增删快。

双链表可以直接对表头和表尾进行操作,因此 LinkedList本身多了很多直接操作首尾元素特有的API。

这些方法并不常用,因为我们也可以使用从 List 和 Collection 接口继承下来的方法。

**************************************************************************************************************

首先,LinkedList 类中定义了一个静态内部类 Node,表示链表中的节点 。

节点有三个属性值,分别是 前驱指针,当前元素,后继指针。

LinkedList 同时也定义了一个头指针和尾指针,指向第一个节点和最后一个节点。

起始时,first 和 last 都定义为(指向) null,插入第一个节点后,两个指针都指向了该节点。

插入第二个节点时,头指针不发生改变,只改变尾指针。

后续插入的原理类似,实质上就是节点的指针会发生变化而已。

7.Iterator迭代器的底层源码分分析

首先,Iterator 是一个接口,是不能直接创建对象的。

这个接口定义了一些方法,如 hasNext 和 next。

在每个集合类中,都定义了一个内部类 Itr 用于实现 Iterator 接口。

同时定义了一个 iterator 方法,用于获取迭代器对象。

在内部类 Itr 中,有两个成员变量:

cursor :游标,也就是迭代器中的指针。由于没有赋值,所以默认初始化为 0,即默认指向 0 索引

lastRet:表示上一次(刚刚)操作的元素的索引。

初始情况下,cursor 为 0 。

调用 hasNext 方法会对游标进行校验,判断 cursor 是否不等于集合长度。

调用 next 方法 ,cursor会进行二次校验,不合法会抛出异常。然后 cursor 和 lastRet 会向后移动,将元素插入到底层数组中。

当已经遍历到最后一个元素时,cursor 会等于 size,此时 hasNext 会返回 false,next 也会抛出异常。

我们注意到内部类 Itr 的成员变量中,还有一个变量 modCount ,它代表集合变化的次数。

对集合每 add 或 remove 一次,modCount 都会 ++。

我们在创建迭代器对象时,会将这个次数告诉迭代器。

迭代器在遍历的过程中,会调用 checkForComodification 方法校验次数是否正确,即该次数是否和一开始记录的次数 expectedModCount 相同。

如果不同,代表遍历过程中,使用了集合的方法添加或删除元素,此时就会导致并发修改异常。

如果相同,证明当前集合没有发生改变,可以正常遍历。

8.Set集合的使用

特点:

① 无序:存取顺序不一致

② 不重复:每个元素都是唯一的(可以利用该性质进行去重)

③ 无索引:没有带索引的方法,所以不能使用普通 for 进行遍历,也不能通过索引来获取元素。

实现类:

HashSet:无序,不重复,无索引

LinkedHashSet:有序,不重复,无索引

TreeSet:可排序,不重复,无索引


Set 接口继承自 Collection 接口,因此 Collection 中的方法 Set 都可以使用。

Set 中没有什么额外的方法需要学习,直接使用 Collection 中的方法即可。

Set 集合也是一个接口,所以我们不能直接创建其对象,需要创建其实现类对象。

public class Demo {
    public static void main(String[] args) {
        //1.创建一个Set集合的对象
        Set<String> set = new HashSet<>();

        //2.添加元素
        boolean res1 = set.add("aaa");
        boolean res2 = set.add("aaa");
        set.add("bbb");
        set.add("ccc");

        //不重复
        System.out.println(res1);//true
        System.out.println(res2);//false
        //无序
        System.out.println(set);//[aaa, ccc, bbb]

        //3.遍历集合(无序)
        for (String s : set) {
            System.out.println(s);//aaa ccc bbb
        }
    }
}

细节:

① 由于 Set 集合的不可重复性,所以调用 add 方法:

如果要添加的元素不存在,返回值为 true;

如果要添加的元素已经存在,返回值为false。

② 由于 Set 集合的无序性,所以无论直接打印 Set,还是通过遍历,都可能和存入的顺序不一致。

9.HashSet集合的底层原理

(1)哈希值

① 哈希值是根据 hashCode 方法所计算出来的 int 类型的整数。

② 该方法定义在 Object类中,所有对象都可以调用,默认使用地址值进行计算。

由于使用地址值进行计算,所以不同对象计算的哈希值是不一样的。

③ 我们希望不同的对象属性值相同,就代表这个对象已经重复,而并不希望根据地址值来判断。

所以一般情况下,会重写 hashCode 方法,改为利用对象内部的属性值来计算哈希值。

Student类:

public class Student {
    private String name;
    private int age;

    public Student() {
    }

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

    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;
    }
}

测试类:

public class HashDemo {
    public static void main(String[] args) {
        Student s1=new Student("zhangsan",23);
        Student s2=new Student("zhangsan",23);

        //1.如果没有重写hashCode方法,不同对象计算出的哈希值是不同的
        System.out.println(s1.hashCode());
        System.out.println(s2.hashCode());
    }
}

运行结果:

此时由于 s1 和 s2 的地址值并不相同,所以计算得到的哈希值也是不一样。

我们想要改为根据属性值进行计算哈希值,就必须重写 hashCode 方法。

运行结果:


在小部分情况下,不同的属性值或者不同的地址值计算出来的哈希值,是有可能相同的,这个情况被称为 "哈希碰撞"。

由于 int 取值范围一共就 42 个亿多,这时如果创建 50 亿个对象,int 的范围必然支撑不住,所以可能会出现哈希碰撞的情况,但这种可能性是极小的。

对于String类,可以发现内部已经重写了 hashCode 方法,使其可以根据字符串来计算哈希值。

但对于不同的字符串 "abc" 和 "acD" ,计算出的哈希值却是一样的,发生了哈希碰撞。

(2)底层原理

HashSet 集合底层采取哈希表存储元素,哈希表设计一种对于增删改查数据性能都很好的结构。

哈希表组成:

JDK8 之前:数组 + 链表

元素存入的位置会根据数组长度和哈希值进行计算,公式如下:

如果存入位置已经有元素,会调用 equals 方法,和链表中的所有元素进行 一 一比较 。

当 数组中的元素为 16 (数组长度 ) x 0.75 (加载因子) = 12 时,数组则会触发扩容,长度变为原来的两倍,即 16 x 2 = 32。


JDK8 开始:数组 + 链表 + 红黑树

和JDK8之前的区别

① 元素存入数组中的链表,由头插法变为尾插法。

② 当数组中,有某个链表的长度 > 8 而且 数组长度 >= 64 时,当前的链表会自动转变成红黑树。

所以说 数组,链表,红黑树三种结构可同时存在。

注:

如果集合中存储的是自定义对象 ,必须要重写 hashCode 和 equals 方法

hashCode :用于根据对象的属性值计算哈希值,从而进一步计算出该元素存入数组中的位置。

equals:用于当数组中存入的位置已经有元素了,和该位置的链表中的每个元素根据属性值进行 一 一比较。

(3)三个问题

问题一:HashSet 为什么存和取的顺序不一样?

HashSet 在遍历时,数组会按照从左到右,从上到下的顺序进行遍历,如上图。

因为在存入元素时,未必就一定按照这个顺序进行存,所以取的时候结果就有可能不一致了。


问题二:HashSet 为什么没有索引?

虽然说数组的确是有索引的,但是数组中的每个位置都存放着一个链表或红黑树,包含多个元素。

在这三个数据结构的组合下, 根本没有办法去定义元素的索引,所以只能取消索引了。


问题三:HashSet 是利用什么机制去保证数据去重的呢?

① 利用 hashCode 方法计算哈希值(自定义类需要重写,不重写根据对象地址值计算),从而确定当前元素在数组中存放的位置。

② 再利用 equals 方法去比较对象内部中的属性值是否相同(自定义类需要重写,不重写比较对象地址值)。

10.LinkedHashSet的底层原理

特点:有序,不重复,无索引

**question1:**LinkedHashSet 是如何实现有序(存取顺序一致)的呢?

原理: 底层数据结构依然是哈希表,只是每个元素又额外多了一个双链表 的机制用于记录存储的顺序。

所以在打印或者遍历时,都是根据 头指针 head 来进行一个一个遍历的,从而实现了有序。


**question2:**以后如果要数据去重,我们使用哪个?

一般默认使用 HashSet,如果要求去重且存取有序,才使用 LinkedHashSet 。

因为 LinkedHashSet 虽然实现了有序,但额外定义了一个双链表,所以底层相比于 HashSet,效率要低一点。

所以优先选择 HashSet,虽然无序,但效率高。

11.TreeSet集合的底层原理

特点:可排序(按照元素的默认规则进行排序),不重复,无索引

注: TreeSet 集合底层是基于红黑树的数据结构实现排序的,增删改查性能都较好。

排序的默认规则:

① 对于数值类型:Integer,Double,默认按照从小到大的顺序进行排序。

public class TreeSetDemo {
    public static void main(String[] args) {
        //1.创建TreeSet集合对象
        TreeSet<Integer> ts = new TreeSet<>();

        //2.添加元素
        ts.add(4);
        ts.add(5);
        ts.add(2);
        ts.add(3);
        ts.add(1);

        //3.打印集合
        System.out.println(ts);//[1, 2, 3, 4, 5]

        //4.遍历集合
        Iterator<Integer> it = ts.iterator();
        while (it.hasNext()) {
            int i = it.next();
            System.out.println(i);//1 2 3 4 5
        }
    }
}

② 对于字符、字符串类型:按照字符在 ASCII 码表中的数字升序进行排序。

③ 对于自定义类,需要指定比较规则。

方式一:(默认排序/自然排序) JavaBean类实现 Compareable 接口指定比较规则**。**

Student类:

//实现Comparable接口,由于定义的是Student对象间的排序规则,所以泛型直接限定类型
public class Student implements Comparable<Student> {
    private String name;
    private int age;

    public Student() {
    }

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

    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 "Student{name = " + name + ", age = " + age + "}";
    }

    //重写compareTo方法,指定比较规则
    @Override
    public int compareTo(Student o) {
        //按照年龄升序排序
        return this.getAge() - o.getAge();
    }
}

细节:

① 实现Comparable接口时,由于定义的是Student对象间的排序规则,所以泛型直接限定类型。

② 由于TreeSet底层数据结构是红黑树,不是哈希表,所以不用重写 hashCode 和 equals 方法。

③ 对于 compareTo 方法:

I. this:表示当前要添加的元素

II. o :表示已经在红黑树中存在的元素

返回值:

I. 负数:表示当前要插入的元素 < 已排好的元素,插在该元素的左子树

II. 正数:表示当前要插入的元素 > 已排好的元素,插在该元素的右子树

III. 0 : 表示当前要插入的元素 = 已排好的元素,即该元素已存在,舍弃

fang'sh

public class TreeSetSort {
    public static void main(String[] args) {
        //1.创建四个学生对象
        Student s1 = new Student("wangwu", 25);
        Student s2 = new Student("lisi", 24);
        Student s3 = new Student("zhangsan", 23);
        Student s4 = new Student("zhaoliu", 26);

        //2.创建集合对象
        TreeSet<Student> ts=new TreeSet<>();

        //3.添加元素
        ts.add(s1);
        ts.add(s2);
        ts.add(s3);
        ts.add(s4);

        //4.打印集合
        System.out.println(ts);
    }
}

运行结果:

底层红黑树变化(省略叶子节点):

为了方便理解,这里附上红黑树的规则 和 添加节点时红黑树的调整规则:


方式二: (比较器排序) 创建TreeSet 对象时,传递比较器 Comparator 对象指定比较规则。

**使用原则:**默认使用第一种,如果第一种不能满足当前需求,就使用第二种。


**Test:**向集合中存入四个字符串,"c","ab","df","qwer",要求按照长度排序,长度一样则按照字符排序。

在 String 类中,java的开发者已经根据方式一,实现了 Compareable 接口并指定了比较规则,默认根据字符在ASCII 码表中的顺序进行排序。

此时,我们如果想重新定义比较规则,那就必须得修改 String 类源码中的比较规则,显然这时不可能的。

所以,采用方式二,在创建集合对象的时候, 直接传递比较器对象,重新指定比较规则。

public class TreeSetSort {
    public static void main(String[] args) {
        //1.创建集合
        TreeSet<String> ts = new TreeSet<>(new Comparator<String>() {
            @Override
            public int compare(String o1, String o2) {
                //按照长度排序
                int i = o1.length() - o2.length();
                //如果长度相同,则按照字符排序(调用默认排序规则)
                i = (i == 0 ? o1.compareTo(o2) : i);
                return i;
            }
        });

        //2.添加元素
        ts.add("c");
        ts.add("ab");
        ts.add("df");
        ts.add("qwer");

        //3.打印集合
        System.out.println(ts);//[c, ab, df, qwer]

    }
}

细节:

① Comparator 是一个函数是接口,可以使用 Lambda 表达式。

② String 类中已经制定了比较规则,而现在通过构造方法的方式,又重新制定了一个新的比较规则。这表明,在方式一和方式二都存在的情况下,方式二的优先级比方式一更高

③ compare 方法的形参:

I. o1:表示当前要添加的元素

II. o2:表示已经在红黑树中存在的元素

返回值:

I. 负数:表示当前要插入的元素 < 已排好的元素,插在该元素的左子树

II. 正数:表示当前要插入的元素 > 已排好的元素,插在该元素的右子树

III. 0 : 表示当前要插入的元素 = 已排好的元素,即该元素已存在,舍弃

④ 在 Arrays 类中,其 sort 方法也传递了一个 Comparator 比较器对象,可以参考一起学习。

Java常用API(三)https://blog.csdn.net/xpy2428507302/article/details/139356503?spm=1001.2014.3001.5501

12.单列集合的使用场景

三、双列集合

特点:

① 双列集合需要一次存一对数据,分别为键和值。

② 键不能重复,但值是可以重复的。

③ 键和值是 一 一对应的,每个键只能找到自己所对应的值。

④ "键 + 值" 这个整体被称为 "键值对" 或者 "键值对对象",在Java中叫做 "Entry对象"。

1.Map集合的使用

由于 Map 是一个接口, 我们不能直接创建他的对象。

所以,调用方法时,只能创建他的实现类对象。

其次,由于 Map 中存的是键值对,是一对元素,所以泛型也要有两个(K:key,V:value)。

Map是双列集合的顶层接口,它的功能是全部双列集合都可以继承使用的。

(1)put方法
public class Demo {
    public static void main(String[] args) {
        //创建Map集合的实现类对象(接口多态)
        Map<String, Integer> map = new HashMap<>();

        //1.put  添加元素
        Integer result1 = map.put("语文", 90);
        System.out.println(result1);//null
        map.put("数学", 95);
        map.put("英语", 92);
        System.out.println(map);//{数学=95, 语文=90, 英语=92}

        Integer result2 = map.put("语文", 80);
        System.out.println(result2);//90
        System.out.println(map);//{数学=95, 语文=80, 英语=92}
    }
}

细节:

① put 方法不光会添加元素,还有覆盖功能:

I. 在添加元素时,如果键不存在,会直接将键值对对象添加到 Map 集合中,方法返回 null;

II. 在添加元素时,如果键已经存在,那么会将原有的键值对对象的覆盖,并把被覆盖的值返回。

② 由于返回的是被覆盖的值,所以 put 方法的返回值类型是 V,即值的数据类型。

(2)remove方法
public class Demo {
    public static void main(String[] args) {
        //创建Map集合的实现类对象(接口多态)
        Map<String, Integer> map = new HashMap<>();

        //1.put  添加元素
        map.put("语文", 90);
        map.put("数学", 95);
        map.put("英语", 92);

        //2.remove  根据键删除键值对元素
        Integer result=map.remove("英语");
        System.out.println(result);//92
        System.out.println(map);//{数学=95, 语文=90}
    }
}

细节:

删除后会将被删除的键值对对象的值进行返回,所以remove方法的返回值类型是 V,即值的数据类型。

(3)clear方法
public class Demo {
    public static void main(String[] args) {
        //创建Map集合的实现类对象(接口多态)
        Map<String, Integer> map = new HashMap<>();

        //1.put  添加元素
        map.put("语文", 90);
        map.put("数学", 95);
        map.put("英语", 92);

        //3.clear 清空集合
        map.clear();
        System.out.println(map);//{}
    }
}
(4)containsKey和containsValue方法
public class Demo {
    public static void main(String[] args) {
        //创建Map集合的实现类对象(接口多态)
        Map<String, Integer> map = new HashMap<>();

        //1.put  添加元素
        map.put("语文", 90);
        map.put("数学", 95);
        map.put("英语", 92);
    
        //4.containsXxx  判断键/值是否包含
        boolean KeyResult= map.containsKey("英语");
        System.out.println(KeyResult);//true

        boolean ValueResult= map.containsValue(100);
        System.out.println(ValueResult);//false
    }
}
(5)isEmpty方法
public class Demo {
    public static void main(String[] args) {
        //创建Map集合的实现类对象(接口多态)
        Map<String, Integer> map = new HashMap<>();

        //1.put  添加元素
        map.put("语文", 90);
        map.put("数学", 95);
        map.put("英语", 92);

        //5.isEmpty  判断集合是否为空
        boolean result = map.isEmpty();
        System.out.println(result);//false
    }
}
(6)size方法
public class Demo {
    public static void main(String[] args) {
        //创建Map集合的实现类对象(接口多态)
        Map<String, Integer> map = new HashMap<>();

        //1.put  添加元素
        map.put("语文", 90);
        map.put("数学", 95);
        map.put("英语", 92);

        //6.size  获取集合长度
        int size = map.size();
        System.out.println(size);//3
    }
}

2.Map集合的遍历方式

(1)键找值

将所有的键存到一个单列集合中,遍历再在使用 get 方法获取每个键所对应的值。

public class IterateDemo1{
    public static void main(String[] args) {
        //创建Map集合对象
        Map<String, String> map = new HashMap<>();

        //添加元素
        map.put("工藤新一", "毛利兰");
        map.put("服部平次", "远山和叶");
        map.put("黑羽快斗", "中森青子");
        map.put("京极真", "铃木园子");

        //1.通过键找值
        //获取所有的键,将这些键放到一个单列集合当中
        Set<String> keys = map.keySet();
        for (String key : keys) {
            //遍历单列集合,得到每一个键
            String value = map.get(key);
            System.out.println(key + "=" + value);
        }
    }
}

细节:

① keySet 方法可以自动获取所有的键,并创建一个单列集合将这些键存放到其中。

② get方法可以根据键返回所对应的值。

(2)键值对

将所有的键值对对象存放到一个单列集合中,遍历再使用 getXxx 分别获取每个对象中的键和值。

import java.util.HashMap;
import java.util.Map;
import java.util.Set;

public class IterateDemo2 {
    public static void main(String[] args) {
        //创建Map集合对象
        Map<String, String> map = new HashMap<>();

        //添加元素
        map.put("工藤新一", "毛利兰");
        map.put("服部平次", "远山和叶");
        map.put("黑羽快斗", "中森青子");
        map.put("京极真", "铃木园子");

        //2.通过键值对对象进行遍历
        //获取所有的键值对对象
        Set<Map.Entry<String, String>> entries = map.entrySet();
        for (Map.Entry<String, String> entry : entries) {
            //遍历集合,使用getXxx方法获取键和值
            String key = entry.getKey();
            String value = entry.getValue();
            System.out.println(key + "=" + value);
        }
    }
}

细节:

① entrySet 方法可以自动获取所有的键值对对象,并创建一个 Set 集合将这些对象存放到其中。

② 这个 Set 集合确实是一个单列集合,因为集合中的每个元素是一个键值对对象,不是键和值两个元素。

注意这里使用了泛型嵌套,Set 的泛型是键值对类型,而键值对对象中有两个泛型:键和值。

③ Entry 事实上是 Map 接口中的内部接口,所以在表示 Entry 类型时,

需要使用:外部接口名.内部接口名

Tips:只有在已经导包的情况下:import java.util.Map.Entry;

才可以直接使用 Entry,即 Set<Entry<String,string>>

④ 键值对对象通过 getKey 和 getValue 方法可以获取键和值。

(3)Lambda表达式
public class IterateDemo3 {
    public static void main(String[] args) {
        //创建Map集合对象
        Map<String, String> map = new HashMap<>();

        //添加元素
        map.put("工藤新一", "毛利兰");
        map.put("服部平次", "远山和叶");
        map.put("黑羽快斗", "中森青子");
        map.put("京极真", "铃木园子");

        //3.使用Lambda表达式进行遍历
        //内部类书写方式 
        /*
        map.forEach(new BiConsumer<String, String>() {
            @Override
            public void accept(String key, String value) {
                System.out.println(key + "=" + value);
            }
        });*/

        //Lambda表达式书写方式
        map.forEach((key, value) -> System.out.println(key + "=" + value));
    }
}

细节:

① BiConsumer 是一个函数式接口,所以可以使用 Lambda表达式。

② 由于这里实现类对象是 HashMap,所以查看 HashMap 中的 forEach 方法源码。

底层事实上用到也是一个增强 for 进行遍历,然后将 key 和 value 交给 accept 方法进行处理。

3.HashMap集合的使用

特点:

① HashMap 是 Map 接口中的一个实现类。

② 没有额外需要学习的方法,直接使用 Map 中的方法就可以了。

③ 特点都是由键决定的:无序,不重复,无索引 --> 指的都是键

④ HashMap 和 HashSet 的底层原理是基本一模一样 的,都是哈希表结构。

HashMap 和 HashSet 底层的区别:

① HashMap 在计算哈希值时,只根据键进行计算,与值无关。

② HashMap 在利用 equals 方法进行比较时,只比较键的属性值,与值无关。

③ 如果键比较的结果一样,那么会将新的键值对对象进行覆盖,而不是和 HashSet 一样进行舍弃

注:

由于都是哈希表结构,所以 HashMap 也依赖 hashCode 和 equals 来保证键的唯一

所以:如果存储的是自定义对象,需要重写 hashCode 和 equals 方法。

但如果存储的是自定义对象,则不需要重写 hashCode 和 equals 方法。

4.linkedHashMap集合的使用

特点 (由键决定) 有序,不重复,无索引

原理: 和 LinkedHashSet 一样,底层数据结构依然是哈希表,只是每个键值对元素又额外多了一个双链表 的机制用于记录存储的顺序。

public class LinkHashMapDemo {
    public static void main(String[] args) {
        //创建集合对象
        LinkedHashMap<String, Integer> map = new LinkedHashMap<>();

        //添加元素
        map.put("语文", 90);
        map.put("数学", 95);
        map.put("英语", 92);

        System.out.println(map);//{语文=90, 数学=95, 英语=92}
    }
}

5.TreeMap集合的使用

原理: TreeMap 和 TreeSet 底层原理一样,都是红黑树结构的。

特点 (由键决定):可排序(对键进行排序),不重复,无索引

注: 排序规则默认按照的从小到大进行排序,也可以自己指定键的排序规则。

public class SortDemo1 {
    public static void main(String[] args) {
        //创建集合对象
        TreeMap<Integer,String> map = new TreeMap<>();

        //添加元素
        map.put(2,"矿泉水");
        map.put(4,"方便面");
        map.put(3,"可口可乐");
        map.put(5,"奥利奥");
        map.put(1,"蛋黄派");

        //打印集合
        System.out.println(map);
        //{1=蛋黄派, 2=矿泉水, 3=可口可乐, 4=方便面, 5=奥利奥}
    }
}

细节:

其实所谓默认,实质上是 Java 已经按照自定义排序规则的方式一指定了规则,不需要我们手动指定了而已。

这里键是 Integer 类型,所以查看 Integer 类的源码。

可以发现,该类已经实现了 Compareable 接口,并重写了 compareTo 方法。

自定义排序规则:

方式一: 实现 Compareable 接口指定比较规则

student类:

//实现Comparable接口,由于定义的是Student对象(键)间的排序规则,所以泛型直接限定类型
public class Student implements Comparable<Student> {
    private String name;
    private int age;

    public Student() {
    }

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

    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 "Student{name = " + name + ", age = " + age + "}";
    }

    @Override
    public int compareTo(Student o) {
        //this:表示当前要添加的元素
        //  o :表示红黑树中存在的元素
        int i = this.getAge() - o.getAge();
        return i == 0 ? this.getName().compareTo(o.name) : i;
    }
}

测试类:

public class SortDemo3 {
    public static void main(String[] args) {
        //1.创建四个学生对象
        Student s1 = new Student("wangwu", 25);
        Student s2 = new Student("lisi", 24);
        Student s3 = new Student("zhangsan", 23);
        Student s4 = new Student("zhaoliu", 26);

        //2.创建集合对象
        TreeMap<Student,String> map=new TreeMap();

        //3.添加元素
        map.put(s1,"上海");
        map.put(s2,"北京");
        map.put(s3,"南京");
        map.put(s4,"深圳");
        map.put(s2,"合肥");//会进行覆盖

        //4.打印集合
        System.out.println(map);
        /* {Student{name = zhangsan, age = 23}=南京, 
            Student{name = lisi, age = 24}=合肥,
            Student{name = wangwu, age = 25}=上海,
            Student{name = zhaoliu, age = 26}=深圳}*/
    }
}

注意:

这两种方式的排序规则中的形参和 TreeSet 中一模一样,但返回值为 0 时的意义不一样:

I. 负数:表示当前要插入的元素 < 已排好的元素,插在该元素的左子树

II. 正数:表示当前要插入的元素 > 已排好的元素,插在该元素的右子树

III. 0 : 表示当前要插入的元素 = 已排好的元素,即该元素已存在,进行覆盖( 覆盖键值对对象的值)

**方式二:**创建集合式传递比较器 Comparator 对象指定比较规则(优先级更高)。

比如:想要 Integer 类型的键按照降序排列,此时我们无法修改 java 已经写好的源代码,可以采用方式二进行指定比较规则。

public class SortDemo2 {
    public static void main(String[] args) {
        //创建集合对象
        TreeMap<Integer, String> map = new TreeMap<>(new Comparator<Integer>() {
            @Override
            public int compare(Integer o1, Integer o2) {
                //o1:当前要添加的元素
                //o2:已经在红黑树中存在的元素
                return o2 - o1;
            }
        });

        //添加元素
        map.put(2, "矿泉水");
        map.put(4, "方便面");
        map.put(3, "可口可乐");
        map.put(5, "奥利奥");
        map.put(1, "蛋黄派");

        //打印集合
        System.out.println(map);
        //{5=奥利奥, 4=方便面, 3=可口可乐, 2=矿泉水, 1=蛋黄派}
    }
}

7. 双列集合的使用场景

① 默认情况下:使用 HashMap --> 效率最高

② 如果要保证存取有序:使用 LinkedHashMap

③ 如果要进行排序:使用 TreeMap

四、可变参数

1.引言

在对于计算 2个,3个,4个这种确定数量的数据时,我们可以在方法中定义多个形参。

但如果是 n个数据呢?也就是形参个数不确定时,该如何解决这个问题?


我们通常会定义一个数组,将这 n 个数据存入数组中,再将数组作为方法的形参。

但这种方法过于麻烦,有没有不需要定义数组的方法,就可以解决这个问题呢?

2.定义

**可变参数:**方法的形参是可以发生改变的(JDK5开始)。

**格式:**数据类型... 变量名(如:int... args)

**底层原理:**可变参数事实上底层就是一个数组,但这个数组无需手动创建,java会自动创建好。

3.细节

① 在方法的形参中最多只能写一个可变参数,否则报错。

因为可变参数表示参数不确定,即可以接收多个数据。

假设有 10 个数据,在这种情况下,无法确定这 10个中,多少个属于 args1,多少个属于 args2。


② 在方法中,如果还有其他的形参,可变参数需要在最后面。

只有前面参数的个数确定了,后面的所有参数才能都交给可变参数。

假设有 10 个数据,在这种情况下,第一个数据是 a,剩下 9 个属于可变参数 agrs。

如果可变参数不写在最后,那么可变参数 agrs 会代表全部的 10个数据,后续的 a 将没有数据。

五、Collections集合工具类

1.定义

**作用:**Collectons 不是一个集合,而是集合工具类,提供了一系列操作集合的方法。

2.方法

(1)addAll和shuffle方法
public class Demo {
    public static void main(String[] args) {
        ArrayList<String> list = new ArrayList<>();
        //1.addAll  批量添加元素
        Collections.addAll(list,"abc","bcd","qwer","df","asdf","zxcv","1234","qwer");
        System.out.println(list);

        //2.shuffle 打乱
        Collections.shuffle(list);
        System.out.println(list);
    }
}

运行结果:

注意点:

① addAll 方法的形参中的泛型只有一个,所以只能传递单列集合,不能传递双列集合

② shuffle 方法的形参是一个 List类型的集合,所以只能传递 List 系列的集合,不能传递 Set 系列

(2)其他方法
java 复制代码
public class Demo2 {
    public static void main(String[] args) {
        System.out.println("-------------sort默认规则--------------------------");
        //默认规则,需要重写Comparable接口compareTo方法。
        //Integer已经实现,按照从小打大的顺序排列
        //如果是自定义对象,需要自己指定规则
        ArrayList<Integer> list1 = new ArrayList<>();
        Collections.addAll(list1, 10, 1, 2, 4, 8, 5, 9, 6, 7, 3);
        Collections.sort(list1);
        System.out.println(list1);


        System.out.println("-------------sort自己指定规则规则--------------------------");
        Collections.sort(list1, new Comparator<Integer>() {
            @Override
            public int compare(Integer o1, Integer o2) {
                return o2 - o1;
            }
        });
        System.out.println(list1);

        Collections.sort(list1, (o1, o2) -> o2 - o1);
        System.out.println(list1);

        System.out.println("-------------binarySearch--------------------------");
        //需要元素有序
        ArrayList<Integer> list2 = new ArrayList<>();
        Collections.addAll(list2, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10);
        System.out.println(Collections.binarySearch(list2, 9));
        System.out.println(Collections.binarySearch(list2, 1));
        System.out.println(Collections.binarySearch(list2, 20));

        System.out.println("-------------copy--------------------------");
        //把list3中的元素拷贝到list4中
        //会覆盖原来的元素
        //注意点:如果list3的长度 > list4的长度,方法会报错
        ArrayList<Integer> list3 = new ArrayList<>();
        ArrayList<Integer> list4 = new ArrayList<>();
        Collections.addAll(list3, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10);
        Collections.addAll(list4, 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0);
        Collections.copy(list4, list3);
        System.out.println(list3);
        System.out.println(list4);

        System.out.println("-------------fill--------------------------");
        //把集合中现有的所有数据,都修改为指定数据
        ArrayList<Integer> list5 = new ArrayList<>();
        Collections.addAll(list5, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10);
        Collections.fill(list5, 100);
        System.out.println(list5);

        System.out.println("-------------max/min--------------------------");
        //求最大值或者最小值
        ArrayList<Integer> list6 = new ArrayList<>();
        Collections.addAll(list6, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10);
        System.out.println(Collections.max(list6));
        System.out.println(Collections.min(list6));

        System.out.println("-------------max/min指定规则--------------------------");
        // String中默认是按照字母的abcdefg顺序进行排列的
        // 现在我要求最长的字符串
        // 默认的规则无法满足,可以自己指定规则
        // 求指定规则的最大值或者最小值
        ArrayList<String> list7 = new ArrayList<>();
        Collections.addAll(list7, "a","aa","aaa","aaaa");
        System.out.println(Collections.max(list7, new Comparator<String>() {
            @Override
            public int compare(String o1, String o2) {
                return o1.length() - o2.length();
            }
        }));

        System.out.println("-------------swap--------------------------");
        ArrayList<Integer> list8 = new ArrayList<>();
        Collections.addAll(list8, 1, 2, 3);
        Collections.swap(list8,0,2);
        System.out.println(list8);
    }
}

六、不可变集合

1.定义

**不可变集合:**不可以被修改的集合(添加、删除、修改元素均不可以)。

应用场景:

① 如果某个数据不能被修改,把它防御性的拷贝到不可变集合中是个很好的实践。

② 当集合对象被不可信的库调用时,不可变形式是安全的。

2.书写格式

在 List,Set,Map接口中,都存在静态的of 方法,可以获得一个不可变的集合。

(1)不可变的 List 集合

注:

① 一旦创建完毕后,是无法进行修改的,只能进行查询操作。

② of 方法的形参是一个可变参数,所以创建集合时,可以传递很多数据。

(2)不可变的 Set 集合

细节:

① 由于 Set 集合是不可重复的,所以当创建一个不可变的 Set 集合时,of 方法中的参数一定要保证唯一性。

如果 of 方法中的参数重复了,则会报错。

② 和 List 中的of 方法一样,形参是一个可变参数,所以创建集合时,可以传递很多数据。

(3)不可变的 Map 集合

细节:

① 可以看出,of 方法把奇数位参数当作键,偶数位参数当作值。

② 由于 Map 集合中键是是不能重复的,所以 of 方法中的键也不能重复。

③ of 方法最多只能容纳 10 个键值对,因为该方法的参数并不是可变参数,而是固定数量的形参。

of 方法提供 11 个重载方法,参数中键值对的数量由 0 到 10。

但并没有形参为可变参数的重载方法,所以参数中键值对最多只能为 10个。


question1:为什么不能定义一个可变参数的 of 的重载方法呢?

由于键值对是两个变量,类型可能不一样,如果要用可变参数,形式应该为:of(K... k, V... v)

但这样写就错了,前面说过,**在方法的形参中最多只能写一个可变参数,**所以没有办法定义。


question2:那如果我想创建一个键值对超过 10 个的不可变的Map集合,该如何实现呢?

应该有人能想到,那我将键值对这个整体作为一个对象,不就只需要一个可变参数了吗?

没错,Map 也是这么实现的,它提供了一个 ofEntries 方法,形参是一个 Entry 类型的可变参数。

java 复制代码
public class MapDemo2 {
    public static void main(String[] args) {
        //创建一个普通的Map集合
        HashMap<String, String> hm = new HashMap<>();
        hm.put("aaa", "111");
        hm.put("bbb", "222");
        hm.put("ccc", "333");
        hm.put("ddd", "444");
        hm.put("eee", "555");
        hm.put("fff", "666");
        hm.put("ggg", "777");
        hm.put("hhh", "888");
        hm.put("iii", "999");
        hm.put("jjj", "101010");
        hm.put("kkk", "111111");

        //利用上面的数据来创建一个不可变的Map集合
        //1.获取到所有的键值对对象(Entry对象)
        Set<Map.Entry<String, String>> entries = hm.entrySet();
        //2.把entries变成一个数组
        Map.Entry[] temp = new Map.Entry[0];
        Map.Entry[] arr = entries.toArray(temp);
        //3.创建不可变的Map集合
        Map map = Map.ofEntries(arr);
        System.out.println(map);
    }
}

细节:

前面说过,可变参数本质上就是一个数组。

所以需要通过 toArray 方法将 entries 集合变成一个数组,再传给 ofEntries 方法。

toArray 方法有两个重载方法。

无参的 toArray 方法默认返回一个Object 类型的数组,并不常用。

有参的 toArray 方法需要传递一个数组作为参数,返回值的类型则是这个数组的类型。

如果集合的长度(Entries) > 数组的长度(temp):数据在数组中放不下,此时会根据实际数据的个数重新创建数组。

如果集合的长度(Entries) <= 数组的长度(temp):数据在数组中放的下,此时不会创建新数组,而是直接用 temp。

所以,实质上可以简写为:

java 复制代码
Map<String,String> map = Map.ofEntries(hm.entrySet().toArray(new Map.Entry[0]));

但是这种写法过于麻烦,也不容易记,为此 Map 又定义了一个 copyOf 方法进行了封装。

在底层,会首先判断传入的 集合是否是可变集合。

如果是,则直接返回该集合;

如果不是,则根据该集合创建一个不可变集合。

java 复制代码
public class MapDemo2 {
    public static void main(String[] args) {
        //创建一个普通的Map集合
        HashMap<String, String> hm = new HashMap<>();
        hm.put("aaa", "111");
        hm.put("bbb", "222");
        hm.put("ccc", "333");
        hm.put("ddd", "444");
        hm.put("eee", "555");
        hm.put("fff", "666");
        hm.put("ggg", "777");
        hm.put("hhh", "888");
        hm.put("iii", "999");
        hm.put("jjj", "101010");
        hm.put("kkk", "111111");

        //利用上面的数据来创建一个不可变的Map集合
        Map<String,String> map=Map.copyOf(hm);
        System.out.println(map);
    }
}
相关推荐
CV-deeplearning5 分钟前
如何用python调用C++处理图片
开发语言·c++·python
IT猿手20 分钟前
2024最新算法:吸血水蛭优化器(Blood-Sucking Leech Optimizer,BSLO)求解23个函数,MATLAB代码
开发语言·算法·matlab·优化算法·最新算法
huaqianzkh24 分钟前
Jetpack架构组件_Navigaiton组件_1.Navigaiton切换Fragment
java·android studio·安卓
Java编程乐园28 分钟前
【第三方JSON库】org.json.simple用法初探—Java编程【Eclipse平台】【不使用项目管理工具】【不添加依赖解析】
java·eclipse·json
MoonDreamV31 分钟前
SpringMVC-01-回顾MVC架构
java
让你三行代码QAQ31 分钟前
SpringCloud-nacos基础
java·spring·spring cloud
ganjiee00071 小时前
记录一个笔误引发的bug导致生产环境报错,但是本地环境,测试环境运行正常
java·bug·multivaluemap
微服务技术分享1 小时前
Spring框架的最新进展:2023年Spring Boot和Spring Cloud功能更新
java·开发语言
翠花也老了1 小时前
命令模式(Command Pattern)
java·设计模式·命令模式
ITfeib1 小时前
spring boot starter
java·spring boot·后端