09_集合框架

总体架构图

本节内容架构要掌握,总体来看这就是架构图,其中实线是继承部分,虚线是实现部分

image-20201204163846541

为了节省时间和空间,API只是简单的过一遍,更重要的还是在于源码分析


Collection

  • 位于Java.util包下
  • Collection,单列集合,类似数组一般,不过集合的长度是可以动态扩展的

API

  • boolean add(E e);//将元素添加到集合中
    boolean addAll(Collection<? extends E> c);//将指定集合中的所有元素添加到集合中
    
  • boolean remove(Object o);//删除指定元素
    boolean removeAll(Collection<?> c);//删除所有指定集合中包含的元素
    default boolean removeIf(Predicate<? super E> filter);//删除所有满足指定正则的元素
    void clear();//清空集合内容
    
  • int size();//返回集合大小
    Iterator<E> iterator();//迭代器,用于遍历
    
  • 断言

    boolean isEmpty();//是否为空
    boolean contains(Object o);//是否包含指定内容
    boolean containsAll(Collection<?> c);//是否包含指定集合中的全部内容
    boolean retainAll(Collection<?> c);//集合保留与指定集合相同的内容(保留交集)
    

boolean equals(Object o);//是否和指定元素相等


- 转换

```java
Object[] toArray();//转为Object数组
<T> T[] toArray(T[] a);//转换为你输入数组类型的数组
Stream<E> stream();//转换为流
//一个新的Collection
Collection<Integer> list = new ArrayList();

//赋值10个
for (int i = 1; i <= 10; i++) {
    list.add(i);
}

//假如不为空
if (!list.isEmpty()) {
    //转换为Object数组
    Object[] objects = list.toArray();
    //输出Object数组:12345678910
    Arrays.stream(objects).forEach(System.out::print);

    //换行
    System.out.println();

    //转换为指定类型的数组
    Integer[] array = list.toArray(new Integer[0]);
    //输出:12345678910
    Arrays.stream(array).forEach(System.out::print);
}

//换行
System.out.println();

Collection<Integer> list2 = new ArrayList();

//根据指定的集合数据,添加到集合中
list2.addAll(list);
list2.add(11);

//转换为stream流,输出:1234567891011
list2.stream().forEach(System.out::print);

//删除
list.remove(9);

//保留交集
list2.retainAll(list);

System.out.println();

//1234567810
list2.stream().forEach(System.out::print);

//清空
list2.clear();

List

List介绍

  • List最大的特点就是有序,可重复
  • 有序:指的是进入和输出的顺序是确定的
  • 可重复:List中的元素可以有多个重复值

API

  • boolean add(E e);//将元素添加到集合中
    boolean addAll(Collection<? extends E> c);//将指定集合中的所有元素添加到集合中
    
    boolean addAll(int index, Collection<? extends E> c);//将指定集合从集合的指定位置插入进去
    void add(int index, E element);//根据指定的下标添加值到对应的位置中
    
  • boolean remove(Object o);//删除指定元素
    boolean removeAll(Collection<?> c);//删除所有指定集合中包含的元素
    default boolean removeIf(Predicate<? super E> filter);//删除所有满足指定正则的元素
    void clear();//清空集合内容
    
    E remove(int index);//根据指定的下标删除对应的内容
    
  • 
    void replaceAll(UnaryOperator<E> operator);//用函数式接口的返回值替代原来的值
    void sort(Comparator<? super E> c);//排序,使用Comparator进行排序
    E set(int index, E element);//根据指定的下标将值修改为指定的内容
    
  • int size();//返回集合大小
    Iterator<E> iterator();//迭代器,用于遍历
    
    E get(int index);//根据指定下标查询值
    int indexOf(Object o);//返回对象在集合中首次出现的位置
    int lastIndexOf(Object o);//返回对象在集合中最后一次出现的位置
    ListIterator<E> listIterator();//迭代器的升级,不仅可以从前向后遍历,也可以从后向前遍历,还可以自由增删元素
    
  • 断言

    boolean isEmpty();//是否为空
    boolean contains(Object o);//是否包含指定内容
    boolean containsAll(Collection<?> c);//是否包含指定集合中的全部内容
    boolean retainAll(Collection<?> c);//集合保留与指定集合相同的内容(保留交集)
    boolean equals(Object o);//是否和指定元素相等
    
  • 转换

    Object[] toArray();//转为Object数组
    <T> T[] toArray(T[] a);//转换为你输入数组类型的数组
    Stream<E> stream();//转换为流
    
    List<E> subList(int fromIndex, int toIndex);//将指定索引范围(不包含右边)转换为新的List,相当于截取一小部分出来
    
  • 不难看到,我其实是先将Collection中的方法和List中新增的方法使用回车进行了隔离
  • 我们其实不难看出来,这些新增加的方法都是一些对索引的操作,或者说是只能由索引带来的操作

Collection<Integer> collection = new ArrayList();

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


List<Integer> list = new ArrayList<>();

//给指定的位置添加内容
list.add(0, 0);
//从集合的指定位置开始插入指定集合的内容
list.addAll(0, collection);

//输出:123456789100
list.stream().forEach(System.out::print);

System.out.println();


//将所有数据替换为所有数据-1
list.replaceAll(integer -> integer - 1);

list.stream().forEach(System.out::print);

System.out.println();

//删除指定位置的元素
list.remove(10);

//转为List的迭代器,可以向前和向后遍历:0123456789
ListIterator<Integer> iterator = list.listIterator();
while (iterator.hasNext()) {
    System.out.print(iterator.next());
}

System.out.println();

List<Integer> subList = list.subList(0, 1);
//0
subList.forEach(System.out::print);

ArrayList

特点

  • ArrayList底层是使用数组来存储的数据结构,是数组列表
  • ArrayList只能装载引用类型,也就是说当我们使用基本类型的时候只能使用它的包装类
  • 因为基于数组,所以ArrayList查找快,增删慢
  • ArrayList是非线程安全的

深入理解

构造

  • 首先我们来看一下它的构造方法
private static final Object[] DEFAULTCAPACITY_EMPTY_ELEMENTDATA = {};
private static final Object[] EMPTY_ELEMENTDATA = {};
public ArrayList() {
    //无参构造方法,直接让方法为默认的对象数组,默认的对象集合是一个空的对象数组
    this.elementData = DEFAULTCAPACITY_EMPTY_ELEMENTDATA;
}
//指定内容的构造方法
public ArrayList(int initialCapacity) {
    //假如容量>0,那么让elementData缓冲数组为新的对象数组,新的对象数组的容量为指定的容量
    if (initialCapacity > 0) {
        this.elementData = new Object[initialCapacity];
    } else if (initialCapacity == 0) {
        //假如容量==0,那么就和无参构造方法一样
        this.elementData = EMPTY_ELEMENTDATA;
    } else {
        //假如容量<0,直接抛异常
        throw new IllegalArgumentException("Illegal Capacity: "+initialCapacity);
    }
}
//指定集合的构造方法
public ArrayList(Collection<? extends E> c) {
    //collection.toArray(),来自Collection集合,具体的作用是将集合转换为数组,这里的缓冲数组就成为了转换的数组
    elementData = c.toArray();
    //假如长度不为0
    if ((size = elementData.length) != 0) {
        //假如缓冲数组的类别和Object[]的类别不同,那么直接利用Arrays.copy方法复制过去
        if (elementData.getClass() != Object[].class)
            elementData = Arrays.copyOf(elementData, size, Object[].class);
    } else {
        //假如长度为0,那么还是和无参构造方法一样
        this.elementData = EMPTY_ELEMENTDATA;
    }
}

我们可以看到,无参构造,集合长度为0的构造,指定长度为0的构造其实在这个时候并没有指定集合的容量大小,只是创建了

  • 构造方法的BUG

我们看指定长度的构造方法,当我们指定了长度的构造方法之后,但是size没有改变,size可以说明,就是ArrayList的大小。

ArrayList arrayList = new ArrayList(10);

//0
System.out.println(arrayList.size());

添加

  • 接下来我们来看一下增加的方法
private void ensureCapacityInternal(int minCapacity) {
    ensureExplicitCapacity(calculateCapacity(elementData, minCapacity));
}

private static int calculateCapacity(Object[] elementData, int minCapacity) {
    //假如是空的,那么就返回 DEFAULT_CAPACITY和指定的数据的较大值,DEFAULT_CAPACITY为10
    if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA) {
        return Math.max(DEFAULT_CAPACITY, minCapacity);
    }
    return minCapacity;
}

private void ensureExplicitCapacity(int minCapacity) {
	/*
		这是一个比较重要的变量,作用是来记录对象的修改次数,当增删改的时候我们一定会看到这个对象
		在单线程的条件下,这个对象貌似没有什么用,但是在多线程的情况下可以检测这个变量抛出异常,确保数据安全性
    */
    modCount++;

	/*
		我们刚才已经知道,缓冲数组和我们的集合一样的
		那么在这里就是判断传递过来的数值如果比我们的数组长度大,那么就要进行扩容
	*/
    if (minCapacity - elementData.length > 0)
        grow(minCapacity);
}

calculateCapacity我们可以看到,初始值假如小于10的话,按照10来,假如比10大,那么就按照指定的值

//重点的扩容方法
private void grow(int minCapacity) {
    int oldCapacity = elementData.length;
    //位运算没忘记吧,右移一位就相当于除2,这里也就相当于新的数组容量大小为老的容量的1.5倍,也就是扩容1.5倍数
    int newCapacity = oldCapacity + (oldCapacity >> 1);
    //假如新的容量值还比指定的容量值要小,那么新的容量值就是指定的容量值,这个情况一般在自己指定大小的时候会用到
    if (newCapacity - minCapacity < 0)
        newCapacity = minCapacity;
    //经历了以上的方法,假如新的容量值竟然要比最大值还要大,将会导致数据溢出,那么直接使用解决策略
    if (newCapacity - MAX_ARRAY_SIZE > 0)
        newCapacity = hugeCapacity(minCapacity);
	//将旧的数组复制到新的数组中,旧的数组不要了,作为垃圾回收
    elementData = Arrays.copyOf(elementData, newCapacity);
}
//ArrayList的溢出解决策略
private static int hugeCapacity(int minCapacity) {
    //当值小于0,直接抛出异常
    if (minCapacity < 0)	throw new OutOfMemoryError();
    //假如数据溢出,直接为Integer的最大值,没有溢出就是为设定的最大值,设定的最大值为MAX_ARRAY_SIZE=Integer减8
    return (minCapacity > MAX_ARRAY_SIZE) ?	Integer.MAX_VALUE :	MAX_ARRAY_SIZE;
}
public boolean add(E e) {
    //看看是不是要扩容
    ensureCapacityInternal(size + 1);
    //数组的下一个值增加
    elementData[size++] = e;
    return true;
}
public void add(int index, E element) {
    //超范围直接抛异常
    rangeCheckForAdd(index);
	//看看是不是要扩容
    ensureCapacityInternal(size + 1);
    //System.arraycopy(上一个数组,上个数组的起始位置,目标数组,目标数组的起始位置,复制的长度);
    System.arraycopy(elementData, index, elementData, index + 1,size - index);
    //目标位置
    elementData[index] = element;
    //size,ArrayList的大小
    size++;
}

这就是底层,一般来讲数组的复制最终都会跑到这个System.arraycopy()

public boolean addAll(Collection<? extends E> c) {
    //转数组
    Object[] a = c.toArray();
    //得到数组长度
    int numNew = a.length;
    //判定,size(ArrayList)默认的话是0,但是如果之前加过会改变
    ensureCapacityInternal(size + numNew);
    //再一次拷贝
    System.arraycopy(a, 0, elementData, size, numNew);
    //size更改
    size += numNew;
    //只要加入的集合长度不为0,就是真的
    return numNew != 0;
}
//指定位置,添加所有
public boolean addAll(int index, Collection<? extends E> c) {
    //超范围直接抛异常
    rangeCheckForAdd(index);
    //转数组
    Object[] a = c.toArray();
    //获取长度
    int numNew = a.length;
    //是否要扩容
    ensureCapacityInternal(size + numNew);
	//集合长度-目标位置,获得的结果就是长度
    int numMoved = size - index;
	//假如长度>0
    if (numMoved > 0)
        //最终要到数组的拷贝,这里是先把原数组进行拷贝,中间现在空出了一部分
        System.arraycopy(elementData, index, elementData, index + numNew,numMoved);
	//把空出来的部分补上
    System.arraycopy(a, 0, elementData, index, numNew);
    //ArrayList的长度更改
    size += numNew;
    //返回结果
    return numNew != 0;
}

删除

学习了添加,其实删除也是一样的,底层都是调用了数组的拷贝方法

其他

如果我要使用线程安全的集合应该怎么办

两种方法,一个是使用Vector,一个是使用Collections.synchronizedList把集合包装成一个线程安全的数组容器。

这两种方法都是将所有的方法全都加上一层synchronized

为啥默认起始容量为10

没有什么必须,可能就是当时程序员头脑一排得出来的

删除

删除的时候并不会删除容量,如果要移除多余的容量就要使用方法trimToSize

public void trimToSize() {
	//修改次数增加
    modCount++;
    //假如集合的容量比预计的小,那么缩小(重新拷贝)
    if (size < elementData.length) {
        elementData = (size == 0)
            ? EMPTY_ELEMENTDATA
            : Arrays.copyOf(elementData, size);
    }
}

API使用

属于List的方法

  • 添加
方法 说明
boolean add(E) 添加
void add(int,E) 指定位置添加
boolean addAll(Collection<? extends E>) 添加所有指定集合中的元素
boolean addAll(int,Collection<? extends E>) 指定位置添加所有集合中的元素
  • 查询
方法 说明
E get(int) 获得
int indexOf(Object) 查询指定对象在集合中的位置
Iterator<E> iterator() 迭代器
ListIterator<E> listIterator() 获得列表迭代器
int lastIndexOf(Object) 获得指定元素在集合中最后出现的位置
int size() 获取集合的长度

列表迭代器和迭代器不同的一点是:列表迭代器可以从后向前遍历,而迭代器只能从前向后遍历

  • 删除
方法 说明
void clear() 清空
E remove() 删除最后一个元素
boolean remove(Object o) 移除指定的元素
boolean removeAll(Collection<?> c) 移除和指定集合产生交集的元素
boolean retainAll(Collection<?> c) 只保留和指定集合产生交集的元素
  • 修改
方法 说明
E set(int index, E element) 直接设置指定位置的值
  • 断言
方法 说明
boolean contains(Object) 是否包含
  • 排序
方法 说明
sort(Comparator<? super E> c) 指定排序规则排序
  • 转换
方法 说明
List<E> subList(int fromIndex, int toIndex) 截取指定范围的元素形成一个新的List,不包括右边的范围
Object[] toArray() 转换为Object数组,底层是System.arraycopy
T[] toArray(T[] a) 转换为指定类型的数组

属于Object的方法

  • 转换
方法 说明
Object clone() 浅克隆

属于AbstractList的方法

  • 删除
方法 说明
void removeRange(int fromIndex, int toIndex) 指定范围删除

属于Iterable的方法

  • 查询
方法 说明
void forEach(Consumer<? super E> action) 遍历

属于Collection的方法

  • 删除
方法 说明
boolean removeIf(Predicate<? super E> filter) 根据正则删除

Vector

深入理解

线程安全

Vector基本和ArrayList差不多,基本就是复制版的ArrayList,但是它的方法都是线程安全的

比如给你们看一个添加操作:

public synchronized void insertElementAt(E obj, int index) {
    modCount++;
    if (index > elementCount) {
        throw new ArrayIndexOutOfBoundsException(index
                                                 + " > " + elementCount);
    }
    ensureCapacityHelper(elementCount + 1);
    System.arraycopy(elementData, index, elementData, index + 1, elementCount - index);
    elementData[index] = obj;
    elementCount++;
}

扩容

扩容的话和ArrayList还是有些区别的

private void grow(int minCapacity) {
    int oldCapacity = elementData.length;
    //capacityIncrement:扩容增量,有一个构造方法可以指定,假如扩容增量>0就用扩容增量,没有就扩容一倍
    int newCapacity = oldCapacity + ((capacityIncrement > 0) ? capacityIncrement : oldCapacity);
    if (newCapacity - minCapacity < 0)
        newCapacity = minCapacity;
    if (newCapacity - MAX_ARRAY_SIZE > 0)
        newCapacity = hugeCapacity(minCapacity);
    elementData = Arrays.copyOf(elementData, newCapacity);
}

扩容增量默认1倍和同步方法的出现导致Vector性能比不上ArrayList,但是在多线程方面是比ArrayList安全的

Stack

class Stack<E> extends Vector<E>

首先看这个源码,Stack继承Vector,说明Vector有的Stack也有

然后看方法

image-20200814154244771

标准的栈结构,入栈,出栈


LinkedList

链表

在讲解LinkedList之前,需要和各位先讲解一下链表的内容

数组

我们先看数组,我们知道,数组我们之前用过,它在内存中需要的是一块连续的存储空间。

因为这个特点,使得数组出现了索引(下标)这种东西,使得它在进行查找的时候特别高效。

那么假如数组的容量超出了还要增加,就要重新创建一个新的数组用来增加

数组的删除有时候不仅仅是单纯的删除,基本类型不可以为null,那么就必须重新创建一个新的数组进行删除

如果是集合必须为引用类型的话,删除没有这么多讲究,但是如果调用trimToSize也是要进行数组的重新部署

image-20200815153551163

链表

作为数据结构之一的链表,有着明确的分类

假如按照方向分类,分为单向链表和双向链表

假如按照是否循环分类,分为普通链表和循环链表

单向链表 双向链表
普通链表 单向链表 双向链表
循环链表 单向循环链表 双向循环链表

下面我们就依次来介绍一下

单向链表

作为最为普通的链表,用来介绍一下概念再好不过了

最普通的链表的一块节点的结构是这样的:

image-20200815154153206

这个节点由两部分组成:数据区,地址区。

其中数据区域负责存放数据,而地址区负责存放下一块节点在内存中的地址

因为链表的这种结构,导致和链表和数组不同的特性:

  • 它只需要一块地址空间,是否连续是无所谓的。这保证了空间更高效的利用率。
  • 因为存放着一块地址空间,所以每一个节点的容量要比数组的每一个节点大一些。

当然了,凡事有利有弊,解决了这个问题,我们就会出现新的问题,我们只需要在不同情况下确保最优选择即可

那么链表的具体结构图是这样的:

image-20200815155801900

image-20200815155951354

为什么有两张图呢,因为这是要分情况的

首先解释一下头结点和首元结点(首结点)和头指针

头指针:指向第一个结点

首元结点:第一个存有有效值的结点

头结点:头结点可以设立可以不设立,设立的时候地址就指向结点1,没有结点1就指向NULL

为什么要设立头结点

  1. 有了头结点之后,插入和删除操作就和其他的元素一致了
  2. 一般来说,头结点的名字就是这条链表的名字

不论那张图,都是前一个节点的地址指向后一个节点,最后一个结点指向NULL

双向链表

为了表述方便,我们不使用头结点的结构,并且不再画出指针

它的结构是这样的:

image-20200815162046555

除了刚才单向链表的特性,双向链表还有指向上一个结点地址的特点

单向循环链表

image-20200815162757994

简单来说,就是构成了一个圆环,让最后一个指针不再指向NULL而是指向第一个结点

双向循环链表

image-20200815163059315

我想,这个结构应该也很容易理解,第一个结点前一个是最后一个结点,最后一个结点的下一个是第一个结点

深入理解

首先看一下字段

image-20200815165026021

没错,就这三个

第一个很容易理解,长度

第二个和第三个我们看注释:一个是始终指向头结点,一个始终指向尾结点

Node

我们接下来看看Node

//Node,静态内部类,其中有几个属性
private static class Node<E> {
    //值
    E item;
    //指向下一个节点
    Node<E> next;
    //指向上一个结点
    Node<E> prev;

    //构造器:上一个结点,值,下一个节点
    Node(Node<E> prev, E element, Node<E> next) {
        this.item = element;
        this.next = next;
        this.prev = prev;
    }
}

从这个结点中我们就可以看到,Node,双向链表

为什么要先讲这个呢,因为这个是LinkedList的基础

first和last

    /**
     * Pointer to first node.
     * Invariant: (first == null && last == null) ||
     *            (first.prev == null && first.item != null)
     */
    transient Node<E> first;


    /**
     * Pointer to last node.
     * Invariant: (first == null && last == null) ||
     *            (last.next == null && last.item != null)
     */
    transient Node<E> last;

看源码非常重要的一点就是看注释

如果单看代码你可能会对这两个代码一头雾水,但是如果看注释你就非常清除

first永远指向第一个节点,last永远指向最后一个节点

构造方法

public LinkedList() {
}
public LinkedList(Collection<? extends E> c) {
    this();
    addAll(c);
}
public boolean addAll(Collection<? extends E> c) {
    return addAll(size, c);
}

有参无参的构造方法是差不多的,有参的是先调用无参的,然后再调用addAll方法

但是我们看到addAll里面又调用了一个新的addAll,老套娃了

检查异常

private boolean isElementIndex(int index) {
    return index >= 0 && index < size;
}

这个主要是用来检查异常的,看到这里没懂不要紧,往后看

获取

private void checkElementIndex(int index) {
    if (!isElementIndex(index))
        throw new IndexOutOfBoundsException(outOfBoundsMsg(index));
}

看到这是不是懂了,用来检查异常的,用到了刚才的检查异常方法

Node<E> node(int index) {

	//假如传入的值小于size/2,也就是说假如传入的值小于链表的一半
    if (index < (size >> 1)) {
        //新建一个节点,让这个节点为开始节点
        Node<E> x = first;
        //进行循环,从头开始遍历,一直到指定的位置
        for (int i = 0; i < index; i++)
            //直到遍历到最后
            x = x.next;
        return x;
    } else {
        //假如传入的值比size/2大,也就是说传入的值大于链表的一半,从尾部开始遍历
        Node<E> x = last;
        //从尾部开始向前遍历,一直到指定的值
        for (int i = size - 1; i > index; i--)
            x = x.prev;
        return x;
    }
}

这个方法注释写的很清楚:返回指定元素索引处的(非空)节点

那么我们之前在讲数据结构的时候曾经讲过,链表在查找数据的时候是非常慢的,必须从一端开始查

可能之前在看图的时候没有感觉,但是是不是一看到代码立刻就有感觉了

public E get(int index) {
    checkElementIndex(index);
    return node(index).item;
}

前面的都明白了,这个代码就没什么好讲的了

添加

private void checkPositionIndex(int index) {
    if (!isPositionIndex(index))
        throw new IndexOutOfBoundsException(outOfBoundsMsg(index));
}

检查异常用的,查看传过来的数值是否>=0并且<=size(链表长度)否则直接抛出异常

  • 向后添加
public boolean add(E e) {
    linkLast(e);
    return true;
}
public void addLast(E e) {
    linkLast(e);
}

上面这两个是add和addLast,其实我们可以看到最终调用linkLast,向链表最后添加数据

void linkLast(E e) {
    //新建一个节点,让这个节点地址指向最后的节点
    final Node<E> l = last;
    //新建一个新的节点,这个节点的前一个节点指向刚才的最后的节点,值为e,后一个地址指向null
    final Node<E> newNode = new Node<>(l, e, null);
    //last节点永远指向最后的节点
    last = newNode;
    //假如新的节点为null,这也是只有一种情况会出现:刚刚创建节点的时候
    if (l == null)
        //让first节点为新的节点
        first = newNode;
    else
        //在一般情况下,也就是插入的情况下,刚才的最后的节点的下一个地址指向新的最后的节点
        l.next = newNode;
    //链表长度+1
    size++;
    //修改次数+1
    modCount++;
}

需要修改次数,因为这样可以保证在并发情况下数据安全

  • 向前添加
public void addFirst(E e) {
    linkFirst(e);
}
private void linkFirst(E e) {
    final Node<E> f = first;
    final Node<E> newNode = new Node<>(null, e, f);
    first = newNode;
    if (f == null)
        last = newNode;
    else
        f.prev = newNode;
    size++;
    modCount++;
}

向前添加,套路基本和向后添加差不多,不BB了

  • 添加所有
public boolean addAll(int index, Collection<? extends E> c) {
	//上面的检查异常用的方法,超过了范围直接抛出越界
    checkPositionIndex(index);
	
    //首先将集合转成数组
    Object[] a = c.toArray();
    //获取集合的长度
    int numNew = a.length;
    //假如集合没有长度,也就是没有集合内容,直接返回false
    if (numNew == 0)
        return false;
    
    //定义两个临时节点
    Node<E> pred, succ;
    
    /*
    	假如指定的索引位置等于链表的长度,也就是直接向链表最后添加
    	注意了,这个非常秒
    	因为 链表长度 = 最终索引+1,而现在当链表长度=索引位置
		而只有一种情况,那就是向链表的最后添加内容,当然了,在初始化添加的时候也是一样,都是向链表最后添加内容
	*/
    if (index == size) {
        succ = null;
        //pred = last,在这里想一下之前我画的链表结构,现在就是让临时的这个节点赋值为最后一个节点
        pred = last;
    } else {
        //假如指定的索引长度不等于链表的长度,也就是在中间插入或者在开始插入,反正不是在最后插入
        //遍历链表,获取到对应位置的节点值
        succ = node(index);
        //给这个节点赋值为对应位置节点的前一个节点
        pred = succ.prev;
    }
	
    //接下来对集合转变为的数组进行遍历,然后进行依次的添加操作
    for (Object o : a) {
        //这个注解是忽略警告的,后面学到注解再来讲
        @SuppressWarnings("unchecked") 
        //这句简单,无非是弄一个临时变量然后赋值
        E e = (E) o;
        //定义一个新的节点,这个节点的前一个节点是我们指定的节点的前一个节点,值为刚才遍历出来的值,后一个节点为null
        Node<E> newNode = new Node<>(pred, e, null);
        /*
	        假如前一个节点为null,就让first节点(始终指向开始节点的节点)初始化
	        这行代码只有一种情况会执行,就是初始化LinkedList的时候
        */
        if (pred == null)
            //初始化first节点
            first = newNode;
        else
            /*
            	其他,也是一般情况,让我们刚刚创建的节点接上之前的节点
            	注意,我们是在for循环中,这句和pred = newNode的操作就是一直创建新的节点
            	然后我们所在的节点和新创建的节点相连接
			*/
            pred.next = newNode;
        //临时节点为我们刚才创建的节点
        pred = newNode;
    }
	//也就是index=size的情况(最后插入),那么假如succ为null,就说明pred是最后一个节点
    if (succ == null) {
        //last始终指向最后一个节点
        last = pred;
    } else {
        //假如是在中间插入,就让pred的下一个指针指向succ
        pred.next = succ;
        //然后succ的上一个指针指向pred
        succ.prev = pred;
    }
	//链表长度增加
    size += numNew;
    //链表的修改次数增加
    modCount++;
    return true;
}

这就是上面的那个老套娃的addAll,可以看出来,指定好插入的位置然后将元素全部添加,后端的元素直接往后排

还记得我们说过的链表结构么?这就是了

当然,我带领你们看的基本都是常用的,其他的源码你们自己去看,套路很清楚,只要思路清晰就没有问题

API使用

来自双端队列的操作

方法 说明
addFirst(E e) 向头添加
addLast(E e) 向尾添加
方法 说明
E removeFirst() 检索并删除第一个元素
E removeLast() 检索并删除最后一个元素
方法 说明
E getFirst() 获取链表头元素
E getLast() 获取链表尾元素
E peek() 检索不删除第一个元素

关于双端队列的其他方法确实也有,但是我并不想多bb,因为它们调用的要么向前添加,要么向后添加

来自List的操作

方法 说明
add(E e) 向尾添加
boolean addAll(Collection<? extends E> c) 添加指定集合中的所有数据
boolean addAll(int index, Collection<? extends E> c) 向链表的指定位置添加指定集合的所有数据,时间复杂度高
add(int index, E element) 向指定位置添加,时间复杂度高
int indexOf(Object o) 查询元素在链表中的位置,时间复杂度高
int lastIndexOf(Object o) 查询元素在链表中最后出现的位置,时间复杂度高
方法 说明
boolean remove(Object o) 删除指定元素,要遍历,时间复杂度高
void clear() 清空所有
方法 说明
E set(int index, E element) 设置指定位置的元素,时间复杂度高
方法 说明
int size() 获取链表长度
E get(int index) 获取指定位置的元素,时间复杂度高
  • 断言
方法 说明
boolean contains(Object o) 链表中是否包含指定元素
  • 转换
方法 说明
Object[] toArray() 转为数组
T[] toArray(T[] a) 转为指定类型的数组

来自Object的方法

  • 转换
方法 说明
Object clone() 浅克隆

CopyOnWriteArrayList

在这里请允许我卖个关子,因为这个东西要到JUC里面去讲

Map

Map介绍

前言

在这里我们可以看到顺序,没有先讲Set,而是直接讲了Map

这个顺序是有理由的,往后看就可以了

Map结构

如果说Collection家族的数据结构还可以让你让你想象一下,那么Map的结构肯定也可以让你脑补一下

简单来讲,Map家族的结构是key-value形式的,这个意思是说一个键对应一个值

这个和Collection不同,Collection是一种值排成了一串,而Map是一一对应。

对于Map,或者你也可以理解为身份证号和名字的对应关系

key是不可以重复的,但是value是可以重复的。这就和身份证号和名字一样

key只能映射到一个值,就像身份证号一样,一次只能映射到一个人身上。

总结起来就是Map的特点

  1. Map是键值对对象,一个键对应一个值
  2. 键不可以重复,但是值可以重复
  3. Map的数据结构与键有关,和值无关

hash,hashcode

哈希,也叫散列表,其实这个哈希在之前的博客中已经讲过

image-20200819203146895

image-20200819203158790


JDK7的HashMap

简单介绍

首先有一个大体的印象,带着这个印象去看源码会比较好理解:

1、JDK7中的HashMap的结构是使用数组+链表进行实现的

2、当我们使用put操作向HashMap中添加数据的时候,我们添加的是key-value的形式,但其实会将这个key-value封装为一个Entry对象,所以数组中存储的其实是一个引用地址,而不是真正的值

3、HashMap的put操作根据key来取,首先根据key取hashcode,得到hashcode的值,然后将值进行缩减,得到一个小的值,算出要存放的数组的下标,如果数组下标处有元素了,那就使用头插法形成一个链表

而且使用头插法的时候,插入的效率会比较快

4、get操作和put方式有些类似,首先根据key获取hashcode(输入值固定,hashcode固定),然后进行缩减,然后就获得了数组的下标,如果有链表,就进行遍历


当我们回顾ArrayList的添加操作的话,我们可以知道直接让数组的下一个位置增加了,也就是说不论扩容,单纯的增加元素是没什么技术含量的,但是这样的插入方式无疑是最快的。并且ArrayList的取值也不慢,因为直接通过下标来取值。

但是HashMap不可以这样增加数据,我们想象一下,假如HashMap像ArrayList一样,增加数据只是简单地放到下一位,这样插入的效率确实会高很多,但是查询的效率会非常低,因为HashMap的取值是使用key来取而不是下标。


深入理解

  • 首先看一下HashMap中比较重要的的属性
//默认数组的初始化容量,16
static final int DEFAULT_INITIAL_CAPACITY = 1 << 4;
//HashMap的存放最大容量
static final int MAXIMUM_CAPACITY = 1 << 30;
//默认的负载因子
static final float DEFAULT_LOAD_FACTOR = 0.75f;
//一个空的Entry数组
static final Entry<?,?>[] EMPTY_TABLE = {};
//表示HashMap中的数组,一开始就是一个空数组,是Entry类型
transient Entry<K,V>[] table = (Entry<K,V>[]) EMPTY_TABLE;
//HashMap中所有元素的多少
transient int size;
//阈值,根据加载因子算出
int threshold;
//负载因子,这个是到时候用到的负载因子
final float loadFactor;
//HashMap的改变次数,多线程下可以保证安全性
transient int modCount;
//哈希种子
transient int hashSeed = 0;
  • 构造方法
public HashMap() {
    //传递的参数为:初始化容量大小16,默认负载因子0.75
    this(DEFAULT_INITIAL_CAPACITY, DEFAULT_LOAD_FACTOR);
}
public HashMap(int initialCapacity) {
    //调用下面的构造方法,传递的参数为:初始化容量大小,默认负载因子0.75
    this(initialCapacity, DEFAULT_LOAD_FACTOR);
}
/**
 * 不论哪个HashMap,最终都要调用这个构造方法
 *
 * @param initialCapacity 初始化的容量大小
 * @param loadFactor      负载因子
 */
public HashMap(int initialCapacity, float loadFactor) {
    //初始化容量<0,直接抛异常
    if (initialCapacity < 0)
        throw new IllegalArgumentException("Illegal initial capacity: " +
                initialCapacity);
    //初始化容量大与最大容量,还是最大容量
    if (initialCapacity > MAXIMUM_CAPACITY)
        initialCapacity = MAXIMUM_CAPACITY;
    //负载因子小于0或者负载因子根部不是个数字,也抛异常
    if (loadFactor <= 0 || Float.isNaN(loadFactor))
        throw new IllegalArgumentException("Illegal load factor: " +
                loadFactor);
    //负载因子初始化
    this.loadFactor = loadFactor;
    //阈值初始化为容量大小
    threshold = initialCapacity;
    //init在HashMap中是空的,但是在LinkedHashMap中是有用的
    init();
}
  • put方法
/**
 * 对HashMap中存值
 *
 * @param key   键
 * @param value 值
 * @return null
 */
public V put(K key, V value) {
    //假如HashMap中的数组是一个空的数组,说明HashMap还是空的
    if (table == EMPTY_TABLE) {
        //初始化,传递了阈值进去
        inflateTable(threshold);
    }
    //假如key是null,没有抛异常,这说明HashMap中,key是可以为null的
    if (key == null)
        return putForNullKey(value);
    //返回一个hash值
    int hash = hash(key);
    /*
        拿hash值和数组的下标进行与运算,得到要插入进去的数组下标
        这里需要说明一下:当length是一个2的幂次方数时,hash & (length-1)其实就是 hash%length
        所以数组的长度必须为2的幂次方
        顺便说一下,indexFor里面其实就是返回hash & (length-1),下面就不贴函数了
     */
    int i = indexFor(hash, table.length);
    /*
        循环,从table[i]开始,也就是从新元素要插入数组的索引位置开始
        只要要插入数组的索引位置不为空,也就是说只要有这个地方有元素就开始死循环
        只要循环一次,链表节点+1
        总之,这个for循环就是遍历数组对应位置中的链表用的
     */
    for (HashMap.Entry<K, V> e = table[i]; e != null; e = e.next) {
        Object k;
        /*
            元素的输入相同,hash值就会相同,但是hash值相同,输入不一定相同
            但是e.hash==hash,e.key==key,key.equals(k)
            这三者加起来就是明明白白地确认输入是相同的
            也就是确认这个节点里key是不是一样
         */
        if (e.hash == hash && ((k = e.key) == key || key.equals(k))) {
            //新的值替换掉老的值
            V oldValue = e.value;
            e.value = value;
            //这是个空的,LinkedHashMao中用
            e.recordAccess(this);
            //返回老的值
            return oldValue;
        }
        //也就是说这个for循环其实就干了一件事:替换掉老的值并返回
    }

    //HashMap的修改数+1,这里需要注意,假如在上面替换掉了老的值,在上面就已经将数据返回了,这里的修改数是不增加的
    modCount++;
    //那么我们说,假如这个值以前没有增加过,那么就在这里增加一个新的元素
    addEntry(hash, key, value, i);
    //新的元素,返回的肯定就是null了
    return null;
}

总结:put方法做了这么几件事情:

1、判断key是否为null,如果为null那么就将这个值放到固定位置上

2、假如不为null,那么使用key获取到hash值并且使用与操作将值缩减为数组大小范围内的值

3、使用for循环来遍历数组下标中的链表,如果有对应的key,那么新的值覆盖老的值,然后返回老的值。假如没有对应的key,那么modCount+1,并且在链表上插入一个新的Entry


/**
 * 将传过来的key进行一个hash运算
 *
 * @param k HashMap中,传过来的key
 * @return 进行完hash运算后的结果
 */
final int hash(Object k) {
    //我们的hashSeed,哈希种子默认为0
    int h = hashSeed;
    if (0 != h && k instanceof String) {
        return sun.misc.Hashing.stringHash32((String) k);
    }

    /*
        这里使用哈希种子和键的哈希值进行一个异或运算
        我们知道,异或运算是相同为0,不同为1
        所以在这里进行异或运算的时候h也会进行变化
     */
    h ^= k.hashCode();

    //然后又进行向右位移,然后进行异或,得到后来的结果
    h ^= (h >>> 20) ^ (h >>> 12);
    //最后返回的时候继续移动,然后进行异或运算
    return h ^ (h >>> 7) ^ (h >>> 4);
}

所以,这里的结果其实不是我们直接调用了hashcode()返回的结果,而是进行运算得到的结果


/**
 * HashMap初始化
 *
 * @param toSize 传递了阈值进来
 */
private void inflateTable(int toSize) {
    //求2的幂次方,数组的容量必须是2的幂次方,这个原因在前面讲了
    int capacity = roundUpToPowerOf2(toSize);
    /*
    	阈值为
    		1、容量*负载因子
    		2、HashMap存放的最大容量+1。
		这两个取一个小值,当然一般情况是容量*负载因子
	*/
    threshold = (int) Math.min(capacity * loadFactor, MAXIMUM_CAPACITY + 1);
    //对HashMap的数组进行了初始化
    table = new HashMap.Entry[capacity];
    initHashSeedAsNeeded(capacity);
}
/**
 * 寻找一个最近的2的幂次方数,将其转换
 *
 * @param number 传进来的数字
 * @return 2的幂次方数
 */
private static int roundUpToPowerOf2(int number) {
    /*
        1               0000 0001
        2               0000 0010
        4               0000 0100
        8               0000 1000
        16              0001 0000
        ....
        所以规律是:只有bit位置上一个为1的才是2的幂次方数

        那么在这里,首先判断是不是大于最大容量
            - 大于最大容量,那么就返回最大容量
            - 没有大于最大容量,判断传进来的数字是否>1
                - 不大于1,返回1
                - 大于1,那么进行Integer.highestOneBit((number - 1) << 1)


        (number - 1) << 1,这个很有讲究
        左移一位,再去调用highestOneBit,因为这个方法的作用是返回小于等于给定数字的最接近的2的幂次方数
        而-1就更好了,因为我们以16为例,16-1=15,15进行翻倍是30,然后去调用highestOneBit,得到的结果还是16

        假如是highestOneBit(number<<1),16翻倍是32,32然后去调用highestOneBit,得到的结果是32,整整大了一倍

     */
    return number >= MAXIMUM_CAPACITY
            ? MAXIMUM_CAPACITY
            : (number > 1) ? Integer.highestOneBit((number - 1) << 1) : 1;
}
/**
 * 传进来的数字转换为小于等于我们传进来的数的2的幂次方数
 *
 * @param i 数字
 * @return 小于i的2的幂次方数
 */
public static int highestOneBit(int i) {
    /*
        比方说我们传了个17进来:0001 0001
            0001 0001
        >>1 0000 1000
        |   0001 1001
        >>2 0000 1100
        |   0001 1101
        >>4 0000 0111
        |   0001 1111
        >>8 0000 0000
        |   0001 1111
        ....
        |   0001 1111

        在最后,return的时候,再次右移一位,也就是说
        i - (i >>> i)
        0001 1111 - 0000 1111 = 0001 0000 = 16

        从上面的代码我们可以得到一个规律,我们所在的最高位有一个1,然后我们右移,让我们数字的最高位后面的所有数字都改成1
        然后最后一减,最后只剩下了最高位的那个1,这就是2的幂次方,只不过是小于等于我们传进来的数
     */
    i |= (i >> 1);
    i |= (i >> 2);
    i |= (i >> 4);
    i |= (i >> 8);
    i |= (i >> 16);
    return i - (i >>> 1);
}

因为int类型是4字节,32个bit位,1+2+4+8+16=32,这样就可以覆盖全部的int


/**
 * 假如key是null,同样也能push进去
 * @param value 值
 * @return
 */
private V putForNullKey(V value) {
    /*
        hashmap要push一个键为null的Entry,直接就放在了数组第一个位置上
        这里对链表进行遍历,替换掉旧数据并返回   
     */
    for (HashMap.Entry<K,V> e = table[0]; e != null; e = e.next) {
        if (e.key == null) {
            V oldValue = e.value;
            e.value = value;
            e.recordAccess(this);
            return oldValue;
        }
    }
    //修改次数+1
    modCount++;
    //假如没有null的键,直接就添加一个新的元素
    addEntry(0, null, value, 0);
    return null;
}

这里就是具体的,假如传入的key-value中,key为null的解决方式

/**
 * 增加元素
 *
 * @param hash        hash值
 * @param key         键
 * @param value       值
 * @param bucketIndex 要插入的数组下标位置
 */
void addEntry(int hash, K key, V value, int bucketIndex) {
    //判断HashMap的总元素是不是大于阈值,以及这个数组的位置上不为空的时候,重新进行扩容
    if ((size >= threshold) && (null != table[bucketIndex])) {
        //进行扩容,我们看到扩容的时候也是扩容为了2*table.length,扩容二倍的原因我们之前已经讲过了,数组容量必须为2的幂次方
        resize(2 * table.length);
        //重新hash,如果这个键为null,那么就是0
        hash = (null != key) ? hash(key) : 0;
        //数组索引重新进行运算
        bucketIndex = indexFor(hash, table.length);
    }

    //进行元素的添加
    createEntry(hash, key, value, bucketIndex);
}

这里需要注意一点,我们之前都以为只要HashMap的数值长度高于阈值就要扩容,其实不是,其实还有一个条件,就是当前数组的位置不为空的时候才会进行扩容操作

而且我们要注意,size的意思是hashmap中存入了多少数据

    /**
     * 扩容操作
     *
     * @param newCapacity 新的数组容量
     */
    void resize(int newCapacity) {
        //获得老的数组,和老的数组的长度
        HashMap.Entry[] oldTable = table;
        int oldCapacity = oldTable.length;
        
        //假如老的数组最大长度为HashMap所允许的最大长度,不进行扩容
        if (oldCapacity == MAXIMUM_CAPACITY) {
            threshold = Integer.MAX_VALUE;
            return;
        }

        //扩容时,定义一个新的数组,新数组的大小为原来数组的二倍
        HashMap.Entry[] newTable = new HashMap.Entry[newCapacity];
        
        //进行数据的转移,将原来数组的数据转移到新数组中
        transfer(newTable, initHashSeedAsNeeded(newCapacity));
        table = newTable;
        //重新对阈值进行计算
        threshold = (int) Math.min(newCapacity * loadFactor, MAXIMUM_CAPACITY + 1);
    }

    /**
     * 将原数组的所有值都放到新数组中
     *
     * @param newTable 新数组
     * @param rehash   是否要重新进行hash计算,我们大部分情况下是false
     */
    void transfer(HashMap.Entry[] newTable, boolean rehash) {
        //获得新的容量大小啊
        int newCapacity = newTable.length;

        //进行数据的转移,我们可以看到一个for嵌套一个while,是双重循环
        for (HashMap.Entry<K, V> e : table) {
            while (null != e) {

                HashMap.Entry<K, V> next = e.next;

                //这一步十分重要,是否要进行rehash,我们一般传过来的都是false
                if (rehash) {
                    e.hash = null == e.key ? 0 : hash(e.key);
                }

                //假如没有进行rehash,那么得到的值只有两个结果:
                //1、元素下标还是原来的 2、元素下标改变为原来的下标+原来的数组长度
                int i = indexFor(e.hash, newCapacity);
                
                /*
	                来看一下这个元素节点,JDK7中的HashMap扩容存在很大的问题
	                
	                首先我们要明确,扩容的目的不仅是为了增大数组的容量,还要让链表的长度变短
	                简单来说是将一个长长的链表转换为一些短链表,所以它必须对链表进行循环,使用重新添加的方式重新将链表的结点一个一个拿出来,然后存到新数组的下标位置
                    
	                但是因为头插法的原因,这样就会造成一些问题:
	                1、扩容之前和扩容之后链表的顺序将会反转,比如原来是1->2->3,扩容后是3->2->1
	                2、多线程下会造成链表循环指向的问题
	                
	                所以其实根源是在于链表的头插法,将链表的顺序进行了改变
                */
                e.next = newTable[i];
                newTable[i] = e;
                e = next;
            }
        }
    }
下面的一切结论都是建立在没有进行rehash的情况下:

假如没有进行rehash,那么得到的结果有两种可能:
1、得到的数组下标是原数组的下标
2、得到的数组下表是原数组下标 + 原数组长度

这一段你们可能很难理解,举个例子就行了,比如说:
1)假如我们的hash值为0101 0101
    1. 扩容前:
    我们要进行indexFor,得到数组的下标
        根据key得到的hash值为 0101 0101
        数组的长度length为16
    进行indexFor运算:0101 0101 & (16-1)
    得到结果:0000 0101:数组下标为5
    2.扩容后
    我们要进行indexFor,得到数组的下标
        根据key得到的hash值为 0101 0101
        数组的长度length为32
    进行indexFor运算:0101 0101 & (32-1)
    得到结果:0001 0101:数组下标为5+16,也就是原数组索引+原数组长度
2)假如我们的hash值为0100 0101
    1. 扩容前:
    我们要进行indexFor,得到数组的下标
        根据key得到的hash值为 0100 0101
        数组的长度length为16
    进行indexFor运算:0101 0101 & (16-1)
    得到结果:0000 0101:数组下标为5
    2.扩容后
    我们要进行indexFor,得到数组的下标
        根据key得到的hash值为 0100 0101
        数组的长度length为32
    进行indexFor运算:0101 0101 & (32-1)
    得到结果:0000 0101:数组下标为5,也就是和原数组的下标相同

所以其实我们只需要看最高位是0还是1,假如我们扩容的高位为0,那么就是原来的,假如为1,那就是原来下标+原数组长度


/**
 * 添加元素
 *
 * @param hash        hash值
 * @param key         键
 * @param value       值
 * @param bucketIndex 要添加的数组下标
 */
void createEntry(int hash, K key, V value, int bucketIndex) {
    //下面即将使用头插法
    //首先获取到这个数组中存放的地址值,也就是链表的头结点
    HashMap.Entry<K, V> e = table[bucketIndex];
    /*
        数组存放的地址值为一个新的HashMap的Entry,也就是说链表的头结点变为了这个新的节点
        同时这里注意,最后传递了原来的头结点,这个最后的e就是节点的next
        头插法完成
     */
    table[bucketIndex] = new HashMap.Entry<>(hash, key, value, e);
    //HashMap总长度+1
    size++;
}

可以看到,头插法的效率就是如此之高,如果使用尾插法,那么首先要对这个链表进行一层遍历,然后才能在最后插入数据

但是在HashMap中,只要使用put方法,那么就要对这整个链表进行遍历,那么遍历完成之后肯定会得到尾部的节点,从这一点来看,在HashMap中头插法和尾插法好像没有什么太大的区别

  • put总要遍历一遍链表

image-20201109113925005

JDK8的HashMap

红黑树

红黑树是一颗平衡树,它在算法导论中是这样定义的:

1、每一个节点是红色的或者是黑色的

2、根节点(第一个节点)必须是黑的

3、每一个叶子节点必须是黑色的,你可以理解为指向null的都是黑色的

image-20201122143537558

4、不能有两个连续的红节点(红色节点的子节点必须是黑色的)

5、黑节点平衡(从任何一个节点到达它的子孙节点中的任何一个叶子节点中,黑节点拥有相同的数量)

比如在上面这张图中,比如30到叶子节点(NULL)中,都是两个黑色节点

红黑树插入新节点改如何判断是红色还是黑色

第五条是最难遵守的,所以我们默认插入的就是红色的节点,然后判断是否遵守其他的四个定义。

因为我们插入红色节点的时候,没有破坏黑节点平衡这个条件。假如我们插入了黑色的节点,很容易就会破坏黑节点平衡这个条件。

红黑树的左旋和右旋

平衡树想要平衡,需要旋转。红黑树就是一颗典型的平衡树,所以它也是需要旋转的。

举个例子:

现在有一颗红黑树(没有画叶子节点):

image-20201122142200346

现在我们要向树中添加一个数字:60

根据我们的计算,最后的结果应当是:30指向45,45下面的左节点位40,右节点为60

image-20201122142331796

我们对比两张图片可以开电脑,原来的30是指向了40,但是现在指向了45,这种情况就是红黑树的左旋。

红黑树左旋

右旋也是一样的,就类似这种结果。

那么现在来总结一下,在红黑树插入新节点的过程中,是如何进行改变的:``

1、假如父节点是黑色的,不用进行调整

2、假如父节点是红色的

  • 假如叔叔为空,旋转+变色
  • 叔叔为黑色,旋转+变色
  • 叔叔为红色,父节点+叔叔节点变为黑色,祖父节点变为红色(进行递归,直到整个红黑树满足规则即可结束)

HashMap的理解

image-20210104162607074

public V put(K key, V value) {
	/*
		四个参数
			第一个hash值
			第四个参数表示如果该key存在值如果为null的话,则插入新的value
			最后一个参数,在hashMap中没有用,可以不用管,使用默认的即可
	*/
    return putVal(hash(key), key, value, false, true);
}
final V putVal(int hash, K key, V value, boolean onlyIfAbsent,boolean evict) {
    //tab为哈希数组,p为该哈希桶(数组桶)的首节点,n为hashMap的长度,i为计算出的数组下标
    Node<K,V>[] tab; Node<K,V> p; int n, i;
    //获取长度并进行扩容,使用的是懒加载,table一开始是没有加载的,等put后才开始加载
    if ((tab = table) == null || (n = tab.length) == 0)	n = (tab = resize()).length;
    /*
		如果计算出的该哈希桶的位置没有值,则把新插入的key-value放到此处
		此处就算没有插入成功,也就是发生哈希冲突时也会把哈希桶的首节点赋予p
	*/
    if ((p = tab[i = (n - 1) & hash]) == null)	tab[i] = newNode(hash, key, value, null);
    //发生哈希冲突的几种情况
    else {
        // e 临时节点的作用, k 存放该当前节点的key 
        Node<K,V> e; K k;
        
        //第一种,插入的key-value的hash值,key都与当前节点的相等,e = p,则表示为首节点
        if (p.hash == hash &&((k = p.key) == key || (key != null && key.equals(k))))	e = p;
        
        //第二种,hash值不等于首节点,判断该p是否属于红黑树的节点
        else if (p instanceof TreeNode)
			/*
            	为红黑树的节点,则在红黑树中进行添加
            	如果该节点已经存在,则返回该节点(不为null)
            	该值很重要,用来判断put操作是否成功,如果添加成功返回null
			*/
            e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);

        //第三种,hash值不等于首节点,不为红黑树的节点,则为链表的节点
        else {
            //遍历该链表
            for (int binCount = 0; ; ++binCount) {
                //如果找到尾部,则表明添加的key-value没有重复,在尾部进行添加
                if ((e = p.next) == null) {
                    p.next = newNode(hash, key, value, null);
                    /*
                    	判断是否要转换为红黑树结构(或者进行扩容),这里的TREEIFY_THRESHOLD为8
                    	但是注意binCount是从0开始增加的,这就代表着加到7的时候就已经是8条数据了
                    */	
                    if (binCount >= TREEIFY_THRESHOLD - 1)	treeifyBin(tab, hash);	break;
                }
                //如果链表中有重复的key,e则为当前重复的节点,结束循环
                if (e.hash == hash &&((k = e.key) == key || (key != null && key.equals(k))))	break;
                
                p = e;
            }
        }
        //有重复的key,则用待插入值进行覆盖,返回旧值。
        if (e != null) { 
            V oldValue = e.value;
            if (!onlyIfAbsent || oldValue == null)	e.value = value;
            afterNodeAccess(e);
            return oldValue;
        }
    }
    //到了此步骤,则表明待插入的key-value是没有key的重复,因为插入成功e节点的值为null
    //修改次数+1
    ++modCount;
    //实际长度+1,判断是否大于临界值,大于则扩容
    if (++size > threshold)	resize();
    afterNodeInsertion(evict);
    //添加成功
    return null;
}
/*
	我们在上面说,当数组中的链表大于8的时候,就要进行判断是否要进行扩容,或者是转换为红黑树
*/
final void treeifyBin(HashMap.Node<K,V>[] tab, int hash) {
    int n, index; HashMap.Node<K,V> e;
    // 如果Map为空或者当前存入数据n(可以理解为map的size())的数量小于64便进行扩容
    if (tab == null || (n = tab.length) < MIN_TREEIFY_CAPACITY)	resize();

    // 如果size()大于64则将正在存入的该值所在链表转化成红黑树
    else if ((e = tab[index = (n - 1) & hash]) != null) {
        HashMap.TreeNode<K,V> hd = null, tl = null;
        do {
            HashMap.TreeNode<K,V> p = replacementTreeNode(e, null);
            if (tl == null)
                hd = p;
            else {
                p.prev = tl;
                tl.next = p;
            }
            tl = p;
        } while ((e = e.next) != null);
        if ((tab[index] = hd) != null)
            hd.treeify(tab);
    }
}

数组扩容只需要满足以下两个条件之一即可:

1、HashMap的容量超过了负载因子的限制容量(默认为0.75)时,扩容。比如数组有16个,填满了12个就开始扩容

2、某个数组桶中,链表的长度大于8并且HashMap数组长度小于64,进行扩容

数组中的链表转换为红黑树的条件:当前数组桶中的链表长度大于等于8并且数组长度大于等于64,开始转换为红黑树。

假如红黑树的节点小于6,那么转换回链表

posted @ 2021-01-10 16:33  嚎羸  阅读(59)  评论(0编辑  收藏  举报