java集合的使用(包含源码分析、底层实现机制)

文章目录

java 的集合的使用

  试想一下,如果我们需要保存多个数据,首先会想到用数组。但是数组有着以下的局限:

  1. 长度开始时必须指定,而且一旦指定,不能更改。
  2. 保存的必须为同一类型的元素。
  3. 使用数组进行增加/删除元素代码—比较麻烦。

  数组扩容:

Person[] person = new Person[1];//大小为1
person[0] = new Person();

//增加新的对象
//创建一个新数组
Person[] person2 = new Person[person.length + 1];
for(){} //进行拷贝,这里略写
//添加新的对象
person2[person.length - 1] = new Person[];

  鉴于数组的局限性,java 中引入了集合的概念。

  集合的优点:

  1. 可以动态的保存任意多个对象,使用方便。
  2. 提供了一系列方便的操作对象的方法:add 、remove、set 、get等。
  3. 使用集合可以直接进行添加,删除新元素的操作。

1 集合的框架体系及主要的分类

  java 中集合类很多,主要分为2大类:Collection 和 Map 。以下它们主要的继承、实现关系:

在这里插入图片描述
在这里插入图片描述

  集合主要为2组:

  1. 单列集合
  2. 双列集合
//1. Collection 接口有两个重要的子接口 List 和 Set, 他们的实现子类都是单列集合
//2. Map 接口的实现子类 是双列集合,存放的 K-V

//Collection,存放单列数据
ArrayList arrayList = new ArrayList();
arrayList.add("jack");
arrayList.add("tom");
//Map,存放双列数据
HashMap hashMap = new HashMap();
hashMap.put("NO1", "北京");
hashMap.put("NO2", "上海");

2 Collection 接口和常用方法

(1)Collection 接口实现类的特点

  1. Collection 的实现子类可以存放多个元素,每个元素可以是 Object。
  2. 有些 Collection 的实现类,可以存放重复的元素,有些不可以。
  3. 有些 Collection 的实现类,有些是有序的(List),有些不是有序(Set)。
  4. Collection 接口没有直接的实现子类,是通过它的子接口 Set 和 List 来实现的。

(2)Collection 接口常用方法,以实现子类 ArrayList 进行案例演示

public class Collection_ {
    public static void main(String[] args) {
        List list = new ArrayList();
        //add() 方法:添加单个元素
        list.add("jack");//添加字符串
        list.add(10);//自动装箱
        
        //remove :删除指定元素
        list.remove(0);//删除第一个元素
        list.remove("jack");//指定删除某个元素
        
        //contains :查找元素是否存在
        System.out.println(list.contains("jack"));
        //如果存在,则返回 true; 没有,则返回 false
        
        //size() :获取元素个数
       	int n = list.size();
        
        //isEmpty() :判断集合是否为空
        boolean flag = list.isEmpty();
        //如果为空,返回 true; 不为空,返回 false
        
        //clear() :清空集合所有元素
        list.clear();
        
        //addAll() :添加多个元素
        ArrayList list2 = new ArrayList();
        list2.add("西游记");
        list2.add("红楼梦");
        list.addAll(list2);//即,addAll()里面可以传一个集合
        
        //containsAll():查找多个元素是否都存在
        boolean flag2 = list.containsAll(list2);
        //如果都存在,则返回 true; 没有,则返回 false
        
        //removeAll():删除多个元素
        list.removeAll(list2);
        
    }
}

(3)Collection 接口遍历元素的方法

1 使用 Iterator(迭代器)
1.1 基本介绍

  Iterator 的结构如下图:
在这里插入图片描述

基本介绍如下:

  1. Iterator 对象称为迭代器,主要用于 Collection 集合中的元素。
  2. 所有实现了 Collection 接口的集合类都有一个 iterator() 方法,用以返回一个实现了 Iterator 接口的对象,即可以返回一个迭代器。
  3. Iterator 仅用于遍历集合, Iterator 本身并不存放对象。
1.2 Iterator 接口的方法和遍历的执行原理

在这里插入图片描述

执行原理:

得到一个集合的迭代器: Iterator iterator = coll.iterator();

判断是否还有下一个元素:hasNext()

while(iterator.hasNext()) {
    System.out.println(iterator.next());
    //next() 的作用:
    //1.下移
    //2.将下移以后,集合位置上的元素返回
}

在这里插入图片描述

  如上图,一开始迭代器 iterator 指向集合中第一个元素的上一个位置,iterator.hasNext() 为对下一个位置进行判断,如果存在元素则返回 true,没有则返回 false。

  iterator.next() 执行之后表示,iterator 指向了下一个位置,并返回此位置上的元素。

注意: 在调用 iterator.next() 方法之前,必须要调用 iterator.hasNext() 进行检测。如果没有调用 iterator.hasNext() 进行检测,且恰好下一条记录无效,若此时调用 iterator.next() ,系统将会抛出 NoSuchElementException 异常。

案例演示:

public class Collection_2 {
    public static void main(String[] args) {
        Collection col = new ArrayList();
        
        col.add("三国演义");
        col.add("西游记");
        col.add("红楼梦");
        
        //遍历集合并输出
        //得到集合对应的迭代器
        Iterator iterator = col.iterator();
        while(iterator.hasNext()) {
            System.out.println(iterator.next());
        }
        
        //如果希望再次进行遍历
        //需要重置迭代器去
        iterator = col.iterator();
        
    }
}

2 使用 for 循环增强进行遍历

  增强 for 循环,可以代替 iterator 迭代器,特点是:增强 for 循环就是简化版的 iterator,本质上是一样的。只能用于遍历集合或数组。

基本语法:

for(元素类型 元素名 : 集合名或数组名) {
    访问元素;
}
例:
for(Object ob: col) {
    System.out.println(ob);
}

3 List 接口和常用方法

3.1 基本介绍

List 接口是 Collection 接口的子接口

  1. List 集合中元素有序(即添加顺序和取出的顺序一致)、且可重复。

    List list = new ArrayList();
    list.add("tom");//为第一个
    list.add("jack");//为第二个
    list.add("mary");//为第三个
    //即存储形式类似数组
    
  2. List 集合的每个元素都有其对应的顺序索引,即支持索引,索引从 0 开始。

  3. List 容器中的元素都对应一个整数型的序号记载其在容器中的位置,可以根据序号存取容器中的元素。序号也是从 0 开始的。

    System.out.println(list.get(2));
    //输出 mary,即为集合中第三个元素
    
  4. JDK API 中 List 接口的实现类有:常用的有 ArrayList、LinkedList、Vector

在这里插入图片描述

3.2 List 接口的常用方法

  通过代码进行演示:

 importjava.util.ArrayList;
 importjava.util.List;

 publicclassListMethod{
     @SuppressWarnings({"all"})
     public static void main(String[] args){
         List list=new ArrayList();
         list.add("张三丰");
         list.add("贾宝玉");

        //void add(int index, Object ele):在 index 位置插入 ele 元素
        //在 index = 1 的位置插入一个对象
        list.add(1, "杨过");
        System.out.println("list=" + list);
         
        //boolean addAll(int index, Collection eles):
        //从 index 位置开始将 eles 中的所有元素添加进来
        List list2 = new ArrayList();
        list2.add("jack");
        list2.add("tom");
        list.addAll(1, list2);
        System.out.println("list=" + list);
         
        //Object get(int index):获取指定 index 位置的元素
        //int indexOf(Object obj):返回 obj 在集合中首次出现的位置
        System.out.println(list.indexOf("tom"));//2
        
        //int lastIndexOf(Object obj):返回 obj 在当前集合中末次出现的位置
        list.add("杨过");
        System.out.println("list=" + list);
        System.out.println(list.lastIndexOf("杨过"));
         
        //Object remove(int index):移除指定 index 位置的元素,并返回此元素
        list.remove(0);
        System.out.println("list=" + list);
        
        //Object set(int index, Object ele):
        //设置指定 index 位置的元素为 ele , 相当于是替换.
        list.set(1, "玛丽");
        System.out.println("list=" + list);
        
        //List subList(int fromIndex, int toIndex):
        //返回从 fromIndex 到 toIndex 位置的子集合
        // 注意返回的子集合 fromIndex <= subList < toIndex
        List returnlist = list.subList(0,2);
        System.out.println("returnlist="+returnlist);
     }
 }

3.3 List 的 3 种遍历方法(上文已经讲解过)

  1. 使用迭代器
  2. 使用增强 for 循环
  3. 使用普通的 for 循环
for(int i = 0; i < col.size(); i++) {
    System.out.println(col.get(i));
}

4 ArrayList 底层结构和源码分析

4.1 注意事项:

  1. permits all elements(允许所有元素), including null(包括 null), ArrayList 可以加入 null, 并且多个。

  2. ArrayList 是由数组来实现数据存储的。

  3. ArrayList 基本等同于 Vector ,除了 ArrayList 是线程不安全的(执行效率高)。观察源码可知,ArrayList 的方法都没有使用 synchronized 关键字。

    在多线程的情况下,不建议使用 ArrayList。

4.2 结论

关于 ArrayList 的底层结构和源码,我们先给出结论,后续再进行代码的追踪。

  1. ArrayList 中维护了一个 Object 类型的数组 elementData。

    transient Object[] elementData; //transient 表示瞬间,短暂的,表示该属性不会被序列化

  2. 当创建 ArrayList 对象时,如果使用的是无参构造器,则初始 elementData 容量为0,第一次添加,则扩容 elementData 为 10,如果需要再次进行扩容,则扩容elementData为原来的1.5倍。

  3. 如果使用的是指定大小的构造器,则初始 elementData 容量为指定大小,如果需要扩容,则直接扩容 elementData 为原来的1.5倍。

在这里插入图片描述

4.3 源码分析

(1)使用无参构造器初始化

  使用已下代码进行源码分析:

import java.util.ArrayList;

public class ArrayList01 {
    public static void main(String[] args) {
        //使用无参构造器
        ArrayList arrayList = new ArrayList();

        for(int i = 1; i <= 10; i++) {
            arrayList.add(i);
        }
    }
}

当使用无参构造器时,elementData 会进行初始化。
在这里插入图片描述

  以下为 DEFAULTCAPACITY_EMPTY_ELEMENTDATA 在ArrayList中的定义:

private static final Object[] DEFAULTCAPACITY_EMPTY_ELEMENTDATA = {};

  可以看出,当调用无参构造器时,elementData 数组将会初始化为一个空数组。

第一次增加元素

  由于我们增加的是 int 类型,所以会进行一个自动装箱。

在这里插入图片描述

  然后将进入 add() 方法中:

在这里插入图片描述

  可以看出,进入 add() 方法之中,先会调用 ensureCapacityInternal() 方法,size 为集合中元素的个数,因为我们正在第一次增加元素,所以此时 size = 0,即我们传入的参数为 size + 1 = 1

  接下来,我们进入 ensureCapacityInternal() 方法看看:

在这里插入图片描述

  可以看到,将要执行 calculateCapacity() 方法,然后再返回来执行 ensureExplicitCapacity() 方法。

  所以我们进入 calculateCapacity() 方法:

在这里插入图片描述

  可以看到,进入之后,会进行一个 if 判断:分析之后易得,calculateCapacity() 方法返回的是DEFAULT_CAPACITY值为10

private static int calculateCapacity(Object[] elementData, int minCapacity) {
    // 如果elementData是默认的空数组,则返回默认容量和最小容量中的较大值
    // 这是因为当ArrayList初始化时使用的是空数组,此时需要根据传入的最小容量和默认容量来确定实际容量
    if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA) {
        return Math.max(DEFAULT_CAPACITY, minCapacity);
    }
    // 如果elementData不是默认的空数组,直接返回传入的最小容量
    // 这种情况通常发生在ArrayList已经初始化过,且有数据的情况下,此时只需根据传入的最小容量来确定容量即可
    return minCapacity;
}

  执行完之后,我们返回ensureCapacityInternal()方法,继续执行ensureExplicitCapacity() 方法,此时我们传入的参数为10

在这里插入图片描述

  进入ensureExplicitCapacity() 方法:

在这里插入图片描述

private void ensureExplicitCapacity(int minCapacity) {
    // 每次尝试修改ArrayList的容量时,modCount都会增加,这用于支持快速失败的迭代器
    modCount++;

    // 溢出敏感的代码,防止整数溢出
    // 如果最小容量大于当前数组的长度,则需要扩容
    if (minCapacity - elementData.length > 0)
        // 调用grow方法进行扩容,传入最小容量
        grow(minCapacity);
}

  可以看到,minCapacity - elementData.length = 10 - 0 > 0 ,所以将会调用 grow() 方法对elementData 数组进行扩容,我们接下来继续进入grow() 方法:

在这里插入图片描述

private void grow(int minCapacity) {
    // 获取当前数组的长度
    int oldCapacity = elementData.length;
    // 新容量为旧容量的1.5倍,通过位运算实现
    int newCapacity = oldCapacity + (oldCapacity >> 1);
    // 如果新容量仍小于最小容量,则将新容量设为最小容量
    if (newCapacity - minCapacity < 0)
        newCapacity = minCapacity;
    // 如果新容量大于最大数组大小,则调用hugeCapacity方法计算新容量
    if (newCapacity - MAX_ARRAY_SIZE > 0)
        newCapacity = hugeCapacity(minCapacity);
    // 使用Arrays.copyOf方法复制数组,扩容到新容量
    // 由于minCapacity通常接近当前元素个数,所以这种方式效率较高
    elementData = Arrays.copyOf(elementData, newCapacity);
}

流程为:

  1. int oldCapacity = elementData.length 此时,elementData 数组是一个空数组,所以长度为0

  2. int newCapacity = oldCapacity + (oldCapacity >> 1); // 新容量为旧容量的1.5倍,通过位运算实现

    由于 oldCapacity 此时等于0,所以 newCapcity 计算出来也为0。

  3. // 如果新容量仍小于最小容量,则将新容量设为最小容量 if (newCapacity - minCapacity < 0) newCapacity = minCapacity;

  4. 接下来,直接执行最后一句,数组的复制,这一句执行完成之后,elementData 数组的扩容便完成了。

在这里插入图片描述

  然后,让我们返回梦开始的地方,add() 方法:

在这里插入图片描述

public boolean add(E e) {
    ensureCapacityInternal(size + 1);  // Increments modCount!!
	//扩容成功之后,开始把元素存入elementData数组中
    elementData[size++] = e;
    return true;
}

  后续继续添加元素,直到 elementData 数组满了,将会继续按上面的源码逻辑进行扩容。

总结:

ArrayList 使用无参构造器,elementData 数组会初始化为一个空数组,开始添加元素之后,如果是第一个元素,elementData 数组会自动扩容为 10,后续再次扩容时将会按照原来大小的 1.5 倍进行扩容。

(2)使用有参构造器指定大小

  感兴趣的小伙伴,可以根据上述方法进行源码分析。由于通过构造器指定 elementData 数组大小与无参构造器差别较小,我在这里只说明不同的部分。

  传入一个 int 型参数:

import java.util.ArrayList;

public class ArrayList01 {
    public static void main(String[] args) {
        //使用有参构造器
        ArrayList arrayList = new ArrayList(2);

        for(int i = 1; i <= 10; i++) {
            arrayList.add(i);
        }
    }
}

在这里插入图片描述

  可以看到,elementData 数组会通过我们传入的参数进行初始化。


5 Vector 底层源码分析

5.1 结论

  1. Vector 类的定义说明

在这里插入图片描述

  1. Vector 底层也是一个对象数组,protected Object[] elementData;
  2. Vector 是线程同步的,即线程安全,Vector 类的操作方法带有 synchronized

在这里插入图片描述

  1. 在开发中,需要线程同步安全时,考虑使用 Vector。

  2. 与 ArrayList 的比较:

底层结构版本线程安全(同步)效率扩容倍数
ArrayList可变数组jdk 1.2不安全,效率高如果是有参构造器,按原来的1.5倍扩容
如果是无参构造器
1.第一次给一个默认容量10
2.从第二次开始,按原来的1.5倍扩容
Vector可变数组 Object[]jdk 1.0安全,效率低如果是无参构造器,默认10个容量,满之后,按原来的2倍扩容
如果是指定大小,则每次都按原来的2倍扩容

5.2 底层源码分析

(1)无参构造器

  以下面的代码进行分析:

import java.util.Vector;

public class Vector_ {
    public static void main(String[] args) {
        Vector vector = new Vector();

        for(int i = 1; i <= 10; i++) {
            vector.add(i);
        }
        vector.add(11);
    }
}

  首先,我们调用无参构造器:

在这里插入图片描述

分析: 可以看到,无参构造器中,使用 this(10) 调用同类中的有参构造器,有参构造器中又调用另一个构造器 this(initialCapacity,capacityIncrement) ,我们进入这个构造器中:

在这里插入图片描述

  执行之后,elementData 数组获得了10个容量。

  在我们添加到10个元素之前,elementData 数组都不会进行扩容,直到我们开始添加第11个元素:

在这里插入图片描述

分析: 此时,我们进入 add() 方法中,将执行 ensureCapacityHelper() 方法:

在这里插入图片描述

分析: 此时,我们传入的 minCapacity 的值为 11,即我们需要 11 个容量才能装下数据。进行 if 判断,所需的最小容量大于 elementData 数组的容量,所以我们执行 grow() 方法进行扩容,接下来我们进入 grow() 方法:

在这里插入图片描述

分析: 扩容的算法为:

private void grow(int minCapacity) {
    // 获取当前数组的容量
    int oldCapacity = elementData.length;
    // 计算新的容量,如果capacityIncrement大于0,
    //则新的容量为旧容量加上capacityIncrement,
    // 否则新的容量为旧容量的两倍,这是一种溢出意识的代码,防止整数溢出
    int newCapacity = oldCapacity + ((capacityIncrement > 0) ?
                                     capacityIncrement : oldCapacity);
    // 如果新容量仍小于最小需要的容量,则将新容量设置为最小需要的容量
    if (newCapacity - minCapacity < 0)
        newCapacity = minCapacity;
    // 如果新容量大于数组允许的最大容量,则调用hugeCapacity方法来确定新容量
    if (newCapacity - MAX_ARRAY_SIZE > 0)
        newCapacity = hugeCapacity(minCapacity);
    // 使用Arrays.copyOf方法将原数组复制到新数组中,新数组的容量为newCapacity
    elementData = Arrays.copyOf(elementData, newCapacity);
}

  扩容成功之后,elementData 数组可以存放下,第11个元素,我们返回 add() 方法进行添加。

在这里插入图片描述

(2)使用有参构造器指定大小

  这里不再过多赘述,只是在创建时,elementData 数组的初始容量有所变化,其他完全一样。


6 LinkedList 底层结构和源码分析

6.1 结论

  1. LinkedList 底层实现了双向链表和双端队列特点
  2. 可以添加任意元素(元素可以重复),包括可以添加 null
  3. 线程不安全,没有实现同步
  4. LinkedList 底层维护了一个双向链表
  5. LinkedList 中维护了两个属性 first 和 last 分别指向首节点和尾节点

在这里插入图片描述

  1. 每个节点(Node 对象),里面又维护了 prev、next、item 三个属性,其中通过 prev 指向前一个,通过 next 指向下一个节点。最终实现双向链表。

在这里插入图片描述

  1. 因此,LinkedList 的元素的 添加和删除 ,不是通过数组完成的,相对来说效率较高。

6.2 LinkedList 的增加

用以下代码进行分析:

import java.util.LinkedList;

public class LinkedList_ {
    public static void main(String[] args) {
        LinkedList list = new LinkedList();

        //添加
        list.add("one");
        list.add("second");
        list.add("third");
    }
}

  首先,调用无参构造器进行初始化:

在这里插入图片描述

  初始化之后,list 的结构为:

在这里插入图片描述

  开始添加,我们进入 add() 方法:

在这里插入图片描述

分析: 调用 linkLast() 方法,我们进入此方法:

在这里插入图片描述

void linkLast(E e) {
    // 获取当前链表的最后一个节点
    final Node<E> l = last;
    // 创建一个新的节点,其前驱节点为当前最后一个节点,后继节点为null,元素为e
    final Node<E> newNode = new Node<>(l, e, null);
    // 将新节点设置为链表的最后一个节点
    last = newNode;
    // 如果链表原本为空(即最后一个节点为null),则将新节点也设置为第一个节点
    if (l == null)
        first = newNode;
    // 否则,将原最后一个节点的后继节点指向新节点
    else
        l.next = newNode;
    // 链表大小加1
    size++;
    // 修改计数加1,用于支持迭代器的快速失败机制
    modCount++;
}

分析: 这是我们第一次添加元素,此时的链表是为空的。

final Node<E> l = last :此时,first 和 last 都为 null,所以执行之后 l = null

final Node<E> newNode = new Node<>(l, e, null); :这里创建一个新的节点,把 l 传给节点的 prev ,e 传给节点的 item,null 传给节点的 next。

last = newNode; :让 last 指向新节点。

此时,l == null 为真,执行 first = newNode :让 first 指向新节点。

在这里插入图片描述

开始添加第二个元素

  我们进入 add() 方法:

在这里插入图片描述

  同样,我们进入 linkLast() 方法:

在这里插入图片描述

void linkLast(E e) {
    // 获取当前链表的最后一个节点
    final Node<E> l = last;
    // 创建一个新的节点,其前驱节点为当前最后一个节点,后继节点为null,元素为e
    final Node<E> newNode = new Node<>(l, e, null);
    // 将新节点设置为链表的最后一个节点
    last = newNode;
    // 如果链表原本为空(即最后一个节点为null),则将新节点也设置为第一个节点
    if (l == null)
        first = newNode;
    // 否则,将原最后一个节点的后继节点指向新节点
    else
        l.next = newNode;
    // 链表大小加1
    size++;
    // 修改计数加1,用于支持迭代器的快速失败机制
    modCount++;
}

分析: 现在,我们正在添加第二个元素。

final Node<E> l = last; :新建一个 Node 为 l,此时,last 指向 第一个节点,所以这句执行之后,l 也指向了 第一个节点。

在这里插入图片描述

final Node<E> newNode = new Node<>(l, e, null); :这里创建一个新的节点,把 l 传给节点的 prev ,e 传给节点的 item,null 传给节点的 next。所以这句执行之后,新节点的 prev 指向了第一个节点。

在这里插入图片描述

last = newNode; :让 last 指向新节点。

在这里插入图片描述

if 进行判断,此时 l 不为 null ,所以执行 else 分支的语句,l.next = newNode; :执行之后,第一个节点的 next 指向了第二个节点:

在这里插入图片描述

至此,list 中已经成功的添加了2个元素。

6.3 LinkedList 的删除

  使用以下代码进行分析:

import java.util.LinkedList;

public class LinkedList_ {
    public static void main(String[] args) {
        LinkedList list = new LinkedList();

        //添加
        list.add("one");
        list.add("second");
        list.add("third");

        //删除
        list.remove(1);
    }
}

  我们进入 remove() 方法:

在这里插入图片描述

  首先,对传入的参数进行一个检验,使用 checkElementIndex() 方法:

在这里插入图片描述

在这里插入图片描述

  检验完成之后,开始执行 unlink() 方法,我们进入此方法:

E unlink(Node<E> x) {
    // 断言x不为空,确保传入的节点有效
    // assert x != null;
    // 获取要删除节点的元素
    final E element = x.item;
    // 获取要删除节点的后继节点
    final Node<E> next = x.next;
    // 获取要删除节点的前驱节点
    final Node<E> prev = x.prev;

    // 如果要删除的节点是第一个节点
    if (prev == null) {
        // 将第一个节点指向其后继节点
        first = next;
    } else {
        // 将前驱节点的后继指向要删除节点的后继
        prev.next = next;
        // 断开要删除节点与其前驱的连接
        x.prev = null;
    }

    // 如果要删除的节点是最后一个节点
    if (next == null) {
        // 将最后一个节点指向其前驱
        last = prev;
    } else {
        // 将后继节点的前驱指向要删除节点的前驱
        next.prev = prev;
        // 断开要删除节点与其后继的连接
        x.next = null;
    }

    // 清除要删除节点的元素引用,便于垃圾回收
    x.item = null;
    // 链表大小减1
    size--;
    // 修改计数加1,用于支持迭代器的快速失败机制
    modCount++;
    // 返回被删除节点的元素
    return element;
}

在这里插入图片描述

final E element = x.item; :获取要删除节点的元素,我们要删除的是索引为1的节点, 由于索引从0开始,即要删除的节点为上图的中间节点。element = "second"

final Node<E> next = x.next; :获取要删除节点的后继节点,即 next 指向最后一个节点。
final Node<E> prev = x.prev; :获取要删除节点的前驱节点,即 prev 指向第一个节点。

在这里插入图片描述

由于我们要删除的是中间节点,所以 prev 和 next 都不为 null ,所以执行else 分支语句:

  • prev.next = next; :将前驱节点的后继指向要删除节点的后继

    x.prev = null; :断开要删除节点与其前驱的连接

  • next.prev = prev; :将后继节点的前驱指向要删除节点的前驱

    x.next = null; : 断开要删除节点与其后继的连接

在这里插入图片描述

到这里,再继续执行,元素就删除成功了。


6.4 LinkedList 和 ArrayList 的比较

底层结构增删的效率改查的效率
ArrayList可变数组较低
数组扩容
较高
LinkedList双向链表较高
通过链表追加
较低

如何选择 ArrayList 和 LinkedLis:

  1. 如果我们改查的操作多,选择 ArrayList
  2. 如果我们增删的操作多,选择 LinkedList
  3. 一般来说,在程序中,80%-90%都是查询,因此大部分情况下会选择 ArrayList
  4. 在一个项目中,根据业务灵活选择,也可能是,一个模块使用 ArrayList ,另一个模块使用的是 LinkedList,也就是说,要根据业务进行选择。

7 Set 接口和常用方法

7.1 基本介绍

  1. 无序(添加和取出的顺序不一致),没有索引
  2. 不允许重复元素,所以最多包含一个 null
  3. JDK API 中 Set 接口的实现类有:

在这里插入图片描述

  1. 和 List 接口一样,Set 接口也是 Collection 的子接口,因此,常用方法和 Collection 接口一样。

遍历方法:

  • 使用迭代器

  • 使用增强 for 循环

  • 要注意不能使用索引来进行遍历

    因为,Set 接口中没有 get() 方法


8 HashSet 的底层结构和源码分析

8.1 结论

  1. HashSet 实现了 Set 接口
  2. HashSet 实际上是 HashMap,从下图构造器可以看出:

在这里插入图片描述

  1. 可以存放 null 值,但是只能有一个 null
  2. HashSet 不保证元素是有序的,取决于 hash() 方法之后,再确定索引的结果。(即,不保证存放元素的顺序和取出的顺序一致)
  3. 不能有重复元素/对象。

8.2 源码分析

8.2.1 HashMap 简要介绍

  HashSet 的底层是 HashMap,HashMap 的底层是(数组 + 链表 + 红黑树)。

​ HashMap 底层维护了一个 table 表,该表由数组 + 链表组成:

在这里插入图片描述

在这里插入图片描述

步骤为:

  1. 先获取元素的哈希值(hashCode 方法)。

  2. 对哈希值进行一个运算,得出一个索引值,即为要存放在哈希表中的位置号。

  3. 如果该位置上没有其他元素,则直接进行存放

    如果该位置上已经有其他元素,则需要进行 equal() 方法判断(注意equals 方法是可以由程序员自己重写的),如果相等,则不再添加,如果不相等,则以链表的方式添加。

8.2.2 HashSet 源码分析
(1)结论
  1. HashSet 底层是 HashMap
  2. 添加一个元素时,先得到 hash 值,该值会转成索引值。
  3. 找到存储数据表 table,看这个索引位置是否已经存放了元素。
  4. 如果没有,直接加入。
  5. 如果有,调用 equals 比较,如果相同,就放弃添加;如果不相同,则添加到最后。
  6. 在 Java8 中,如果一条链表的元素个数到达 TREEIFY_THRESHOLD (默认值是8),并且 table 的大小 >= MIN_TREEIFY_CAPACITY (默认是64),就会进行树化(红黑树)。
(2)源码分析

用以下代码进行分析:

import java.util.HashSet;

public class HashSet_ {
    public static void main(String[] args) {
        HashSet hashSet = new HashSet();
        
        //添加元素
        hashSet.add("xiong");
        hashSet.add("study");
        hashSet.add("xiong");
    }
}

  我们直接从 add() 方法开始,第一次添加元素,进入 add() 方法:

在这里插入图片描述

  其中,PRESENT 的定义为:它的作用是:为使用HashMap,而进行的一个占位。

private static final Object PRESENT = new Object();

  我们继续进入 map.put() 的方法之中:

在这里插入图片描述

  接下来,我们分析 hash() 方法,进入该方法:

在这里插入图片描述

static final int hash(Object key) {
    int h;
    // 如果键为null,返回0作为其哈希值
    return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
    // 否则,先获取键的hashCode值赋给h
    // 然后将h与h右移16位后的值进行异或运算,得到最终的哈希值
    // 这样做可以减少哈希冲突,使高位和低位的信息都能参与到哈希值的计算中
}

  通过 hash() 方法,将会计算出一个 hash 值,这个值将用于哈希表的索引计算。

  计算完 hash 值之后,我们接着进入 putVal() 方法:

final V putVal(int hash, K key, V value, boolean onlyIfAbsent, boolean evict) {
    Node<K,V>[] tab; Node<K,V> p; int n, i;
    // 检查哈希表是否初始化,如果未初始化,则进行初始化
    if ((tab = table) == null || (n = tab.length) == 0)
        n = (tab = resize()).length;
    // 计算哈希值对应的索引位置
    if ((p = tab[i = (n - 1) & hash]) == null)
        // 如果该位置为空,则直接插入新节点
        tab[i] = newNode(hash, key, value, null);
    else {
        Node<K,V> e; K k;
        // 检查链表头节点是否与要插入的键相同
        if (p.hash == hash &&
            ((k = p.key) == key || (key != null && key.equals(k))))
            e = p;
        // 如果头节点是树节点,则调用树化方法插入
        else if (p instanceof TreeNode)
            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);
                    // 如果链表长度超过阈值,则进行树化
                    if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
                        treeifyBin(tab, hash);
                    break;
                }
                // 如果找到相同键的节点,则退出循环
                if (e.hash == hash &&
                    ((k = e.key) == key || (key != null && key.equals(k))))
                    break;
                p = e;
            }
        }
        // 如果找到相同键的节点,则更新值
        if (e != null) { // existing mapping for key
            V oldValue = e.value;
            if (!onlyIfAbsent || oldValue == null)
                e.value = value;
            afterNodeAccess(e);
            return oldValue;
        }
    }
    // 修改计数加1
    ++modCount;
    // 如果哈希表大小超过阈值,则进行扩容
    if (++size > threshold)
        resize();
    // 插入节点后的钩子方法,用于支持子类的扩展
    afterNodeInsertion(evict);
    return null;
}

分析:

  1. 初始化检查

    • if ((tab = table) == null || (n = tab.length) == 0):检查哈希表是否已经初始化。如果未初始化,则调用resize()方法进行初始化,并获取新的表长n

    分析 resize() 方法,由于我们是第一次添加,所以 table 为 null,进入 resize() 方法:

    final Node<K,V>[] resize() {
        Node<K,V>[] oldTab = table; // 旧的哈希表
        int oldCap = (oldTab == null) ? 0 : oldTab.length; // 旧的哈希表容量
        int oldThr = threshold; // 旧的阈值
        int newCap, newThr = 0; // 新的哈希表容量和新的阈值
    
        // 如果旧的哈希表容量大于0
        if (oldCap > 0) {
            // 如果旧的哈希表容量已经达到最大值
            if (oldCap >= MAXIMUM_CAPACITY) {
                threshold = Integer.MAX_VALUE; // 设置阈值为最大值
                return oldTab; // 返回旧的哈希表
            }
            // 否则,尝试将哈希表容量翻倍
            else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY &&
                     oldCap >= DEFAULT_INITIAL_CAPACITY)
                newThr = oldThr << 1; // 阈值也翻倍
        }
        // 如果旧的哈希表容量为0,但阈值大于0,说明初始容量被放在阈值中
        else if (oldThr > 0)
            newCap = oldThr;
        // 否则,使用默认的初始容量和负载因子
        else {
            newCap = DEFAULT_INITIAL_CAPACITY;
            newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY);
        }
    
        // 如果新的阈值为0,计算新的阈值
        if (newThr == 0) {
            float ft = (float)newCap * loadFactor;
            newThr = (newCap < MAXIMUM_CAPACITY && ft < (float)MAXIMUM_CAPACITY ?
                      (int)ft : Integer.MAX_VALUE);
        }
        threshold = newThr; // 更新阈值
        @SuppressWarnings({"rawtypes","unchecked"})
        Node<K,V>[] newTab = (Node<K,V>[])new Node[newCap]; // 创建新的哈希表
        table = newTab; // 更新哈希表引用
    
        // 如果旧的哈希表不为空,重新分配节点到新的哈希表
        if (oldTab != null) {
            for (int j = 0; j < oldCap; ++j) {
                Node<K,V> e;
                if ((e = oldTab[j]) != null) {
                    oldTab[j] = null; // 清空旧的哈希表位置
                    if (e.next == null)
                        newTab[e.hash & (newCap - 1)] = e; // 如果链表只有一个节点,直接放入新的哈希表
                    else if (e instanceof TreeNode)
                        ((TreeNode<K,V>)e).split(this, newTab, j, oldCap); // 如果是树节点,调用split方法重新分配
                    else { // 重新分配链表节点
                        Node<K,V> loHead = null, loTail = null; // 低位链表头尾
                        Node<K,V> hiHead = null, hiTail = null; // 高位链表头尾
                        Node<K,V> next;
                        do {
                            next = e.next;
                            // 根据哈希值决定节点放入低位还是高位
                            if ((e.hash & oldCap) == 0) {
                                if (loTail == null)
                                    loHead = e;
                                else
                                    loTail.next = e;
                                loTail = e;
                            }
                            else {
                                if (hiTail == null)
                                    hiHead = e;
                                else
                                    hiTail.next = e;
                                hiTail = e;
                            }
                        } while ((e = next) != null);
                        // 清空链表尾部的next指针
                        if (loTail != null) {
                            loTail.next = null;
                            newTab[j] = loHead;
                        }
                        if (hiTail != null) {
                            hiTail.next = null;
                            newTab[j + oldCap] = hiHead;
                        }
                    }
                }
            }
        }
        return newTab; // 返回新的哈希表
    }
    
    1. 初始化变量
      • Node<K,V>[] oldTab = table;:获取当前的哈希表,此时 tablenull
      • int oldCap = (oldTab == null) ? 0 : oldTab.length;:旧的哈希表容量为0。
      • int oldThr = threshold;:旧的阈值为0(默认情况下)。
      • int newCap, newThr = 0;:声明新的哈希表容量和新的阈值。
    2. 处理旧哈希表容量
      • if (oldCap > 0):旧的哈希表容量为0,不执行此分支。
      • else if (oldThr > 0):旧的阈值为0,不执行此分支。
      • else:使用默认的初始容量和负载因子。负载因子DEFAULT_LOAD_FACTOR(0.75)
        • newCap = DEFAULT_INITIAL_CAPACITY;:新的容量为默认初始容量(通常是16)。
        • newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY);:计算新的阈值(通常是12)。
    3. 计算新的阈值
      • if (newThr == 0):新的阈值已经计算为12,不执行此分支。
      • threshold = newThr;:更新阈值为12。
    4. 创建新的哈希表
      • Node<K,V>[] newTab = (Node<K,V>[])new Node[newCap];:创建新的哈希表,容量为16。
      • table = newTab;:更新哈希表引用。
    5. 返回新的哈希表
      • return newTab;:返回新的哈希表。
  2. 计算索引位置

    • if ((p = tab[i = (n - 1) & hash]) == null):计算哈希值对应的索引位置i,并检查该位置是否为空。如果为空,则直接插入新节点。
  3. 处理链表头节点

    • if (p.hash == hash && ((k = p.key) == key || (key != null && key.equals(k)))):检查链表头节点是否与要插入的键相同。如果相同,则将头节点赋值给e
    • else if (p instanceof TreeNode):如果头节点是树节点,则调用putTreeVal方法进行树化插入。
  4. 遍历链表

    • for (int binCount = 0; ; ++binCount):遍历链表,查找是否存在相同键的节点。
    • if ((e = p.next) == null):如果链表末尾,则插入新节点。
    • if (e.hash == hash && ((k = e.key) == key || (key != null && key.equals(k)))):如果找到相同键的节点,则退出循环。
  5. 更新值

    • if (e != null):如果找到相同键的节点,则更新值,并返回旧值。
  6. 修改计数和扩容

    • ++modCount:修改计数加1,用于支持迭代器的快速失败机制。
    • if (++size > threshold):如果哈希表大小超过阈值,则进行扩容。
  7. 插入节点后的钩子方法

    • afterNodeInsertion(evict):插入节点后的钩子方法,用于支持子类的扩展。
(3)总结
  1. HashSet 底层是 HashMap,第一次添加时,table 数组扩容到16,临界值(threshold)是16 * 加载因子(loadFactor)是0.75 = 12

  2. 如果 table 数组使用到了临界值 12,就会扩容到 16 * 2 = 32,新的临界值就是 32 * 0.75 = 24,以此类推

  3. 在 Java8 中,如果一条链表的元素个数到达 TREEIFY_THRESHOLD (默认值是8),并且 table 的大小 >= MIN_TREEIFY_CAPACITY (默认是64),就会进行树化(红黑树)。

    如果一条链表的元素个数到达了8,而此时,table 的大小为 16,继续添加元素到链表后面,链表长度会为9,同时 table 会进行扩容,大小为 16 * 2 = 32,继续添加元素到链表后面,链表长度变为 10,同时 table 数组会进行扩容,大小为 32 * 2 = 64。

    在这种情况下,继续在这条链表后面添加元素,该链表会马上进行树化。


9 LinkedHashSet 分析与使用

  1. LinkedHashSet 是 HashSet 的子类

  2. LinkedHashSet 底层是一个 LinkedHashMap,底层维护了一个 数组 + 双向链表

    创建一个 LinkedHashSet 对象:可以看到其源码创建的实际上是 LinkedHashMap

    LinkedHashSet list = new LinkedHashSet();
    

在这里插入图片描述

在这里插入图片描述

  1. LinkedHashSet 根据元素的 hashCode 值来决定元素的存储位置,同时使用链表维护元素的次序,这使得元素看起来是以插入顺序保存的。

  2. LinkedHashSet 不允许添加重复元素。

结论与源码分析:

  1. 在 LinkedHashSet 中维护了一个 hash 表和双向链表(LinkedHashSet 有head 和 tail)

在这里插入图片描述

  1. 每一个节点有 before 和 after 属性,这样可以形成双向链表。

    在LinkedHashMap 中,使用的节点是 Entry,它继承了 HashMap 的 Node 节点:

在这里插入图片描述

  1. 在添加一个元素时,先求 hash 值,再求索引,确定该元素在 table 的位置,然后将添加的元素加入到双向链表(如果已经存在,不添加【添加原则和 HashSet 一样】)
tail.next = newElement;
newElement.pre = tail;
tail = newElement;
  1. 这样的话,我们遍历 LinkedHashSet 也能确保插入顺序和遍历顺序一致
import java.util.LinkedHashSet;

public class LinkedHashSet_ {
    public static void main(String[] args) {
        LinkedHashSet linkedHashSet = new LinkedHashSet();

        linkedHashSet.add("jack");
        linkedHashSet.add("xiong");
        linkedHashSet.add("sing");

    }
}

在这里插入图片描述


10 Map 接口

10.1 Map 接口实现类的特点

JDK 8 的 Map 的特点:

  1. Map 与 Collection 并列存在。用于保存具有映射关系的数据:Key - Value

  2. Map 中的 key 和 value 可以任何引用类型的数据,会封装到 HashMap$Node 对象中

  3. Map 中的 key 不允许重复,原因和 HashSet 的原理一样。

  4. Map 中的 value 可以重复。

  5. Map 的 key 可以为 null,value 也可以为 null,注意 key 为 null ,只能有一个,value 为 null,可以多个。即,key 具有唯一性,不允许重复,value 则没有这个限制。

  6. 常用 String 类作为 Map 的 key。但实际上,key , value 接收类型都为 Object。

  7. key 和 value 之间存在单向一对一关系,即通过指定的 key 总能找到对应的 value。

  8. Map 存放数据的 key - value 示意图,一对 k - v 是放在一个 HashMap$Node 中的,因为 Node 实现了 Entry 接口。

在这里插入图片描述

  也就是说,Map 在存放数据的时候,会将数据 Hash$Node 类型转为 Map.Entry,再将转化后的数据,存入EntrySet 集合中,EntrySet 集合存放的数据类型为 Map.Entry,但是实际存放的类型是 HashMap$Node,这是因为 HashMap$Node implement Map.Entry

  为什么要这样操作呢?

  这是因为 Map.Entry 提供了 getKey() 和 getValue() 方法,可以直接获取 key - value 的值。简单来说,就是为了方便我们进行数据的遍历和查找。

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

public class HashMap_ {
    public static void main(String[] args) {
        Map map = new HashMap();

        map.put("no1","熊大");
        map.put("no2","熊二");


    }
}

在这里插入图片描述

  Map 也提供了,KeySet 和 Values 内部类,可以用来单独的存放 key 值和 value 值。

//使用 keySet() 方法和 values() 方法
Set set1 = map.keySet();
Collection values = map.values();

10.2 Map 接口的常用方法

  1. put :添加
  2. remove :根据键删除映射关系
  3. get :根据键获取值(即根据 key,获取对应的 value)
  4. size :获取元素个数
  5. isEmpty :判断个数是否为 0
  6. clear :清除所有元素
  7. containsKey :查找键是否存在

通过代码进行讲解:

@SuppressWarnings({"all"})
public class MapMethod {
    public static void main(String[] args) {
        //演示map接口常用方法
        Map map = new HashMap();
        map.put("one", new Book("", 100));//OK
        //需要注意的是,如果添加的值,key 已经存在了,
        //但是 value 不同,此时会进行替换
        //即用新的 key,value 值替换原来的
        map.put("one", "熊大");//替换
        map.put("two", "熊二");//OK
        map.put("three", "光头强");//OK
        map.put("four", null);//OK
        map.put(null, "刘亦菲");//OK
        map.put("five", "吉吉国王");//OK
        map.put("xiong", "xiong 的小弟");
        System.out.println("map=" + map);

        //remove:根据键删除映射关系
        map.remove(null);
        System.out.println("map=" + map);
        //get:根据键获取值
        Object val = map.get("鹿晗");
        System.out.println("val=" + val);
        //size:获取元素个数
        System.out.println("k-v=" + map.size());
        //isEmpty:判断个数是否为0
        System.out.println(map.isEmpty());//F
        //clear:清除 k-v
        //map.clear();
        System.out.println("map=" + map);
        //containsKey:查找键是否存在
        System.out.println("结果=" + map.containsKey("xiong"));//T
    }
}

class Book {
    private String name;
    private int num;

    public Book(String name, int num) {
        this.name = name;
        this.num = num;
    }
}

  看结果:

在这里插入图片描述

10.3 Map 接口的遍历方法

  1. containsKey :查找键是否存在
  2. keySet :获取所有的键(key)
  3. entrySet :获取所有关系 k - z
  4. values :获取所有的值(value)

使用代码进行具体演示和讲解:

import javax.imageio.stream.IIOByteBuffer;
import java.util.*;

@SuppressWarnings({"all"})
public class MapFor {
    public static void main(String[] args) {
        Map map = new HashMap();
        map.put("one", "熊大");
        map.put("two", "熊二");
        map.put("three", "光头强");
        map.put("four", null);

        //第一组: 先取出 所有的Key, 通过Key 取出对应的Value
        Set keyset = map.keySet();
        //1.使用增强for 循环
        for (Object key : keyset) {
            System.out.println(key + "-" + map.get(key));
        }
        //2.使用迭代器
        Iterator iterator = keyset.iterator();
        while (iterator.hasNext()) {
            Object key =  iterator.next();
            System.out.println(key + "-" + map.get(key));

        }

        //第二组:把所有的 values 取出
        Collection values = map.values();
        //可以使用 Collection 接口的遍历方法
        //1.增强for 循环
        for (Object value : values) {
            //因为不能通过 value 查找 key ,所以只能输出 value
            System.out.println(value);
        }
        //2.使用迭代器
        while (iterator.hasNext()) {
            Object value =  iterator.next();
            System.out.println(value);
        }

        //第三组:通过 EntrySet 来获取 k-v
        Set entrySet = map.entrySet();
        //1.增强for循环
        for(Object entry: entrySet) {
            Map.Entry entry1 = (Map.Entry) entry;
            System.out.println(entry1.getKey() + "-" +
                    entry1.getValue());
        }
        //2.迭代器
        while (iterator.hasNext()) {
            Object entry =  iterator.next();
            Map.Entry entry1 = (Map.Entry) entry;
            System.out.println(entry1.getKey() + "-" +
                    entry1.getValue());
        }
    }
}

11 HashMap 底层结构和源码解读

11.1 结论

  1. Map 接口的常用实现类:HashMap、Hashtable 和 Properties。
  2. HashMap 是 Map 接口使用频率最高的实现类。
  3. HashMap 是以 key - value 对的方式来存储数据(HashMap$Node 类型)
  4. key 不能重复,但是值可以重复,允许使用 null 键 和 null 值。
  5. 如果添加相同的 key ,则会覆盖原来的 key - value,等同于修改。(key 不会替换,value 会替换)
  6. 与 HashSet 一样,不保证映射的顺序,因为底层是以 hash 表的方式来存储的。(JDK 8 的 hashMap 底层 数组 + 链表 + 红黑树)
  7. HashMap 没有实现同步,因此是线程不安全的,方法没有做同步互斥的操作,没有 synchronized

11.2 源码分析

  先分析下面的示意图:

在这里插入图片描述

  • (k, v) 是一个 Node 实现了 Map.Entry<K, V> ,查看 HashMap 源码可以清楚的知道。
  • jdk 7.0 的 HashMap 底层实现(数组 + 链表),jdk 8.0 底层(数组 + 链表 + 红黑树)
(1)结论

扩容机制和 HashSet 相同。

  1. HashMap 底层维护了 Node 类型的数组 table,默认为 null。

  2. 当创建对象时,将加载因子(loadfactor)初始化为 0.75

  3. 当添加 key - value 时,通过 key 的哈希值得到在 table 的索引。然后判断该索引处是否有元素,如果没有元素直接添加;如果该索引处有元素,继续判断该元素的 key 准备加入的 key 是否相等。如果相等,则直接替换 value;如果不相等,则需要判断当前位置是树结构还是链表结构,然后做出相应的处理,如果添加时发现 table 表的容量不够,则需要进行扩容。

  4. 第一次添加,则需要扩容 table 的容量为 16,临界值(threshold)为 12 (16 * 0.75 得出)

  5. 以后再扩容,则会把 table 表的容量扩大为原来的2倍,临界值为原来的2倍,依次类推。

  6. 在 Java8 中,如果一条链表的元素个数到达 TREEIFY_THRESHOLD (默认值是8),并且 table 的大小 >= MIN_TREEIFY_CAPACITY (默认是64),就会进行树化(红黑树)。

    如果一条链表的元素个数到达了8,而此时,table 的大小为 16,继续添加元素到链表后面,链表长度会为9,同时 table 会进行扩容,大小为 16 * 2 = 32,继续添加元素到链表后面,链表长度变为 10,同时 table 数组会进行扩容,大小为 32 * 2 = 64。

    在这种情况下,继续在这条链表后面添加元素,该链表会马上进行树化。

(2)源码分析

  以下面代码进行分析:

import java.util.HashMap;

public class HashMap_ {
    public static void main(String[] args) {
        HashMap hashMap = new HashMap();

        hashMap.put(1,1);
        hashMap.put(1,2);

        System.out.println(hashMap);
    }
}

  首先,我们进入 HashMap 的无参构造器:可以看到加载因子(loadfactor)初始化为 0.75

在这里插入图片描述

  观察 debug 的控制台:可以看到 table 刚开始确实为 null

在这里插入图片描述

  接下来,我们开始添加数据,进入put方法:

在这里插入图片描述

  继续进入 hash() 方法:可以看到 哈希值计算的算法

在这里插入图片描述

  接着进入putVal() 方法,进行第一次添加数据,原理和 HashSet 是一样的,这里就不再重复讲述了。

  我们下面来重点关注,hashMap.put(1,1); hashMap.put(1,2); 中的第二条语句,value 值的替换,同样的我们直接进入,putVal() 方法:

final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
               boolean evict) {
    Node<K,V>[] tab; Node<K,V> p; int n, i;
    // 获取哈希表数组
    if ((tab = table) == null || (n = tab.length) == 0)
        // 如果哈希表为空或长度为0,初始化哈希表
        n = (tab = resize()).length;
    // 计算table表的索引
    if ((p = tab[i = (n - 1) & hash]) == null)
        // 如果table表的该位置为空,创建新节点并放入该位置
        tab[i] = newNode(hash, key, value, null);
    else {
        Node<K,V> e; K k;
        // table表的该位置不为空,检查第一个节点
        if (p.hash == hash &&
            ((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);
                    if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
                        // 如果链表长度超过阈值,树化链表
                        treeifyBin(tab, hash);
                    break;
                }
                if (e.hash == hash &&
                    ((k = e.key) == key || (key != null && key.equals(k))))
                    // 找到相同键的节点
                    break;
                p = e;
            }
        }
        // 如果找到相同键的节点
        if (e != null) { // existing mapping for key
            V oldValue = e.value;
            // 替换值
            if (!onlyIfAbsent || oldValue == null)
                e.value = value;
            // 调用 afterNodeAccess 方法
            afterNodeAccess(e);
            // 返回旧值
            return oldValue;
        }
    }
    // 增加 modCount 和 size
    ++modCount;
    if (++size > threshold)
        // 如果 size 超过阈值,扩容
        resize();
    // 调用 afterNodeInsertion 方法
    afterNodeInsertion(evict);
    return null;
}

12 Hashtabel 的解读

12.1 基本介绍

  1. 存放的元素是键值对:即 K - V
  2. Hashtable 的键和值都不能为 null,否则会抛出 NullPointerException 异常。
  3. Hashtable 的使用方法基本和 HashMap 一样。
  4. Hashtable 是线程安全的(使用的方法基本上3都有 synchronized 关键字),HashMap 是线程不安全的

12.2 简单的源码分析

(1)结论
  1. Hashtable 在底层维护了一个数组 table 存放的类型为 Hashtable$Entry,初始化大小为 11,临界值为(threshold)为 11 * 0.75 = 8,加载因子(loadfactor)也为0.75

在这里插入图片描述

在这里插入图片描述

简单的和 HashMap 比较一下:

  • HashMap 维护的是 table 数组,存放的数据类型是,HashMap$Node
  • Hashtable 维护的是 table 数组,存放的数据类型是 , Hashtable$Entry

需要注意的是,HashMap$Node 是 HashMap 的内部类,Hashtable$Entry 是 Hashtable 的内部类,table 数组是他们本类中独立定义的,不是同一个。

  1. 当 Hashtable 中的 table 数组里面的元素数量超过临界值(threshold)大小时,table 数组会进行扩容。

    table 数组的初始化大小为 11,临界值为 11 * 0.75 = 8,当添加的元素数量到达8时,如果继续添加元素,table 数组将会进行扩容。

    扩容机制为:int newCapacity = (oldCapacity << 1) + 1 :简单理解就是原来的数组容量 ✖ 2 + 1;

    比如说,第一次扩容 table 容量为 11,扩容之后的数组容量为 11 * 2 + 1 = 23,后面的以此类推即可。

(2)源码分析验证

  使用以下代码进行分析:

import java.util.Hashtable;

public class Hashtable_ {
    public static void main(String[] args) {
        Hashtable hashtable = new Hashtable();

        hashtable.put(null, 1);
        hashtable.put(2,2);
    }
}

  首先,进入Hashtable 的无参构造器:可以看到初始化table数组大小为11,加载因子为0.75

在这里插入图片描述

  接着,我们进入put() 方法中:

在这里插入图片描述

  观察 put() 方法的源码,可以看到如果 value 值为空,会抛出异常;同样的,如果 key = null,在我们计算 int hash = key.hashCode(); 时,由于 key = null,同样会抛出空指针异常。

扩容机制的验证:

  设计以下代码进行分析:

import java.util.Hashtable;

public class Hashtable_ {
    public static void main(String[] args) {
        Hashtable hashtable = new Hashtable();

        hashtable.put(1,1);
        hashtable.put(2,2);
        hashtable.put(3,3);
        hashtable.put(4,4);
        hashtable.put(5,5);
        hashtable.put(6,6);
        hashtable.put(7,7);
        hashtable.put(8,8);

        hashtable.put("棐","木");
    }
}

  当执行到,hashtable.put("棐","木") ,我们查看 debug 的控制台:

在这里插入图片描述

  可以看到此时,table 表中的元素已经到达了 8 个,如果继续添加,table 将会进行扩容,我们继续执行 hashtable.put("棐","木") 语句,进入 put() 方法:

在这里插入图片描述

  执行之后,我们需要进入 addEntry() 方法:

在这里插入图片描述

  可以看到,此时 if(count >= threshold) 为真,进入 if 语句,执行 rehash() 方法开始扩容,我们进入 rehash() 方法:

protected void rehash() {
    // 获取当前哈希表的容量
    int oldCapacity = table.length;
    // 获取当前哈希表的数组引用
    Entry<?,?>[] oldMap = table;

    // 计算新的容量,新容量为旧容量的两倍加一
    int newCapacity = (oldCapacity << 1) + 1;
    // 检查新容量是否超过最大数组大小
    if (newCapacity - MAX_ARRAY_SIZE > 0) {
        // 如果旧容量已经达到最大数组大小,不再扩容
        if (oldCapacity == MAX_ARRAY_SIZE)
            // 保持当前容量,直接返回
            return;
        // 否则,将新容量设置为最大数组大小
        newCapacity = MAX_ARRAY_SIZE;
    }
    // 创建新的哈希表数组
    Entry<?,?>[] newMap = new Entry<?,?>[newCapacity];

    // 增加修改计数
    modCount++;
    // 重新计算阈值,阈值为新容量乘以负载因子,但不超过最大数组大小加一
    threshold = (int)Math.min(newCapacity * loadFactor, MAX_ARRAY_SIZE + 1);
    // 将新的哈希表数组赋值给 table
    table = newMap;

    // 遍历旧哈希表的每个位置
    for (int i = oldCapacity ; i-- > 0 ;) {
        // 获取旧哈希表该位置的链表头节点
        for (Entry<K,V> old = (Entry<K,V>)oldMap[i] ; old != null ; ) {
            // 保存当前节点
            Entry<K,V> e = old;
            // 移动到下一个节点
            old = old.next;

            // 计算当前节点在新哈希表中的索引
            int index = (e.hash & 0x7FFFFFFF) % newCapacity;
            // 将当前节点插入到新哈希表的对应位置
            e.next = (Entry<K,V>)newMap[index];
            newMap[index] = e;
        }
    }
}

  执行完成之后,table 数组的容量就扩大为了 23。

在这里插入图片描述

(3)HashMap 和 Hashtable 的比较
版本线程安全(同步)效率允许 null 键 null 值
HashMap1.2不安全可以
Hashtable1.0安全较低不可以

13 Properties 类的基本介绍

  1. Properties 类继承自 Hashtable 类,并且实现了 Map 接口,也是使用一种键值对的形式来保存数据。

  2. Properties 的使用特点和 Hashtable 类似。

  3. Properties 还可以用于 从 xxx.properties 文件中,加载数据到 Properties 类对象,并进行读取和修改。

  4. 需要注意的是:在项目中,xxx.properties 文件通常作为配置文件,在 I/O 流中有所应用。

    有兴趣了解的读者,可以先参考下面这篇文章:

    Java 读写Properties配置文件 - 旭东的博客 - 博客园

public class Properties_ {
    public static void main(String[] args) {
		//Properties 继承 Hashtable
 		//通过 k-v 存放数据,key 和 value 不能为 null      
 		
		Properties properties = new Properties();
        //1.添加
        //键值都不允许为 null
 		//properties.put(null, "abc");//抛出 空指针异常
		//properties.put("abc", null); //抛出 空指针异常
		properties.put("john", 100);//k-v
 		properties.put("lucy", 100);
 		properties.put("lic", 100);
 		properties.put("lic", 88);//如果有相同的 key , value 被替换
		System.out.println("properties=" + properties);
        //2.通过key 获取对应值
		System.out.println(properties.get("lic"));//88
 		//3.删除
		properties.remove("lic");
 		System.out.println("properties=" + properties);
 		//4.修改,利用替换的原理
		properties.put("john", "约翰");
 		System.out.println("properties=" + properties);
    }
}

14 TreeSet 的解读与分析

在这里插入图片描述

  TreeSet 的最大特征就是可以进行排序,TreeSet 的底层实际上是TreeMap。TreeSet 内部使用一个 TreeMap 来存储元素,其中键是 TreeSet 的元素,值是一个固定的 PRESENT 对象。

  TreeSet 本质上还是一个 Set,不能添加重复的 key 值。

在这里插入图片描述

(1)使用无参构造器

  TreeSet 使用无参构造器 ,数据的存入顺序和取出顺序还是不一样的,即数据还是无序的。

import java.util.TreeSet;

public class TreeSet_ {
    public static void main(String[] args) {
        TreeSet treeSet = new TreeSet();

        treeSet.add("xiong");
        treeSet.add("fei");
        treeSet.add("mu");
        treeSet.add("csdn");

        System.out.println(treeSet);
    }
}

  输出结果为:

在这里插入图片描述

  TreeSet 的底层为 TreeMap ,我们进入它的构造器:

在这里插入图片描述

(2)使用 TreeSet(Comparator<E>) 构造器

  使用这个构造器,可以传入一个我们自定义的比较器,根据需求构建排序规则。

  使用以下代码进行分析:

import java.util.Comparator;
import java.util.TreeSet;

public class TreeSet_ {
    public static void main(String[] args) {
        //TreeSet treeSet = new TreeSet();
        TreeSet treeSet = new TreeSet(new Comparator() {
            @Override
            public int compare(Object o1, Object o2) {
                //使用compareTo 方法,让数据按字符串从小到大排序
                return ((String)o1).compareTo((String) o2);
            }
        });

        treeSet.add("xiong");
        treeSet.add("fei");
        treeSet.add("mu");
        treeSet.add("csdn");

        System.out.println(treeSet);
    }
}

  首先进入构造器TreeSet(Comparator<E>)方法中:

在这里插入图片描述

  我们继续进入 new TreeMap<>(comparator) 这个对象的创建中:我们将会进入 TreeMap 源码中:

在这里插入图片描述

  可以看到,我们传入的比较器被赋给了 TreeMap 的 comparator

  我们继续,开始使用 add() 方法添加元素,进入 add() 方法:

在这里插入图片描述

  继续进入put() 方法:put 方法是属于 TreeMap 的方法:因为在创建对象调用构造器进行初始化时,把创建的 new TreeMap<>(comparator) 对象赋给了 TreeSet 的 m,m.put() 时会通过动态绑定机制,调用 TreeMap 的 put 方法。

public V put(K key, V value) {
    // 获取根节点
    Entry<K,V> t = root;
    if (t == null) {
        // 如果根节点为空,进行类型检查(确保键不为null)
        compare(key, key); 

        // 创建新的根节点
        root = new Entry<>(key, value, null);
        // 更新树的大小
        size = 1;
        // 增加修改计数
        modCount++;
        // 返回null,表示插入成功
        return null;
    }
    int cmp;
    Entry<K,V> parent;
    // 分离比较器和可比较路径
    Comparator<? super K> cpr = comparator;
    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
                return t.setValue(value); // 如果键相等,替换值并返回旧值
        } while (t != null);
    }
    else {
        // 键为null时抛出NullPointerException
        if (key == null)
            throw new NullPointerException();
        @SuppressWarnings("unchecked")
        Comparable<? super K> k = (Comparable<? super K>) key;
        // 使用Comparable接口进行比较
        do {
            parent = t;
            cmp = k.compareTo(t.key);
            if (cmp < 0)
                t = t.left; // 如果键小于当前节点的键,移动到左子树
            else if (cmp > 0)
                t = t.right; // 如果键大于当前节点的键,移动到右子树
            else
                return t.setValue(value); // 如果键相等,替换值并返回旧值
        } while (t != null);
    }
    // 创建新节点
    Entry<K,V> e = new Entry<>(key, value, parent);
    if (cmp < 0)
        parent.left = e; // 插入到父节点的左子树
    else
        parent.right = e; // 插入到父节点的右子树
    // 插入后调整树的平衡
    fixAfterInsertion(e);
    // 更新树的大小
    size++;
    // 增加修改计数
    modCount++;
    // 返回null,表示插入成功
    return null;
}

执行 treeSet.add("xiong") 时的逻辑

  1. 初始化
    • Entry<K,V> t = root;:获取 TreeMap 的根节点。
    • if (t == null) { ... }:检查根节点是否为空。如果是第一次插入,根节点为空。
  2. 根节点为空
    • compare(key, key);:进行类型检查,确保键不为null。这里会调用 Comparatorcompare 方法,但两个参数都是 "xiong",所以不会抛出异常。
    • root = new Entry<>(key, value, null);:创建新的根节点,键为 "xiong",值为 PRESENT,父节点为 null
    • size = 1;:更新树的大小为1。
    • modCount++;:增加修改计数。
    • return null;:返回null,表示插入成功。

详细步骤

  1. 获取根节点
    • Entry<K,V> t = root;:获取当前树的根节点。初始时,根节点为空。
  2. 根节点为空
    • if (t == null) { ... }:根节点为空,说明这是第一次插入。
    • compare(key, key);:调用 Comparatorcompare 方法,确保键不为null。这里调用 ((String)o1).compareTo((String) o2),但两个参数都是 "xiong",所以返回0。
    • root = new Entry<>(key, value, null);:创建新的根节点,键为 "xiong",值为 PRESENT,父节点为 null
    • size = 1;:更新树的大小为1。
    • modCount++;:增加修改计数。
    • return null;:返回null,表示插入成功。

执行 treeSet.add("fei") 的代码逻辑

初始状态

TreeSet 已经通过 treeSet.add("xiong") 插入了一个元素,此时 TreeMap 的结构如下:

  • 根节点:"xiong",值为 PRESENT

执行 treeSet.add("fei")

  1. 获取根节点
    • Entry<K,V> t = root;:获取当前树的根节点。此时根节点是 "xiong"
  2. 根节点不为空
    • Comparator<? super K> cpr = comparator;:获取比较器。
    • if (cpr != null) { ... }:比较器不为空,使用比较器进行比较。
      • do { ... } while (t != null);:遍历树,找到插入位置。
      • parent = t;:保存当前节点的父节点,初始时为 "xiong"
      • cmp = cpr.compare(key, t.key);:使用比较器比较键 "fei""xiong"
        • ((String)"fei").compareTo((String)"xiong") 返回 -1,因为 "fei" 小于 "xiong"
      • if (cmp < 0) t = t.left;:因为 "fei" 小于 "xiong",移动到左子树。
      • while (t != null);:此时 tnull,退出循环。
  3. 插入新节点
    • Entry<K,V> e = new Entry<>(key, value, parent);:创建新节点,键为 "fei",值为 PRESENT,父节点为 "xiong"
    • if (cmp < 0) parent.left = e;:因为 "fei" 小于 "xiong",将新节点插入到父节点的左子树。
    • fixAfterInsertion(e);:插入后调整树的平衡。对于 TreeMap,这通常涉及红黑树的调整,但在这个简单的插入中,可能不需要调整。
    • size++;:更新树的大小为2。
    • modCount++;:增加修改计数。
    • return null;:返回null,表示插入成功。

最终状态

插入 "fei" 后,TreeMap 的结构如下:

  • 根节点:"xiong",值为 PRESENT
  • 左子节点:"fei",值为 PRESENT

需要注意的是:如果我们添加的元素通过我们自定义的比较器,比较之后是相等的,则该元素是不会被添加的。


15 TreeMap 的解读与分析

在这里插入图片描述

使用以下代码进行分析:

import java.util.Comparator;
import java.util.TreeMap;

public class TreeMap_ {
    public static void main(String[] args) {
        //使用默认的构造器,数据还是无序的
        //TreeMap treeMap = new TreeMap();

        TreeMap treeMap = new TreeMap(new Comparator() {
            @Override
            public int compare(Object o1, Object o2) {
                //使用compareTo 方法,让数据按字符串从小到大排序
                return ((String)o1).compareTo((String) o2);
            }
        });

        treeMap.put("xiong", "熊大");
        treeMap.put("fei", "棐木");
        treeMap.put("laoda", "牢大");

        System.out.println(treeMap);
    }
}

  这里需要注意的是,TreeMap 的数据是以键值对的形式存入的。但是,我们传入的比较器,比较的仍然是 key 的值。也就是说,按 key 的值构建排序。

  排序的方法和上面的 TreeSet 是一样的,都是通过 put 方法进行排序的,需要注意的是:如果我们添加的元素通过我们自定义的比较器,比较之后是相等的(比较的是 key 值),则该元素的 value 值会替换原来的 value 值。

treeMap.put("xiong", "熊大");
treeMap.put("fei", "棐木");
treeMap.put("xiong", "牢大");//牢大 会替换 熊大

16 在实际开发中如何选择合适的集合实现类?

  在项目开发中,我们应该怎么样选择合适的集合实现类呢?

  选择哪一种,我们首先需要明确业务的操作特点,然后根据这个特点去选择与之适配的集合实现类的特性。

  1. 先判断存储的类型(是一组对象还是一组键值对的形式)

    • 单列
    • 双列
  2. 一组对象(单列):Collection 接口

    • 允许重复:List

      • 增删操作多:LinkedList(底层维护了一个双向链表)
      • 改查操作多:ArrayList(底层维护了一个 Object 类型的可变数组)
    • 不允许重复:Set

      • 无序:HashSet(底层是 HashMap,维护了一个哈希表(数组+链表+红黑树))
      • 排序:TreeSet(内部使用一个 TreeMap 来存储元素,可以通过构造器传入自定义的比较器,构建排序条件)
      • 插入和取出顺序一致:LinkedHashSet,底层维护了一个数组+双向链表
  3. 一组键值对(双列):Map 接口

    • 键无序:HashMap(底层是:哈希表 jdk 7是数组+链表;jdk8 是数组+链表+红黑树)
    • 键排序:TreeMap(可以通过构造器传入自定义的比较器,构建排序条件)
    • 键插入和取出的顺序一致:LinkedHashMap
    • 读取文件:Properties

17 Collections 工具类(这里只介绍使用较多的)

17.1 基本介绍

注意:前面提到的是 Collection 接口。

  1. Collections 是一个操作 Set、List 和 Map 等集合的工具类。

  2. Collections 中提供了一系列静态的方法对集合元素进行排序、查询和修改等操作。

17.2 排序操作(均为 static 方法)

public class Collections_ {
    public static void main(String[] args) {
        
		//创建ArrayList 集合,用于测试.
 		List list = new ArrayList();
 		list.add("tom");
 		list.add("smith");
 		list.add("king");
 		list.add("milan");
 		list.add("tom");
    }
}
(1)reverse(List) :反转 List 中元素的顺序

在这里插入图片描述

Collections.reverse(list);
(2)shuffle(List) :对 List 集合元素进行随机排序

在这里插入图片描述

Collections.shuffle(list);
(3)sort() :排序
  • sort(List) :根据元素的自然顺序对指定的 List 集合元素进行升序排序

  • sort(List, Comparator)::根据指定的 Comparator 产生的顺序对 List 集合元素进行排序

在这里插入图片描述

Collection.sort(list);

在这里插入图片描述

//我们希望按照字符串的长度大小排序
Collections.sort(list, new Comparator() {
     @Override
     public int compare(Object o1, Object o2) {
         //可以加入校验代码
         return ((String) o2).length()- ((String) o1).length();
     }
 });
(4)swap(List, int, int) :将指定 List 集合中的 i 处元素和 j 处元素进行交换(索引从0开始)

在这里插入图片描述

Collections.swap(list, 0, 1);

17.3 查找、替换操作

(1)Object max(Collection) :根据元素的自然顺序,返回给定集合中的最大元素

在这里插入图片描述

Collection.max(list);
(2)Object max(Collection, Comparator) :根据 Comparator 指定的顺序返回给定集合中的最大元素

在这里插入图片描述

//比如,我们要返回长度最大的元素
Object maxObject = Collections.max(list, new Comparator() {
 	@Override
 	public int compare(Object o1, Object o2) {
 		return ((String)o1).length()- ((String)o2).length();
     }
 });
(3)Object min(Collection) :根据元素的自然顺序,返回给定集合中的最小元素

在这里插入图片描述

(4)Object min(Collection, Comparator) :根据 Comparator 指定的顺序返回给定集合中的最小元素

在这里插入图片描述

(5)int frequency(Collection, Object) :返回指定集合中指定元素的出现次数

在这里插入图片描述

Collection.frequency(list, "tom");
(6)void copy(List dest, List src) :将 src 中的内容复制到 dest 中

在这里插入图片描述

(7)boolean replaceAll(List list, Object oldVal, Object newVal) :使用新值替换 List 对象的所有旧值

在这里插入图片描述

(8)如果希望了解更多,可以查阅官方文档。点击以下链接即可直达!

Collections - Java17中文文档 - API参考文档 - 全栈行动派




总结:在这篇文章中,我们对集合做了一个初步的源码分析,通过源码探究了Collection、Map 接口实现类的底层实现方式。看完这篇文章,并亲自动手实操,相信我们都能对集合有一个更加深刻的认识。

技术之路道阻且长,望你我各自努力。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值