Java集合进阶

目录

一、体系结构

1.集合体系结构

​编辑

2.单列集合体系结构

二、单列集合 

1.Collection集合的使用

(1) add 方法 

(2) clear方法

(3)remove方法 

(4)contains 方法

(5)isEmpty方法

(6)size方法

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

(1)迭代器遍历

(2)增强 for 遍历

(3)Lambda表达式遍历

3.List集合的使用

(1)add方法

(2)remove方法

(3)set方法

(4)get方法

4.List集合的遍历方式

(1)普通 for 循环遍历

(2)列表迭代器遍历

① hasPrevious 和 previous

② 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. 双列集合的使用场景

四、可变参数

1.引言

2.定义 

3.细节

五、Collections集合工具类

1.定义

2.方法 

(1)addAll和shuffle方法 

 (2)其他方法

六、不可变集合

1.定义

2.书写格式

 (1)不可变的 List 集合

(2)不可变的 Set 集合

(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(三)icon-default.png?t=N7T8https://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)其他方法
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 类型的可变参数。

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。

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

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

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

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

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

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

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

  • 13
    点赞
  • 13
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值