【JAVA进阶之集合】史上最完整的Collection以及Map集合底层原理分析、各种情况的选型

🔥作者主页小林同学的学习笔录

🔥mysql专栏:小林同学的专栏

目录

1.Collection集合

1.1  数组和集合的区别

1.2  集合类体系结构

1.3  Collection 集合概述和使用

1.4  Collection集合的遍历

1.4.1  迭代器遍历

1.4.2  增强for

2.List集合

2.1  List集合的概述和特点

2.2  List集合的特有方法

2.3  List集合的五种遍历方式

2.3.1  遍历方式

2.3.2  遍历选取

2.4  细节点注意

3.数据结构

3.1  数据结构之栈和队列

3.2  数据结构之数组和链表

4.源码分析

4.1  ArrayList的源码

4.2  LinkedList源码

4.3  迭代器源码

5.List集合的实现类

5.1  List集合子类的特点

5.2  LinkedList集合的特有方法

6.数据结构

6.1  树

6.1.1  二叉树

6.1.2  二叉查找树

6.1.3  二叉树的四种遍历方式

6.1.4  平衡二叉树

6.1.5  红黑树

7.Set集合

7.1  Set集合的概述和特点

7.2  Set集合的实现类

7.3  HashSet

7.4  LinkedHashSet

7.5  TreeSet

7.5.1  自然排序/默认排序Comparable

7.5.2  比较器排序Comparator

7.5.3  两种比较方式总结

8.选型总结

9.Map

9.1  HashMap

9.2  LinkedHashMap

9.3  TreeMap

10.源码分析

10.1  HashMap源码

10.2  TreeMap源码

10.3  选型总结

11.Collections工具类

12.可变参数


有问题可以评论讨论哦!!!

1.Collection集合

1.1  数组和集合的区别

相同点:

  • 都是容器,可以存储多个数据

不同点:

  • 数组的长度是不可变的,集合的长度是可变的

  • 数组可以存基本数据类型和引用数据类型

    集合只能存引用数据类型,如果要存基本数据类型,需要存对应的包装类

1.2  集合类体系结构

1.3  Collection 集合概述和使用

Collection集合概述:

  • 是单例集合的顶层接口,它表示一组对象,这些对象也称为Collection的元素

  • JDK 不提供此接口的任何直接实现.它提供更具体的子接口(如Set和List)实现

创建Collection集合的对象:

  • 多态的方式

  • 具体的实现类ArrayList

Collection集合常用方法:

1.4  Collection集合的遍历

1.4.1  迭代器遍历

迭代器介绍:

  • 迭代器,集合的专用遍历方式

  • Iterator<E> iterator(): 返回此集合中元素的迭代器,通过集合对象的iterator()方法得到

Iterator中的常用方法:

  • boolean hasNext(): 判断当前位置是否有元素可以被取出
  • E next(): 获取当前位置的元素,将迭代器对象移向下一个索引位置

  • void remove(): 删除迭代器对象当前指向的元素

1.4.2  增强for

  • 介绍
    • 它是JDK5之后出现的,其内部原理是一个Iterator迭代器

    • 实现Iterable接口的类才可以使用迭代器和增强for

    • 简化数组和Collection集合的遍历

2.List集合

2.1  List集合的概述和特点

List集合的概述:

  • 有序集合,这里的有序指的是存取顺序

  • 用户可以精确控制列表中每个元素的插入位置,用户可以通过整数索引访问元素,并搜索列表中的元素

  • 与Set集合不同,列表通常允许重复的元素

List集合的特点:

  • 存取有序

  • 可以重复

  • 有索引

2.2  List集合的特有方法

方法介绍:

代码演示:

2.3  List集合的五种遍历方式

2.3.1  遍历方式

  • 迭代器

  • 列表迭代器

  • 增强for

  • Lambda表达式

  • 普通for循环

代码演示:

//创建集合并添加元素
List<String> list = new ArrayList<>();
list.add("aaa");
list.add("bbb");
list.add("ccc");

//1.迭代器
/*Iterator<String> it = list.iterator();
     while(it.hasNext()){
        String str = it.next();
        System.out.println(str);
}*/


//2.增强for
//下面的变量s,其实就是一个第三方的变量而已。
//在循环的过程中,依次表示集合中的每一个元素
/* for (String s : list) {
       System.out.println(s);
   }*/

//3.Lambda表达式
//forEach方法的底层其实就是一个循环遍历,依次得到集合中的每一个元素
//并把每一个元素传递给下面的accept方法
//accept方法的形参s,依次表示集合中的每一个元素
//list.forEach(s->System.out.println(s) );


//4.普通for循环
//size方法跟get方法还有循环结合的方式,利用索引获取到集合中的每一个元素
/*for (int i = 0; i < list.size(); i++) {
            //i:依次表示集合中的每一个索引
            String s = list.get(i);
            System.out.println(s);
        }*/

// 5.列表迭代器
//获取一个列表迭代器的对象,里面的指针默认也是指向0索引的

//额外添加了一个方法:在遍历的过程中,可以添加元素
ListIterator<String> it = list.listIterator();
while(it.hasNext()){
    String str = it.next();
    if("bbb".equals(str)){
        //qqq
        it.add("qqq");
    }
}
System.out.println(list);

2.3.2  遍历选取

2.4  细节点注意

List系列集合中的两个删除的方法:

  • 1.直接删除元素
  • 2.通过索引进行删除

代码演示:

List<String> list = new ArrayList<>();
list.add("A");
list.add("B");
list.add("C");

// 删除元素值为"B"的元素
list.remove("B");


List<String> list = new ArrayList<>();
list.add("A");
list.add("B");
list.add("C");

// 删除索引为1的元素,即删除元素"B"
list.remove(1);

3.数据结构

3.1  数据结构之栈和队列

栈:

先进后出

队列:

先进先出

3.2  数据结构之数组和链表

数组:

查询快、增删慢

单向链表:

查询慢、增删快

链表中的结点是独立的对象,在内存是不连续的,每个结点包含数据值和下一个结点的地址值。单向链表查询慢的原因是无论查询哪个数据都要从头开始找。优化的话可以用双向链表,因为双向链表可以判断想要查的数据离开头近还是结尾近。

双向链表:

4.源码分析

4.1  ArrayList的源码

底层是数组结构

核心步骤:

  1. 创建ArrayList对象的时候,他在底层先创建了一个长度为0的数组。

    数组名字:elementDate,定义变量size。

    size这个变量有两层含义: ①:元素的个数,也就是集合的长度 ②:下一个元素的存入位置

  2. 添加元素,添加完毕后,size++

扩容时机一:

  1. 当存满时候,会创建一个新的数组,新数组的长度,是原来的1.5倍,也就是长度为15.再把所有的元素,全拷贝到新数组中。如果继续添加数据,这个长度为15的数组也满了,那么下次还会继续扩容,还是1.5倍。

扩容时机二:

  1. 一次性添加多个数据,扩容1.5倍不够,怎么办呀?

    如果一次添加多个元素,1.5倍放不下,那么新创建数组的长度以实际为准。

举个例子: 在一开始,如果默认的长度为10的数组已经装满了,在装满的情况下,我一次性要添加100个数据很显然,10扩容1.5倍,变成15,还是不够,怎么办?

此时新数组的长度,就以实际情况为准,就是110

添加一个元素时的扩容:

添加多个元素时的扩容:

4.2  LinkedList源码

底层是双向链表结构

核心步骤:

  1. 刚开始创建的时候,底层创建了两个变量:一个记录头结点first,一个记录尾结点last,默认为null

  2. 添加第一个元素时,底层创建一个结点对象,first和last都记录这个结点的地址值

  3. 添加第二个元素时,底层创建一个结点对象,第一个结点会记录第二个结点的地址值,last会记录新结点的地址值

添加第一个元素:

添加第二个元素:

添加第三个元素:

4.3  迭代器源码

迭代器遍历相关的三个方法:

  • Iterator<E> iterator() :获取一个迭代器对象

  • boolean hasNext() :判断当前指向的位置是否有元素

  • E next() :获取当前指向的元素并移动指针

hashNext()方法:

next()方法:

5.List集合的实现类

5.1  List集合子类的特点

  • ArrayList集合

    底层是数组结构实现,查询快、增删慢

  • LinkedList集合

    底层是链表结构实现,查询慢、增删快

5.2  LinkedList集合的特有方法

6.数据结构

6.1  树

6.1.1  二叉树

二叉树的特点:

  • 二叉树中,任意一个节点的度要小于等于2

    • 节点: 在树结构中,每一个元素称之为节点

    • 度: 每一个节点的子节点数量称之为度、

    • 树高:树的总层数

    • 根节点:最顶层的节点

    • 左子树(Left Subtree):左子树是指该节点的左侧所有子节点的集合。

    • 右子树(Right Subtree):右子树是指该节点的右侧所有子节点的集合。

每一个节点的结构:

6.1.2  二叉查找树

二分查找树在需要频繁进行查找操作,并且希望保持数据有序的情况下是非常适用的。而普通的二叉树则更适用于不需要有序性要求的情况,或者是更关注插入和删除操作的性能。

特点:

  • 每一个节点上最多有两个子节点
  • 任意节点左子树上的值都小于当前节点
  • 任意节点右子树上的值都大于当前节点

规则:

  • 添加节点,小的存左边,大的存右边,一样的不存

弊端:可能出现退化为链表的情况,类似于二分查找树,如果插入的顺序不合适,二叉查找树也可能会退化为链表。这种情况下,树的高度接近于节点数量,导致查找、插入和删除操作的时间复杂度变为 O(n),而不再是理想的 O(log n)。这些弊端意味着在某些特定情况下,二叉查找树可能不太适合,需要考虑其他数据结构或者对其进行优化,比如使用平衡二叉树(如AVL树、红黑树)

6.1.3  二叉树的四种遍历方式

前序遍历:从根节点开始,然后按照当前节点,左子节点,右子节点的顺序遍历

中序遍历:从最左边的子节点开始,然后按照左子结点,当前节点,右子节点的顺序遍历

中序遍历常用于按顺序输出树中的元素

后序遍历:从最左边的子节点开始,然后按照左子节点,右子节点,当前节点的顺序遍历

层序遍历:从根节点开始一层一层的遍历

6.1.4  平衡二叉树

平衡二叉树是一种特殊的二叉查找树,旨在防止树的高度过于高而导致效率低的问题,保持树的高度平衡。

规则:

  • 任意节点左右子树高度差不超过1
  • 添加节点,小的存左边,大的存右边,一样的不存

不是平衡二叉树:

平衡二叉树:

平衡二叉树的旋转:

旋转触发时机

  • 当添加一个节点之后,该树不再是一颗平衡二叉树,就需要相应的旋转机制把二叉树重新变成平衡二叉树

支点

  • 从添加的节点开始,不断往父节点找不平衡的节点

左旋

  • 先确定支点
  • 就是将根节点的右侧往左拉,把支点降级为左子节点,原先的右子节点变成新的父节点,并把多余的左子节点出让,给已经降级的根节点当右子节点

右旋

  • 先确定支点
  • 就是将根节点的左侧往右拉,把支点降级为右子节点,左子节点变成了新的父节点,并把多余的右子节点出让,给已经降级根节点当左子节点

平衡二叉树旋转的四种情况:

左左

  • 左左: 当根节点左子树的左子树有节点插入,导致二叉树不平衡

  • 如何旋转: 直接对整体进行右旋即可,只需要旋转一次

左右

  • 左右: 当根节点左子树的右子树有节点插入,导致二叉树不平衡

  • 如何旋转: 先在左子树对应的节点位置进行左旋,在对整体进行右旋。一般需要旋转多次,先旋转局部再全局旋转

先看问题:

一次右旋转完还是不平衡,因此需要局部左旋,再全局右旋

先局部左旋:

再全局右旋:

右右

  • 右右: 当根节点右子树的右子树有节点插入,导致二叉树不平衡

  • 如何旋转: 直接对整体进行左旋即可,只需要旋转一次

右左

  • 右左:当根节点右子树的左子树有节点插入,导致二叉树不平衡

  • 如何旋转: 先在右子树对应的节点位置进行右旋,在对整体进行左旋

先看问题:

一次左旋转完还是不平衡,因此需要局部右旋,再全局左旋

局部右旋:

全局左旋:

弊端:平衡二叉树可能会导致过多的旋转操作,从而降低性能。

6.1.5  红黑树

红黑树是一种特殊的二叉查找树,之前也被称为“平衡二叉B树”,高度不平衡,但是有特有的红黑规则。

红黑树的节点:

红黑规则:

  1. 每一个节点或是红色的,或者是黑色的
  2. 根节点必须是黑色
  3. 如果一个节点没有子节点或者父节点,则该节点相应的指针属性值为Nil,这些Nil视为叶节点,每个叶节点(Nil)是黑色的
  4. 如果某一个节点是红色,那么它的子节点必须是黑色(不能出现两个红色节点相连 的情况)
  5. 对每一个节点,从该节点到其所有后代叶节点的简单路径上,均包含相同数目的黑色节点

简单路径:意思是只能从头往后面走,不能从后面往前走

红黑树添加节点的默认颜色:

  • 添加节点时,默认为红色,效率高

红黑树添加节点后如何保持红黑规则:

实例:

根据以上面添加节点的规则结果:

7.Set集合

7.1  Set集合的概述和特点

概述:Set集合是一种常见的数据结构,用于存储不重复的元素。每个元素在集合中只能出现一次,如果尝试将重复的元素添加到集合中,则会被忽略。add()方法中,重复了会返回false。

特点:

无序:存取顺序不一致

不重复:可以去除重复

无索引:没有带索引的方法,所以不能用普通for循环遍历,也不能通过索引获取元素

7.2  Set集合的实现类

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

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

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

7.3  HashSet

原理:底层数据结构是哈希表,哈希表是一种对于增删改查数据性能都较好的结构


哈希表组成:

JDK8之前:数组 + 链表

JDK8之后:数组 + 链表 + 红黑树

哈希值:

  • 哈希值简介

    是JDK根据对象的地址或者字符串或者数字算出来的int类型的数值,哈希值通过hashCode()方法计算出来的

  • 如何获取哈希值

    Object类中的public int hashCode():返回对象的哈希码值

  • 哈希值的计算

    • int  index = (数组长度  -  1)  &  哈希值; 

  • 哈希值的特点

    • 如果没有重写hashCode方法,不同的对象计算出的哈希值是不同的

    • 如果已经重写hashCode方法,不同的对象只要属性值相同,计算出的哈希值就是一样的

    • 在小部分情况下,不同的属性值或者不同的地址值计算出来的哈希值也有可能一样。这种称为"哈希碰撞"。

JDK版本不同,底层添加数据的方式也有差异:

注意:

1.当链表的长度大于8并且数组长度大于等于64,该链表会升级为红黑树

2.如果HashSet集合泛型是自定义对象,那么一定要重写hashCode()和equals()方法,不然不会去重

3.String和Integer这两个包装类底层已经重写了hashCode()和equals()方法,因此不需要再重写

1.HashSet为什么存和取的顺序不一样?

当你向 HashSet 中添加元素时,元素被哈希化并存储在内部的哈希表中,这个过程并不考虑元素的顺序。

2.HashSet为什么没有索引?

HashSet 是基于哈希表实现的,它通过哈希值来存储和定位元素,而不是使用索引。

3.HashSet是利用什么机制保证数据的去重的?

HashSet 通过哈希表的特性来保证数据的去重。当你向 HashSet 中添加元素时,HashSet 首先计算元素的哈希码,并检查哈希表中是否已经存在相同哈希码的元素。如果存在相同哈希码的元素,则会调用 equals() 方法来比较这些元素是否相等,如果相等则不添加,否则添加新元素。这样就保证了 HashSet 中不会存在重复的元素。

HashSet底层就是HashMap,因此源码看下面的HashMap就可以

7.4  LinkedHashSet

原理:底层数据结构是哈希表,只是每个元素额外的多了个双向链表机制记录存储的顺序,因此

LinkedHashSet存和取可以做到有序。

虽然LinkedHashSet可以做到既有序又可以去重,但是因为它还维护了一个双向链表也导致效率比较低。

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

默认使用HashSet

如果既要有序又要去重,再考虑LinkedHashSet

7.5  TreeSet

原理:TreeSet集合底层是基于红黑树的数据结构进行排序的,增删改查性能都较好

可排序:按照元素的默认规则(从小到大)排序

TreeSet集合的默认排序规则:

  • 对于数字类型:Integer,Double默认按照从小到大的顺序进行排序
  • 对于字符、字符串类型:按照字符在ASCII码表中的数字升序进行排序

排序的构造方法:

  • TreeSet():根据其元素的自然排序进行排序

  • TreeSet(Comparator comparator) :根据指定的比较器进行排序

7.5.1  自然排序/默认排序Comparable

  • 案例需求

    • 存储学生对象并遍历,创建TreeSet集合使用无参构造方法

    • 要求:按照年龄从小到大排序,年龄相同时,按照姓名的字母顺序排序

  • 实现步骤

  • 1.使用空参构造创建TreeSet集合
    • 用TreeSet集合存储自定义对象,无参构造方法使用的是自然排序对元素进行排序的
  • 2.自定义的Student类实现Comparable接口
    • 自然排序,就是让元素所属的类实现Comparable<E>接口,重写compareTo(T o)方法
  • 3.重写接口中的compareTo方法
    • 重写方法时,一定要注意排序规则必须按照要求的主要条件和次要条件来写

  • compareTo方法返回值
    • 1.如果返回值为正数,表示当前存入的元素是较大值,存右边

    • 2.如果返回值为负数,表示当前存入的元素是较小值,存左边

    • 3.如果返回值为0,表示当前存入的元素跟集合中元素重复了,不存

compareTo()方法的参数解析:

根据年龄进行升序操作

每插入一个数据都会调用compareTo方法,来确定数据在在红黑树的位置并且按照红黑树规则进行调整,调整完成。再重新调用compareTo方法,继续上面的操作。

注意:插入第一个元素因为红黑树没有元素,直接插入到红黑树即可。所有的比较都是从根节点开始比较,实现小的在左边,大的在右边,相等的去除。

代码演示:

想看到this,和o可以自己打印一下

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) {
        //按照对象的年龄进行排序
        //主要判断条件: 按照年龄从小到大排序
        int result = this.age - o.age;
        //次要判断条件: 年龄相同时,按照姓名的字母顺序排序
        result = result == 0 ? this.name.compareTo(o.getName()) : result;
        return result;
    }
}

public class MyTreeSet2 {
    public static void main(String[] args) {
        //创建集合对象
        TreeSet<Student> ts = new TreeSet<>();
	    //创建学生对象
        Student s1 = new Student("zhangsan",28);
        Student s2 = new Student("lisi",27);
        Student s3 = new Student("wangwu",29);
        Student s4 = new Student("zhaoliu",28);
        Student s5 = new Student("qianqi",30);
		//把学生添加到集合
        ts.add(s1);
        ts.add(s2);
        ts.add(s3);
        ts.add(s4);
        ts.add(s5);
		//遍历集合
        for (Student student : ts) {
            System.out.println(student);
        }
    }
}

7.5.2  比较器排序Comparator

  • 实现步骤
    • 1.用TreeSet集合存储自定义对象,带参构造方法使用的是比较器排序对元素进行排序的

    • 2.比较器排序,就是让集合构造方法接收Comparator的实现类对象,重写compare(T o1,T o2)方法

    • 3.重写方法时,一定要注意排序规则必须按照要求的主要条件和次要条件来写

  • compareTo方法返回值
    • 1.如果返回值为正数,表示当前存入的元素是较大值,存右边

    • 2.如果返回值为负数,表示当前存入的元素是较小值,存左边

    • 3.如果返回值为0,表示当前存入的元素跟集合中元素重复了,不存

public class TreeSet01 {
    public static void main(String[] args) {
        /**
         * 需求:
         * 存入四个字符串,"c","ab","df","qwer"
         * 按照长度排序,如果长度一样长就按照首字母排序
         */
//        TreeSet<String> treeSet = 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;
//            }
//        });

        //用lambda表达式简化
        TreeSet<String> treeSet = new TreeSet<>((String o1, String o2) -> {
                int i = o1.length() - o2.length();
                //长度相等就进行默认排序
                i = i == 0 ? o1.compareTo(o2) : i;
                return i;
        });

        treeSet.add("c");
        treeSet.add("ab");
        treeSet.add("df");
        treeSet.add("qwer");

        System.out.println(treeSet);
    }
}


输出结果:

[c, ab, df, qwer]

7.5.3  两种比较方式总结

  • 两种比较方式小结

    • 自然排序: 自定义类实现Comparable接口,重写compareTo方法,根据返回值进行排序

    • 比较器排序: 创建TreeSet对象的时候传递Comparator的实现类对象,重写compare方法,根据返回值进行排序

    • 在使用的时候,默认使用自然排序,当自然排序不满足现在的需求时,必须使用比较器排序

  • 两种方式中关于返回值的规则

    • 如果返回值为负数,表示当前存入的元素是较小值,存左边

    • 如果返回值为0,表示当前存入的元素跟集合中元素重复了,不存

    • 如果返回值为正数,表示当前存入的元素是较大值,存右边

8.选型总结

  • 1.如果想要的集合中的元素可以重复
    • 用ArrayList集合,基于数组的
  • 2.如果想要集合中的元素可重复,而且当前的增删操作明显多于查询
    • 用LinkedList集合,基于链表的
  • 3.如果想对集合中的元素去重
    • 用HashSet集合,基于哈希表的
  • 4.如果想对集合中的元素去重,而且保证存取顺序
    • 用LinkedHashSet集合,基于哈希表和双链表,效率低于HashSet
  • 5.如果想对集合中的元素进行排序
    • 用TreeSet集合,基于红黑树。也可以用List集合实现排序

总结效率:

  • HashSet 在大多数操作上的效率最高,但是它是无序的。
  • LinkedHashSet 在需要保持插入顺序时非常有用,并且具有 HashSet 的快速查找特性。
  • TreeSet 在需要有序集合,并且需要按顺序遍历元素时很有帮助,但是要牺牲一些性能。

9.Map

双列集合的特点:

  • 1.双列集合一次需要存一对数据,分别是键和值

  • 键不可以重复,值可以重复

  • 键和值是一 一对应的,每个键只能找到对应的值

  • 键 + 值这个整体,我们称为“键值对”或者“键值对对象”,在Java中叫做“Entry对象”

Map常见的API:

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

put()方法的细节

可以是添加,也可以是覆盖

在添加数据的时候,如果键不存在,那么直接把键值对对象添加到map集合中,并且返回null

在添加数据的时候,如果键已经存在,那么会把原来的键值对对象覆盖,并且会把被覆盖的值进行返回

remove()方法细节

根据键删除键值对对象,并且返回被删除的键值对对象的值

遍历Map集合:

1.键找值:调用keySet()方法,返回所有键,然后在循环调用get()获取值

2.键值对:调用entrySet()方法,返回所有键值对对象,然后在循环获取

3.lambda表达式进行遍历(forEach(key,values) -> { 具体操作 } );、

9.1  HashMap

特点:

  • HashMap的特点都是有键决定的:无序、不重复、无索引

  • HashMap底层是哈希表结构

  • 依赖hashCode方法和equals方法保证键的唯一

  • 如果键要存储的是自定义对象,需要重写hashCode和equals方法

哈希表组成

JDK8之前:数组 + 链表

JDK8之后:数组 + 链表 + 红黑树

9.2  LinkedHashMap

LinkedHashMap的特点和LinkedHashSet都是有键决定的:有序、不重复、无索引

原理:底层数据结构是哈希表,只是每个键值额外的多了个双向链表机制记录存储的顺序,因此

LinkedHashSet存和取可以做到有序。

9.3  TreeMap

特点:

  • TreeMap跟TreeSet底层原理一样,都是红黑树结构
  • 由键决定:可排序,不重复,无索引
  • 可排序:对键进行排序

两种排序规则:

实现Comparable接口,指定比较规则

创建集合是传递Comparators比较器对象,指定比较规则

10.源码分析

10.1  HashMap源码

可以复制下面代码到NodePad++进行查看,效果更直观

1.看源码之前需要了解的一些内容

Node<K,V>[] table   哈希表结构中数组的名字

DEFAULT_INITIAL_CAPACITY:   数组默认长度16

DEFAULT_LOAD_FACTOR:        默认加载因子0.75

HashMap里面每一个对象包含以下内容:
1.1 链表中的键值对对象
    包含:  
            int hash;         //键的哈希值
            final K key;      //键
            V value;          //值
            Node<K,V> next;   //下一个节点的地址值
            
            
1.2 红黑树中的键值对对象
    包含:
            int hash;                 //键的哈希值
            final K key;              //键
            V value;                  //值
            TreeNode<K,V> parent;      //父节点的地址值
            TreeNode<K,V> left;        //左子节点的地址值
            TreeNode<K,V> right;    //右子节点的地址值
            boolean red;            //节点的颜色
                    


2.添加元素

//执行这句话后,其实就是进行加载因子的赋值,但是并没有创建数组,因此此时table为null
HashMap<String,Integer> hm = new HashMap<>();

//运行这一句才会创建一个默认数组长度为16的数组
hm.put("aaa" , 111);
hm.put("bbb" , 222);
hm.put("ccc" , 333);
hm.put("ddd" , 444);
hm.put("eee" , 555);

添加元素的时候至少考虑三种情况:
2.1数组位置为null
2.2数组位置不为null,键不重复,挂在下面形成链表或者红黑树
2.3数组位置不为null,键重复,元素覆盖

//参数一:键
//参数二:值

//返回值:被覆盖元素的值,如果没有覆盖,返回null
public V put(K key, V value) {
    return putVal(hash(key), key, value, false, true);
}


//利用键计算出对应的哈希值,再把哈希值进行一些额外的处理
//简单理解:返回值就是返回键的哈希值
static final int hash(Object key) {
    int h;
    return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}

//参数一:键的哈希值
//参数二:键
//参数三:值
//参数四:如果键重复了是否保留
//           true,表示老元素的值保留,不会覆盖
//           false,表示老元素的值不保留,会进行覆盖
final V putVal(int hash, K key, V value, boolean onlyIfAbsent,boolean evict) {
        //定义一个局部变量,用来记录哈希表中数组的地址值。
        Node<K,V>[] tab;
        
        //临时的第三方变量,用来记录键值对对象的地址值
        Node<K,V> p;
        
        //表示当前数组的长度
        int n;
        
        //表示索引
        int i;
        
        //把哈希表中数组的地址值,赋值给局部变量tab
        tab = table;

        if (tab == null || (n = tab.length) == 0){
            //1.如果当前是第一次添加数据,底层会创建一个默认长度为16,加载因子为0.75的数组
            //2.如果不是第一次添加数据,会看数组中的元素是否达到了扩容的条件
            //如果没有达到扩容条件,底层不会做任何操作
            //如果达到了扩容条件,底层会把数组扩容为原先的两倍,并把数据全部转移到新的哈希表中
            tab = resize();
            //表示把当前数组的长度赋值给n
            n = tab.length;
        }

        //拿着数组的长度跟键的哈希值进行计算,计算出当前键值对对象,在数组中应存入的位置
        i = (n - 1) & hash;//index
        //获取数组中对应元素的数据
        p = tab[i];
        
        
        if (p == null){

              //  2.1数组位置为null
            //底层会创建一个键值对对象,直接放到数组当中
            tab[i] = newNode(hash, key, value, null);
        }else {

        //2.2数组位置不为null,键不重复,挂在下面形成链表或者红黑树
            Node<K,V> e;
            K k;
            
            //等号的左边:数组中键值对的哈希值
            //等号的右边:当前要添加键值对的哈希值
            //如果键不一样,此时返回false
            //如果键一样,返回true
            boolean b1 = p.hash == hash;
            
            if (b1 && ((k = p.key) == key || (key != null && key.equals(k)))){
                e = p;
            } else if (p instanceof TreeNode){
                //判断数组中获取出来的键值对是不是红黑树中的节点
                //如果是,则调用方法putTreeVal,把当前的节点按照红黑树的规则添加到树当中。
                e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
            } else {
                //如果从数组中获取出来的键值对不是红黑树中的节点
                //表示此时下面挂的是链表
                for (int binCount = 0; ; ++binCount) {
                    if ((e = p.next) == null) {
                        //此时就会创建一个新的节点,挂在下面形成链表
                        p.next = newNode(hash, key, value, null);
                        //判断当前链表长度是否超过8,如果超过8,就会调用方法treeifyBin
                        //treeifyBin方法的底层还会继续判断
                        //判断数组的长度是否大于等于64
                        //如果同时满足这两个条件,就会把这个链表转成红黑树
                        if (binCount >= TREEIFY_THRESHOLD - 1)
                            treeifyBin(tab, hash);
                        break;
                    }
                    //e:              0x0044  ddd  444
                    //要添加的元素: 0x0055   ddd   555
                    //如果哈希值一样,就会调用equals方法比较内部的属性值是否相同

                     //        2.3数组位置不为null,键重复,元素覆盖
                    if (e.hash == hash && ((k = e.key) == key || (key != null && key.equals(k)))){
                         break;
                    }

                    p = e;
                }
            }
            
            //如果e为null,表示当前不需要覆盖任何元素
            //如果e不为null,表示当前的键是一样的,值会被覆盖
            //e:0x0044  ddd  555
            //要添加的元素: 0x0055   ddd   555
            if (e != null) {

                // 2.3数组位置不为null,键重复,元素覆盖
                V oldValue = e.value;
                if (!onlyIfAbsent || oldValue == null){
                    
                    //等号的右边:当前要添加的值
                    //等号的左边:0x0044的值

                     //这里需要注意的是覆盖其实只是把值覆盖,并不会覆盖节点
                    e.value = value;
                }
                afterNodeAccess(e);
                return oldValue;
            }
        }
        
        //threshold:记录的就是数组的长度 * 0.75,哈希表的扩容时机 , 16 * 0.75 = 12,

        //也就是超过12就会进行扩容
        if (++size > threshold){
             resize();
        }
        
        //表示当前没有覆盖任何元素,返回null
        return null;
    }

10.2  TreeMap源码

1.TreeMap中每一个节点的内部属性
K key;                    //键
V value;                //值
Entry<K,V> left;        //左子节点
Entry<K,V> right;        //右子节点
Entry<K,V> parent;        //父节点
boolean color;            //节点的颜色


2.TreeMap类中中要知道的一些成员变量
public class TreeMap<K,V>{
   
    //比较器对象
    private final Comparator<? super K> comparator;

    //根节点
    private transient Entry<K,V> root;

    //集合的长度
    private transient int size = 0;

   

3.空参构造
    //空参构造就是没有传递比较器对象
     public TreeMap() {
        comparator = null;
    }
    
    
    
4.带参构造
    //带参构造就是传递了比较器对象。
    public TreeMap(Comparator<? super K> comparator) {
        this.comparator = comparator;
    }
    
    
5.添加元素
    public V put(K key, V value) {
        return put(key, value, true);
    }

参数一:键
参数二:值
参数三:当键重复的时候,是否需要覆盖值
        true:覆盖
        false:不覆盖
        
    private V put(K key, V value, boolean replaceOld) {
        //获取根节点的地址值,赋值给局部变量t
        Entry<K,V> t = root;
        //判断根节点是否为null
        //如果为null,表示当前是第一次添加,会把当前要添加的元素,当做根节点
        //如果不为null,表示当前不是第一次添加,跳过这个判断继续执行下面的代码
        if (t == null) {
            //方法的底层,会创建一个Entry对象,把他当做根节点
            addEntryToEmptyMap(key, value);
            //表示此时没有覆盖任何的元素
            return null;
        }
        //表示两个元素的键比较之后的结果
        int cmp;
        //表示当前要添加节点的父节点
        Entry<K,V> parent;
        
        //表示当前的比较规则
        //如果我们是采取默认的自然排序,那么此时comparator记录的是null,cpr记录的也是null
        //如果我们是采取比较去排序方式,那么此时comparator记录的是就是比较器
        Comparator<? super K> cpr = comparator;
        //表示判断当前是否有比较器对象
        //如果传递了比较器对象,就执行if里面的代码,此时以比较器的规则为准
        //如果没有传递比较器对象,就执行else里面的代码,此时以自然排序的规则为准
        if (cpr != null) {
            do {
                parent = t;
                cmp = cpr.compare(key, t.key);
                if (cmp < 0)
                    t = t.left;
                else if (cmp > 0)
                    t = t.right;
                else {
                    V oldValue = t.value;
                    if (replaceOld || oldValue == null) {
                        t.value = value;
                    }
                    return oldValue;
                }
            } while (t != null);
        } else {
            //把键进行强转,强转成Comparable类型的
            //要求:键必须要实现Comparable接口,如果没有实现这个接口
            //此时在强转的时候,就会报错。
            Comparable<? super K> k = (Comparable<? super K>) key;
            do {
                //把根节点当做当前节点的父节点
                parent = t;
                //调用compareTo方法,比较根节点和当前要添加节点的大小关系
                cmp = k.compareTo(t.key);
                
                if (cmp < 0)
                    //如果比较的结果为负数
                    //那么继续到根节点的左边去找
                    t = t.left;
                else if (cmp > 0)
                    //如果比较的结果为正数
                    //那么继续到根节点的右边去找
                    t = t.right;
                else {
                    //如果比较的结果为0,会覆盖
                    V oldValue = t.value;
                    if (replaceOld || oldValue == null) {
                        t.value = value;
                    }
                    return oldValue;
                }
            } while (t != null);
        }
        //就会把当前节点按照指定的规则进行添加
        addEntry(key, value, parent, cmp < 0);
        return null;
    }    
    
    
    
     private void addEntry(K key, V value, Entry<K, V> parent, boolean addToLeft) {
        Entry<K,V> e = new Entry<>(key, value, parent);
        if (addToLeft)
            parent.left = e;
        else
            parent.right = e;
        //添加完毕之后,需要按照红黑树的规则进行调整
        fixAfterInsertion(e);
        size++;
        modCount++;
    }
    
    
    
    private void fixAfterInsertion(Entry<K,V> x) {
        //因为红黑树的节点默认就是红色的
        x.color = RED;

        //按照红黑规则进行调整
        
        //parentOf:获取x的父节点
        //parentOf(parentOf(x)):获取x的爷爷节点
        //leftOf:获取左子节点
        while (x != null && x != root && x.parent.color == RED) {
            
            
            //判断当前节点的父节点是爷爷节点的左子节点还是右子节点
            //目的:为了获取当前节点的叔叔节点
            if (parentOf(x) == leftOf(parentOf(parentOf(x)))) {
                //表示当前节点的父节点是爷爷节点的左子节点
                //那么下面就可以用rightOf获取到当前节点的叔叔节点
                Entry<K,V> y = rightOf(parentOf(parentOf(x)));
                if (colorOf(y) == RED) {
                    //叔叔节点为红色的处理方案
                    
                    //把父节点设置为黑色
                    setColor(parentOf(x), BLACK);
                    //把叔叔节点设置为黑色
                    setColor(y, BLACK);
                    //把爷爷节点设置为红色
                    setColor(parentOf(parentOf(x)), RED);
                    
                    //把爷爷节点设置为当前节点
                    x = parentOf(parentOf(x));
                } else {
                    
                    //叔叔节点为黑色的处理方案
                    
                    
                    //表示判断当前节点是否为父节点的右子节点
                    if (x == rightOf(parentOf(x))) {
                        
                        //表示当前节点是父节点的右子节点
                        x = parentOf(x);
                        //左旋
                        rotateLeft(x);
                    }
                    setColor(parentOf(x), BLACK);
                    setColor(parentOf(parentOf(x)), RED);
                    rotateRight(parentOf(parentOf(x)));
                }
            } else {
                //表示当前节点的父节点是爷爷节点的右子节点
                //那么下面就可以用leftOf获取到当前节点的叔叔节点
                Entry<K,V> y = leftOf(parentOf(parentOf(x)));
                if (colorOf(y) == RED) {
                    setColor(parentOf(x), BLACK);
                    setColor(y, BLACK);
                    setColor(parentOf(parentOf(x)), RED);
                    x = parentOf(parentOf(x));
                } else {
                    if (x == leftOf(parentOf(x))) {
                        x = parentOf(x);
                        rotateRight(x);
                    }
                    setColor(parentOf(x), BLACK);
                    setColor(parentOf(parentOf(x)), RED);
                    rotateLeft(parentOf(parentOf(x)));
                }
            }
        }
        
        //把根节点设置为黑色
        root.color = BLACK;
    }

10.3  选型总结

  • HashMap 在大多数操作上的效率最高,但是它是无序的。
  • LinkedHashMap 在需要保持插入顺序或访问顺序时非常有用。
  • TreeMap 在需要按顺序遍历键值对时很有帮助,但是要牺牲一些性能。

你觉得在Map集合中,java会提供一个如果键重复了,不会覆盖的put方法呢?
此时putIfAbsent本身不重要,可以在键重复时,进行不覆盖处理。
主要是传递一个思想:
    代码中的逻辑都有两面性,如果我们只知道了其中的A面,而且代码中还发现了有变量可以控制两面性的发生。
    那么该逻辑一定会有B面。
    
    习惯:
        boolean类型的变量控制,一般只有AB两面,因为boolean只有两个值
        int类型的变量控制,一般至少有三面,因为int可以取多个值。

11.Collections工具类

Collections是集合的一个工具类,并不是集合

常见的API:

12.可变参数

实质上是一个数组

  • 语法
    • 在方法声明时,将可变参数类型放在参数列表的最后,用三个点(...)表示。
    • 可以选择传多个值,也可以选择不传任何值
    • 例如:public void exampleMethod(String... args) { ... }

  • 注意事项

    • 可变参数必须是方法的最后一个参数,一个方法最多只能有一个可变参数。
    • 如果方法有多个参数,可变参数前面的参数必须明确定义类型。
    • 如果方法有固定数量的参数,优先匹配固定参数的方法。
  • 31
    点赞
  • 27
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值