【Java基础系列教程】第二十章 Java集合_超详细源码解析(面试必备技巧)

前言

在前面的学习集合中只是介绍了集合的相关用法,我们想要更深入的去了解集合那就要通过我们去分析它的源码来了解它。希望对集合有一个更进一步的理解!

        既然是看源码那我们要怎么看一个类的源码呢?这里我推荐的方法是:
                1、看继承结构:看这个类的层次结构,处于一个什么位置,可以在自己心里有个大概的了解。
                2、看构造方法:在构造方法中,看做了哪些事情,跟踪方法中里面的方法。
                3、看常用的方法:跟构造方法一样,这个方法实现功能是如何实现的。

        注:既然是源码,为什么要这样设计类,有这样的继承关系。这就要说到设计模式的问题了。所以我们要了解常用的设计模式,才能更深刻的去理解这个类。

一、ArrayList 源码分析

1.1 概述

        ArrayList是可以动态增长的索引序列,它是基于数组实现的List类,该类封装了一个动态再分配的Object[]数组。

        ArrayList的用法和Vector相类似,但是Vector是一个较老的集合,具有很多缺点,不建议使用。另外,ArrayList和Vector的区别是:ArrayList是线程不安全的,当多条线程访问同一个ArrayList集合时,程序需要手动保证该集合的同步性,而Vector则是线程安全的。

        一上来,先来看看源码中的这一段注释,我们可以从中提取到一些关键信息:
    
        Resizable-array implementation of the List interface. Implements all optional list operations, and permits all elements, including null. In addition to implementing the List interface, this class provides methods to manipulate the size of the array that is used internally to store the list. (This class is roughly equivalent to Vector, except that it is unsynchronized.)
    
        从这段注释中,我们可以得知 ArrayList 是一个动态数组,实现了 List 接口以及 List相关的所有方法,它允许所有元素的插入,包括 null。另外,ArrayList 和 Vector 除了线程不同步之外,大致相等。

ArrayList的继承结构:

public class ArrayList<E> extends AbstractList<E>
        implements List<E>, RandomAccess, Cloneable, java.io.Serializable

public abstract class AbstractList<E> extends AbstractCollection<E> implements List<E> 

        1)为什么要先继承AbstractList,而让AbstractList先实现List<E>?而不是让ArrayList直接实现List<E>?                        
                这里是有一个思想,接口中全都是抽象的方法,而抽象类中可以有抽象方法,还可以有具体的实现方法,正是利用了这一点,让AbstractList是实现接口中一些通用的方法,而具体的类,如ArrayList就继承这个AbstractList类,拿到一些通用的方法,然后自己在实现一些自己特有的方法,这样一来,让代码更简洁,就继承结构最底层的类中通用的方法都抽取出来,先一起实现了,减少重复代码。所以一般看到一个类上面还有一个抽象类,应该就是这个作用。

        2)ArrayList实现了哪些接口?
                List<E>接口:我们会出现这样一个疑问,在查看了ArrayList的父类AbstractList也实现了List<E>接口,那为什么子类ArrayList还是去实现一遍呢?
        这是想不通的地方,所以我就去查资料,有的人说是为了查看代码方便,使观看者一目了然,说法不一,但每一个让我感觉合理的,但是在stackOverFlow(IT问答网站)中找到了答案,这里其实很有趣。       
                这其实是一个mistake-(失误),因为他写这代码的时候觉得这个会有用处,但是其实并没什么用,但因为没什么影响,就一直留到了现在。

                RandomAccess接口:这个是一个标记性接口,通过查看api文档,它的作用就是用来快速随机存取,有关效率的问题,在实现了该接口的话,那么使用普通的for循环来遍历,性能更高,例如ArrayList。而没有实现该接口的话,使用Iterator来迭代,这样性能更高,例如LinkedList。所以这个标记性只是为了让我们知道我们用什么样的方式去获取数据性能更好。

                Cloneable接口:实现了该接口,就可以使用Object.Clone()方法了。

                Serializable接口:实现该序列化接口,表明该类可以被序列化,什么是序列化?简单的说,就是能够从类变成字节流传输,然后还能从字节流变成原来的类。
    
        注:所有的空接口,都是标记性接口;

1.2 属性

        ArrayList 的属性非常少,就只有这些。其中最重要的莫过于 elementData 了,ArrayList所有的方法都是建立在 elementData 之上。

//序列化版本号
private static final long serialVersionUID = 8683452581122892189L;

//默认容量的大小
private static final int DEFAULT_CAPACITY = 10;

//空数组常量
private static final Object[] EMPTY_ELEMENTDATA = {};

//默认的空数组常量
private static final Object[] DEFAULTCAPACITY_EMPTY_ELEMENTDATA = {};

//存放元素的数组,从这可以发现 ArrayList 的底层实现就是一个 Object数组
transient Object[] elementData; // non-private to simplify nested class access

//数组中包含的元素个数
private int size;

//数组的最大上限
private static final int MAX_ARRAY_SIZE = Integer.MAX_VALUE - 8;

1.3 方法

1.3.1 构造方法

// ArrayList中储存数据的其实就是一个数组,这个数组就是elementData,在135行定义的 transient Object[] elementData;
public ArrayList() {
    // 调用父类中的无参构造方法,父类中的是个空的构造方法
	super();
    
    // DEFAULTCAPACITY_EMPTY_ELEMENTDATA:是个空的Object[],将elementData初始化,elementData也是个Object[]类型。空的Object[]会给默认大小10,等会会解释什么时候赋值的。
    this.elementData = DEFAULTCAPACITY_EMPTY_ELEMENTDATA;
}

public ArrayList(int initialCapacity) {
    // 父类中空的构造方法
	super(); 
    
    // 判断如果自定义大小的容量是否大于0
    if (initialCapacity > 0) {
        // 将自定义的容量大小当成初始化elementData的大小
        this.elementData = new Object[initialCapacity];
    } else if (initialCapacity == 0) {
        this.elementData = EMPTY_ELEMENTDATA;
    } else {
        throw new IllegalArgumentException("Illegal Capacity: "+
                                           initialCapacity);
    }
}

public ArrayList(Collection<? extends E> c) {
    //参数集合转换为Object类型数组
    Object[] a = c.toArray();
    // 实际容量赋值为数组的长度,在数组长度不为0的情况下,判断参数类型
    if ((size = a.length) != 0) {
        //如果是ArrayList集合,那么直接进行引用传递
        if (c.getClass() == ArrayList.class) {
            elementData = a;
        } else {
            //如果不是ArrayList集合,那么copyOf一个新的数组
            elementData = Arrays.copyOf(a, size, Object[].class);
        }
    } else {
        //替换为空是数组
        elementData = EMPTY_ELEMENTDATA;
    }
}

        从构造方法中我们可以看见,默认情况下,elementData 是一个大小为 0 的空数组,当我们指定了初始大小的时候,elementData 的初始大小就变成了我们所指定的初始大小了。

1.3.2 get 方法

public E get(int index) {
    // 检验索引是否合法
    rangeCheck(index);

    return elementData(index);
}

private void rangeCheck(int index) {
    if (index >= size)
        throw new IndexOutOfBoundsException(outOfBoundsMsg(index));
}

E elementData(int index) {
    return (E) elementData[index];
}

        因为 ArrayList 是采用数组结构来存储的,所以它的 get 方法非常简单,先是判断一下有没有越界,之后就可以直接通过数组下标来获取元素了,所以 get 的时间复杂度是 O(1)。

1.3.3 add 方法

public boolean add(E e) {
    // 确定内部容量是否够了,size是数组中数据的个数,因为要添加一个元素,所以size+1,先判断size+1的这个个数数组能否放得下,就在这个方法中去判断是否数组.length是否够用了。
    ensureCapacityInternal(size + 1);  // Increments modCount!!
    // 在数据中正确的位置上放上元素e,并且size++
    elementData[size++] = e;
    return true;
}

/*
 * ensureCapacityInternal() 确保内部容量是否满足
 * ensureExplicitCapacity() 确保显式容量是否满足
 * calculateCapacity() 计算容量的方法
 */
private void ensureCapacityInternal(int minCapacity) {
    // 确认实际的容量,上面只是将minCapacity=10,这个方法就是真正的判断elementData是否够用
    ensureExplicitCapacity(calculateCapacity(elementData, minCapacity));
}

private static int calculateCapacity(Object[] elementData, int minCapacity) {
    // 看,判断初始化的elementData是不是空的数组,也就是没有长度
    if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA) {
        // 因为如果是空的话,minCapacity=size+1;其实就是等于1,空的数组没有长度就存放不了,所以就将minCapacity变成10,也就是默认大小,但是在这里,还没有真正的初始化这个elementData的大小。
        return Math.max(DEFAULT_CAPACITY, minCapacity);
    }
    return minCapacity;
}

private void ensureExplicitCapacity(int minCapacity) {
    modCount++;

    // minCapacity如果大于了实际elementData的长度,那么就说明elementData数组的长度不够用,不够用那么就要增加elementData的length。这里有的同学就会模糊minCapacity到底是什么呢,这里给你们分析一下

    /* 第一种情况:由于elementData初始化时是空的数组,那么第一次add的时候,minCapacity=size+1;也就minCapacity=1,在上一个方法(确定内部容量ensureCapacityInternal)就会判断出是空的数组,就会给将minCapacity=10,到这一步为止,还没有改变elementData的大小。
 	第二种情况:elementData不是空的数组了,那么在add的时候,minCapacity=size+1;也就是minCapacity代表着elementData中增加之后的实际数据个数,拿着它判断elementData的length是否够用,如果length不够用,那么肯定要扩大容量,不然增加的这个元素就会溢出。
	*/
    if (minCapacity - elementData.length > 0)
        // ArrayList能自动扩展大小的关键方法就在这里了
        grow(minCapacity);
}

public void add(int index, E element) {
    // 检查index也就是插入的位置是否合理。
    rangeCheckForAdd(index);
    // 跟上面的分析一样,具体看上面
    ensureCapacityInternal(size + 1);  // Increments modCount!!
    // 这个方法就是用来在插入元素之后,要将index之后的元素都往后移一位,
    System.arraycopy(elementData, index, elementData, index + 1,
                     size - index);
    // 在目标位置上存放元素
    elementData[index] = element;
    // size增加1
    size++;
} 

private void rangeCheckForAdd(int index) {
    // 插入的位置肯定不能大于size 和小于0
    if (index > size || index < 0)   
        // 如果是,就报这个越界异常
        throw new IndexOutOfBoundsException(outOfBoundsMsg(index));
}

        ArrayList 的 add 方法也很好理解,在插入元素之前,它会先检查是否需要扩容,然后再把元素添加到数组中最后一个元素的后面。在 ensureCapacityInternal 方法中,我们可以看见,如果当 elementData 为空数组时,它会使用默认的大小去扩容。所以说,通过无参构造方法来创建 ArrayList 时,它的大小其实是为 0 的,只有在使用到的时候,才会通过 grow 方法去创建一个大小为 10 的数组。第一个 add 方法的复杂度为 O(1),虽然有时候会涉及到扩容的操作,但是扩容的次数是非常少的,所以这一部分的时间可以忽略不计。如果使用的是带指定下标的 add方法,则复杂度为 O(n),因为涉及到对数组中元素的移动,这一操作是非常耗时的。

        正常情况下会扩容1.5倍,特殊情况下(新扩展数组大小已经达到了最大值)则只取最大值。
    
        当我们调用add方法时,实际上的函数调用如下:

 举例说明一:

List<Integer> lists = new ArrayList<Integer>();
lists.add(8);

        说明:初始化lists大小为0,调用的ArrayList()型构造函数,那么在调用lists.add(8)方法时,会经过怎样的步骤呢?下图给出了该程序执行过程和最初与最后的elementData的大小。

        说明:我们可以看到,在add方法之前开始elementData = {};调用add方法时会继续调用,直至grow,最后elementData的大小变为10,之后再返回到add函数,把8放在elementData[0]中。

举例说明二:

List<Integer> lists = new ArrayList<Integer>(6);
lists.add(8);

        说明:调用的ArrayList(int)型构造函数,那么elementData被初始化为大小为6的Object数组,在调用add(8)方法时,具体的步骤如下:

         说明:我们可以知道,在调用add方法之前,elementData的大小已经为6,之后再进行传递,不会进行扩容处理。

1.3.4 set 方法

public E set(int index, E element) {
    // 检验索引是否合法
    rangeCheck(index);
    // 旧值
    E oldValue = elementData(index);
    // 赋新值
    elementData[index] = element;
    // 返回旧值
    return oldValue;
}

        set 方法的作用是把下标为 index 的元素替换成 element,跟 get 非常类似,所以就不在赘述了,时间复杂度度为 O(1)。

1.3.5 remove 方法 和 clear 方法

public E remove(int index) {
    // 检查index的合理性
    rangeCheck(index);
    // 这个作用很多,比如用来检测快速失败的一种标志。
    modCount++;
    // 通过索引直接找到该元素
    E oldValue = elementData(index);
    // 计算要移动的个数。
    int numMoved = size - index - 1;
    if (numMoved > 0)
        // 这个方法也已经解释过了,就是用来移动元素的。
        System.arraycopy(elementData, index+1, elementData, index,numMoved);
    // 将--size上的位置赋值为null,让gc(垃圾回收机制)更快的回收它。
    elementData[--size] = null; // clear to let GC do its work
    //返回删除的元素。
    return oldValue;
}

// 感觉这个不怎么要分析吧,都看得懂,就是通过元素来删除该元素,就依次遍历,如果有这个元素,就将该元素的索引传给fastRemove(index),使用这个方法来删除该元素,
public boolean remove(Object o) {
	if (o == null) {
		for (int index = 0; index < size; index++)
			if (elementData[index] == null) {
            	fastRemove(index);
                return true;
			}
	} else {
		for (int index = 0; index < size; index++)
			if (o.equals(elementData[index])) {
				fastRemove(index);
				return true;
			}
        }
	return false;
}

// fastRemove(index)方法的内部跟remove(index)的实现几乎一样,这里最主要是知道ArrayList可以存储null值
private void fastRemove(int index) {
    modCount++;
    int numMoved = size - index - 1;
    if (numMoved > 0)
        System.arraycopy(elementData, index+1, elementData, index,
                         numMoved);
    elementData[--size] = null; // clear to let GC do its work
}

        remove 方法与 add 带指定下标的方法非常类似,也是调用系统的 arraycopy 方法来移动元素,时间复杂度为 O(n)。

// clear():将elementData中每个元素都赋值为null,等待垃圾回收将这个给回收掉,所以叫clear
public void clear() {
    modCount++;
    // clear to let GC do its work
    for (int i = 0; i < size; i++)
        elementData[i] = null;
    size = 0;
}


// removeAll(collection c):从此列表中删除指定集合中包含的所有元素。
public boolean removeAll(Collection<?> c) {
    // 批量删除
	return batchRemove(c, false);
}

// batchRemove(xx,xx):用于两个方法,一个removeAll():它只清除指定集合中的元素,retainAll()用来测试两个集合是否有交集。 
// 这个方法,用于两处地方,如果complement为false,则用于removeAll;如果为true,则给retainAll()用,retainAll()是用来检测两个集合是否有交集的。
private boolean batchRemove(Collection<?> c, boolean complement) {
    // 将原集合,记名为A
    final Object[] elementData = this.elementData; 
    // r用来控制循环,w是记录有多少个交集
    int r = 0, w = 0;   
    boolean modified = false;  
    try {
        for (; r < size; r++)
            // 参数中的集合C一次检测集合A中的元素是否有,
            if (c.contains(elementData[r]) == complement)
                // 有的话,就给集合A
                elementData[w++] = elementData[r];
    } finally {
        // Preserve behavioral compatibility with AbstractCollection,
        // even if c.contains() throws.	
        // 如果contains方法使用过程报异常
        if (r != size) {
            // 将剩下的元素都赋值给集合A,
            System.arraycopy(elementData, r,
                             elementData, w,
                             size - r);
            w += size - r;
        }
        if (w != size) {
            // 这里有两个用途,在removeAll()时,w一直为0,就直接跟clear一样,全是为null。
            // retainAll():没有一个交集返回true,有交集但不全交也返回true,而两个集合相等的时候,返回false,所以不能根据返回值来确认两个集合是否有交集,而是通过原集合的大小是否发生改变来判断,如果原集合中还有元素,则代表有交集,而元集合没有元素了,说明两个集合没有交集。
            // clear to let GC do its work
            for (int i = w; i < size; i++)
                elementData[i] = null;
            modCount += size - w;
            size = w;
            modified = true;
        }
    }
    return modified;
}

1.3.6 grow 方法

private void grow(int minCapacity) {
    // overflow-conscious code
    // 将扩充前的elementData大小给oldCapacity
    int oldCapacity = elementData.length;  
    
    // newCapacity就是1.5倍的oldCapacity
    int newCapacity = oldCapacity + (oldCapacity >> 1);
    
    // 这句话就是适应于elementData空数组的时候,length=0,那么oldCapacity=0,newCapacity=0,所以这个判断成立,在这里就是真正的初始化elementData的大小了,就是为10.前面的工作都是准备工作。
    if (newCapacity - minCapacity < 0)
        newCapacity = minCapacity;
    
    // 如果newCapacity超过了最大的容量限制,就调用hugeCapacity,也就是将能给的最大值给newCapacity
    if (newCapacity - MAX_ARRAY_SIZE > 0)
        newCapacity = hugeCapacity(minCapacity);
    // minCapacity is usually close to size, so this is a win:
    // 新的容量大小已经确定好了,就copy数组,改变容量大小咯。
    elementData = Arrays.copyOf(elementData, newCapacity);
}

// 这个就是上面用到的方法,很简单,就是用来赋最大值。
private static int hugeCapacity(int minCapacity) {
    if (minCapacity < 0) // overflow
        throw new OutOfMemoryError();
    // 如果minCapacity都大于MAX_ARRAY_SIZE,那么就Integer.MAX_VALUE返回,反之将MAX_ARRAY_SIZE返回。因为maxCapacity是三倍的minCapacity,可能扩充的太大了,就用minCapacity来判断了。
    // Integer.MAX_VALUE:2147483647   MAX_ARRAY_SIZE:2147483639  也就是说最大也就能给到第一个数值。还是超过了这个限制,就要溢出了。相当于ArrayList给了两层防护。
    return (minCapacity > MAX_ARRAY_SIZE) ?
        Integer.MAX_VALUE : MAX_ARRAY_SIZE;
}

        grow 方法是在数组进行扩容的时候用到的,从中我们可以看见,ArrayList 每次扩容都是扩 1.5 倍,然后调用 Arrays 类的 copyOf 方法,把元素重新拷贝到一个新的数组中去。

1.3.7 size 方法

public int size() {
    return size;
}

        size 方法非常简单,它是直接返回 size 的值,也就是返回数组中元素的个数,时间复杂度为 O(1)。这里要注意一下,返回的并不是数组的实际大小。

1.3.8 indexOf 方法和 lastIndexOf

// 从首开始查找数组里面是否存在指定元素
public int indexOf(Object o) {
    // 查找的元素为空
    if (o == null) { 
        // 遍历数组,找到第一个为空的元素,返回下标
        for (int i = 0; i < size; i++) 
            if (elementData[i]==null)
                return i;
    } else { // 查找的元素不为空
        // 遍历数组,找到第一个和指定元素相等的元素,返回下标
        for (int i = 0; i < size; i++) 
            if (o.equals(elementData[i]))
                return i;
    } 
    // 没有找到,返回-1
    return -1;
}

public int lastIndexOf(Object o) {
    if (o == null) {
        for (int i = size-1; i >= 0; i--)
            if (elementData[i]==null)
                return i;
    } else {
        for (int i = size-1; i >= 0; i--)
            if (o.equals(elementData[i]))
                return i;
    }
    return -1;
}

        indexOf 方法的作用是返回第一个等于给定元素的值的下标。它是通过遍历比较数组中每个元素的值来查找的,所以它的时间复杂度是 O(n)。

        lastIndexOf 的原理跟 indexOf 一样,而它仅仅是从后往前找起罢了。

二、Vector 源码分析

        Vector 很多方法都跟 ArrayList 一样,只是多加了个 synchronized 来保证线程安全罢了。如果照着 ArrayList 的方式再将一次就显得没意思了,所以只把 Vector 与 ArrayList的不同点提一下就可以了。

        Vector 比 ArrayList 多了一个属性:
                protected int capacityIncrement;

        这个属性是在扩容的时候用到的,它表示每次扩容只扩 capacityIncrement 个空间就足够了。该属性可以通过构造方法给它赋值。先来看一下构造方法:

//这个是一个空的Vector构造方法,所以让他使用内置的数组,这里还不知道什么是内置的数组,看它调用了自身另外一个带一个参数的构造器.
public Vector() {
    this(10);
}

//给空的cector构造器用和带有一个特定初始化容量用的,并且又调用了另外一个带两个参数的构造器,并且给容量增长值(capacityIncrement=0)为0,查看vector中的变量可以发现capacityIncrement是一个成员变量
public Vector(int initialCapacity) {
    this(initialCapacity, 0);
}

//构建一个有特定的初始化容量和容量增长值的空的Vector,
public Vector(int initialCapacity, int capacityIncrement) {
    // 调用父类的构造,是个空构造
    super();
    // 小于0,会报非法参数异常
    if (initialCapacity < 0)
        throw new IllegalArgumentException("Illegal Capacity: "+
                                           initialCapacity);
    // elementData是一个成员变量数组,初始化它,并给它初始化长度。默认就是10,除非自己给值。
    this.elementData = new Object[initialCapacity];
    // capacityIncrement的意思是如果要扩增数组,每次增长该值,如果该值为0,那数组就变为两倍的原长度,这个之后会分析到
    this.capacityIncrement = capacityIncrement;
}

        从构造方法中,我们可以看出 Vector 的默认大小也是 10,而且它在初始化的时候就已经创建了数组了,这点跟 ArrayList 不一样。再来看一下 grow 方法:

private void grow(int minCapacity) {
    // overflow-conscious code
    int oldCapacity = elementData.length;
    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);
}

        从 grow 方法中我们可以发现,newCapacity 默认情况下是两倍的 oldCapacity,而当指定了 capacityIncrement 的值之后,newCapacity 变成了oldCapacity+capacityIncrement。

ArrayList和Vector对比:

        1、ArrayList 创建时的大小为 0;当加入第一个元素时,进行第一次扩容时,默认容量大小为 10。

        2、ArrayList 每次扩容都以当前数组大小的 1.5 倍去扩容。

        3、Vector 创建时的默认大小为 10。

        4、Vector 每次扩容都以当前数组大小的 2 倍去扩容。当指定了 capacityIncrement 之后,每次扩容仅在原先基础上增加 capacityIncrement 个单位空间。

        5、ArrayList 和 Vector 的 add、get、size 方法的复杂度都为 O(1),remove 方法的复杂度为 O(n)。

        6、ArrayList 是非线程安全的,Vector 是线程安全的,因为它的方法都加了synchronized关键字。

为什么现在都不提倡使用vector了:

        1)Vector实现线程安全的方法是在每个操作方法上加锁,这些锁并不是必须要的,在实际开发中,一般都是通过锁一系列的操作来实现线程安全,也就是说将需要同步的资源放一起加锁来保证线程安全,

        2)如果多个Thread并发执行一个已经加锁的方法,但是在该方法中,又有Vector的存在,Vector本身实现中已经加锁了,那么相当于锁上又加锁,会造成额外的开销,

        3)在遍历时又得额外加锁,又是额外的开销,还不如直接用ArrayList,然后再加锁呢。

        总结:Vector在你不需要进行线程安全的时候,也会给你加锁,也就导致了额外开销,所以在jdk1.5之后就被弃用了,现在如果要用到线程安全的集合,都是从java.util.concurrent包下去拿相应的类。

三、LinkedList 源码分析

3.1 概述

        LinkedList 是一种可以在任何位置进行高效地插入和移除操作的有序序列,它是基于双向链表实现的。

        LinkedList 是一个继承于AbstractSequentialList的双向链表。它也可以被当作堆栈、队列或双端队列进行操作。

        LinkedList 实现 List 接口,能对它进行队列操作。

        LinkedList 实现 Deque 接口,即能将LinkedList当作双端队列使用。

        LinkedList 实现了Cloneable接口,即覆盖了函数clone(),能克隆。

        LinkedList 实现java.io.Serializable接口,这意味着LinkedList支持序列化,能通过序列化去传输。

        LinkedList 是非同步的。

        先来看看源码中的这一段注释,我们先尝试从中提取一些信息:
    
        Doubly-linked list implementation of the List and Deque interfaces. Implements all optional list operations, and permits all elements (including null).All of the operations perform as could be expected for a doubly-linked list. Operations that index into the list will traverse the list from the beginning or the end, whichever is closer to the specified index.
    
        Note that this implementation is not synchronized. If multiple threads access a linked list concurrently, and at least one of the threads modifies the list structurally, it must be synchronized externally. (A structural modification is any operation that adds or deletes one or more elements; merely setting the value of an element is not a structural modification.) This is typically accomplished by synchronizing on some object that naturally encapsulates the list.

        从这段注释中,我们可以得知 LinkedList 是通过一个双向链表来实现的,它允许插入所有元素,包括 null,同时,它是线程不同步的。如果对双向链表这个数据结构很熟悉的话,学习 LinkedList 就没什么难度了。下面是双向链表的结构:

        双向链表每个结点除了数据域之外,还有一个前指针和后指针,分别指向前驱结点和后继结点(如果有前驱/后继的话)。另外,双向链表还有一个 first 指针,指向头节点,和 last 指针,指向尾节点。

LinkedList的继承结构:

public class LinkedList<E>
    extends AbstractSequentialList<E>
    implements List<E>, Deque<E>, Cloneable, java.io.Serializable{}

        我们可以看到,LinkedList在最底层,说明他的功能最为强大,并且细心的还会发现,ArrayList只有四层,这里多了一层AbstractSequentialList的抽象类,为什么呢?

        1)减少实现顺序存取(例如LinkedList)这种类的工作,通俗的讲就是方便,抽象出类似LinkedList这种类的一些共同的方法

        2)既然有了上面这句话,那么以后如果自己想实现顺序存取这种特性的类(就是链表形式),那么就继承这个AbstractSequentialList抽象类,如果想像数组那样的随机存取的类,那么就去实现AbstracList抽象类。

        3)这样的分层,就很符合我们抽象的概念,越在高处的类,就越抽象,往在底层的类,就越有自己独特的个性。自己要慢慢领会这种思想。

        4)LinkedList的类继承结构很有意思,我们着重要看是Deque接口,Deque接口表示是一个双端队列,那么也意味着LinkedList是双端队列的一种实现,所以,基于双端队列的操作在LinkedList中全部有效。 

各种链表结构:

        单向链表:
                element:用来存放元素;
                next:用来指向下一个节点元素;
                通过每个结点的指针指向下一个结点从而链接起来的结构,最后一个节点的next指向null。

        单向循环链表:
                element、next 跟前面一样;
                在单向链表的最后一个节点的next会指向头节点,而不是指向null,这样存成一个环。

        双向链表:
                element:存放元素;
                pre:用来指向前一个元素;
                next:指向后一个元素;
                双向链表是包含两个指针的,pre指向前一个节点,next指向后一个节点,但是第一个节点head的pre指向null,最后一个节点的tail指向null。

        双向循环链表:
                element、pre、next 跟前面的一样;
                第一个节点的pre指向最后一个节点,最后一个节点的next指向第一个节点,也形成一个“环”。

3.2 属性

接下来看一下 LinkedList 中的属性:

//链表的节点个数
transient int size = 0;

//指向头节点的指针
transient Node<E> first;

//指向尾节点的指针
transient Node<E> last;

        LinkedList 的属性非常少,就只有这些。通过这三个属性,其实我们大概也可以猜测出它是怎么实现的了。注意,头结点、尾结点都有transient关键字修饰,这也意味着在序列化时该域是不会序列化的。

3.3 方法

3.3.1 结点结构

        Node 是在 LinkedList 里定义的一个静态内部类,它表示链表每个节点的结构,包括一个数据域 item,一个后置指针 next,一个前置指针 prev。

//根据前面介绍双向链表就知道这个代表什么了,linkedList的奥秘就在这里。
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;
    }
}

3.3.2 添加元素

        对于链表这种数据结构来说,添加元素的操作无非就是在表头/表尾插入元素,又或者在指定位置插入元素。因为 LinkedList 有头指针和尾指针,所以在表头或表尾进行插入元素只需要 O(1) 的时间,而在指定位置插入元素则需要先遍历一下链表,所以复杂度为 O(n)。

在表头添加元素的过程如下:

         当向表头插入一个节点时,很显然当前节点的前驱一定为 null,而后继结点是 first 指针指向的节点,当然还要修改 first 指针指向新的头节点。除此之外,原来的头节点变成了第二个节点,所以还要修改原来头节点的前驱指针,使它指向表头节点,源码的实现如下:

public void addFirst(E e) {
    linkFirst(e);
}

private void linkFirst(E e) {
    final Node<E> f = first;
    
    //当前节点的前驱指向 null,后继指针原来的头节点
    final Node<E> newNode = new Node<>(null, e, f);
    
    //头指针指向新的头节点
    first = newNode;
    
    //如果原来有头节点,则更新原来节点的前驱指针,否则更新尾指针
    if (f == null)
        last = newNode;
    else
        f.prev = newNode;
    size++;
    modCount++;
}

在表尾添加元素跟在表头添加元素大同小异,如图所示:

         当向表尾插入一个节点时,很显然当前节点的后继一定为 null,而前驱结点是 last 指针指向的节点,然后还要修改 last 指针指向新的尾节点。此外,还要修改原来尾节点的后继指针,使它指向新的尾节点,源码的实现如下:

public void addLast(E e) {
    linkLast(e);
}

/**
* Links e as last element.
*/
void linkLast(E e) {
    // 临时节点l(L的小写)保存last,也就是l指向了最后一个节点
    final Node<E> l = last;
    
    // 将e封装为节点,并且e.prev指向了最后一个节点
    final Node<E> newNode = new Node<>(l, e, null);
    
    // newNode成为了最后一个节点,所以last指向了它
    last = newNode;
    
    // 判断是不是一开始链表中就什么都没有,如果没有,则newNode就成为了第一个节点,first和last都要指向它
    if (l == null)    
        first = newNode;
    else    // 正常的在最后一个节点后追加,那么原先的最后一个节点的next就要指向现在真正的最后一个节点,原先的最后一个节点就变成了倒数第二个节点
        l.next = newNode;
	// 添加一个节点,size自增
    size++;
    modCount++;
}

最后,在指定节点之前插入,如图所示:

         当向指定节点之前插入一个节点时,当前节点的后继为指定节点,而前驱结点为指定节点的前驱节点。此外,还要修改前驱节点的后继为当前节点,以及后继节点的前驱为当前节点,源码的实现如下:

public void add(int index, E element) {
    // 检查参数下标,如果参数下标小于0或大于元素个数,则抛出异常
    checkPositionIndex(index);
	
    // 如果参数下标等于实际元素个数
    if (index == size)
        // 则添加到列表末尾
        linkLast(element);
    else
        // 通过下标获取到现有节点,调用linkBefore方法
        // node(index)有点折半查找的意思
        linkBefore(element, node(index));
}

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

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

// 把指定元素插入到指定节点之前
void linkBefore(E e, Node<E> succ) {
    // assert succ != null;
    // 使用pred保持现有节点的前一个节点
    final Node<E> pred = succ.prev;
    
    // 插入的节点的前一个节点是pred,后一个节点的现有节点succ
    final Node<E> newNode = new Node<>(pred, e, succ);
    
    // 现有节点的前一个节点是插入的节点
    succ.prev = newNode;
    
    // 如果pred为null,那么表示newNode是第一个节点
    if (pred == null)
        first = newNode;
    else
        // pred的下一个节点是插入的节点
        pred.next = newNode;
    size++;
    modCount++;
}

3.3.3 删除元素

        删除操作与添加操作大同小异,例如删除指定节点的过程如下图所示,需要把当前节点的前驱节点的后继修改为当前节点的后继,以及当前节点的后继结点的前驱修改为当前节点的前驱:

         删除头节点和尾节点跟删除指定节点非常类似,就不一一介绍了,源码如下:

//删除表头节点,返回表头元素的值
public E removeFirst() {
    final Node<E> f = first;
    if (f == null)
        throw new NoSuchElementException();
    //实际调用的是unlinkFirst()
    return unlinkFirst(f);
}

private E unlinkFirst(Node<E> f) {
    // assert f == first && f != null;
    final E element = f.item;
    final Node<E> next = f.next;
    f.item = null;
    f.next = null; // help GC
    
    //头指针指向后一个节点
    first = next; 
    if (next == null)
        last = null;
    else
        //新头节点的前驱为 null
        next.prev = null;
    size--;
    modCount++;
    return element;
}

//删除表尾节点,返回表尾元素的值
public E removeLast() {
    final Node<E> l = last;
    if (l == null)
        throw new NoSuchElementException();
    //实际调用的是unlinkLast()
    return unlinkLast(l);
}

private E unlinkLast(Node<E> l) {
    // assert l == last && l != null;
    final E element = l.item;
    final Node<E> prev = l.prev;
    l.item = null;
    l.prev = null; // help GC
    
    //尾指针指向前一个节点
    last = prev;
    if (prev == null)
        first = null;
    else
        //新尾节点的后继为 null
        prev.next = null;
    size--;
    modCount++;
    return element;
}


public E remove(int index) {
    checkElementIndex(index);
    return unlink(node(index));
}

//不能传一个null值过,注意,看之前要注意之前的next、prev这些都是谁。
E unlink(Node<E> x) {
    // assert x != null;
    //拿到节点x的三个属性
    final E element = x.item;
    final Node<E> next = x.next;
    final Node<E> prev = x.prev;
    //这里开始往下就进行移除该元素之后的操作,也就是把指向哪个节点搞定。
    if (prev == null) {
        //说明移除的节点是头节点,则first头节点应该指向下一个节点
        first = next;
    } else {
        //不是头节点,prev.next=next:有1、2、3,将1.next指向3
        prev.next = next;
        //然后解除x节点的前指向。
        x.prev = null;
    }

    if (next == null) {
        //说明移除的节点是尾节点
        last = prev;
    } else {
        //不是尾节点,有1、2、3,将3.prev指向1. 然后将2.next=解除指向。
        next.prev = prev;
        x.next = null;
    }
    //x的前后指向都为null了,也把item为null,让gc回收它
    x.item = null;
    size--;    //移除一个节点,size自减
    modCount++;
    return element;    //由于一开始已经保存了x的值到element,所以返回。
}

3.3.4 获取元素

        获取元素的方法一看就懂,我就不必多加解释了。

//获取表头元素
public E getFirst() {
    final Node<E> f = first;
    if (f == null)
        throw new NoSuchElementException();
    return f.item;
}

//获取表头元素
public E getLast() {
    final Node<E> l = last;
    if (l == null)
        throw new NoSuchElementException();
    return l.item;
}

//获取指定下标的元素
//这里没有什么,重点还是在node(index)中
public E get(int index) {
    checkElementIndex(index);
    return node(index).item;
}

Node<E> node(int index) {
    // assert isElementIndex(index);
	//根据下标是否超过链表长度的一半,来选择从头部开始遍历还是从尾部开始遍历
    if (index < (size >> 1)) {
        Node<E> x = first;
        for (int i = 0; i < index; i++)
            x = x.next;
        return x;
    } else {
        Node<E> x = last;
        for (int i = size - 1; i > index; i--)
            x = x.prev;
        return x;
    }
}

3.3.5 其他方法

//链表的大小
public int size() {
    return size;
}

//添加元素到表尾
public boolean add(E e) {
    linkLast(e);
    return true;
}

//删除指定元素
public boolean remove(Object o) {
    if (o == null) {
        for (Node<E> x = first; x != null; x = x.next) {
            if (x.item == null) {
                unlink(x);
                return true;
            }
        }
    } else {
        for (Node<E> x = first; x != null; x = x.next) {
            if (o.equals(x.item)) {
                unlink(x);
                return true;
            }
        }
    }
    return false;
}

//替换指定下标的值
public E set(int index, E element) {
    checkElementIndex(index);
    Node<E> x = node(index);
    E oldVal = x.item;
    x.item = element;
    return oldVal;
}

//获取表头节点的值,表头为空返回 null
public E peek() {
    final Node<E> f = first;
    return (f == null) ? null : f.item;
}

//获取表头节点的值,表头为空抛出异常
public E element() {
    return getFirst();
}

//获取表头节点的值,并删除表头节点,表头为空返回 null
public E poll() {
    final Node<E> f = first;
    return (f == null) ? null : unlinkFirst(f);
}

//添加元素到表头
public void push(E e) {
    addFirst(e);
}

//删除表头元素
public E pop() {
    return removeFirst();
}

//这个很简单,就是通过实体元素来查找到该元素在链表中的位置。跟remove中的代码类似,只是返回类型不一样。
public int indexOf(Object o) {
    int index = 0;
    if (o == null) {
        for (Node<E> x = first; x != null; x = x.next) {
            if (x.item == null)
                return index;
            index++;
        }
    } else {
        for (Node<E> x = first; x != null; x = x.next) {
            if (o.equals(x.item))
                return index;
            index++;
        }
    }
    return -1;
}

3.4 总结

        1、LinkedList 的底层结构是一个带头/尾指针的双向链表,可以快速的对头/尾节点进行操作,通过一个Node内部类实现的这种链表结构。
    
        2、相比数组,链表的特点就是在指定位置插入和删除元素的效率较高,但是查找的效率就不如数组那么高了。

        3、linkedList不光能够向前迭代,还能像后迭代,并且在迭代的过程中,可以修改值、添加值、还能移除值。

        4、linkedList不光能当链表,还能当队列使用,这个就是因为实现了Deque接口。

四、HashMap 源码分析

4.1 概述

        HashMap基于Map接口实现,元素以键值对的方式存储,并且允许使用null 键和null 值,因为key不允许重复,因此只能有一个键为null,另外HashMap不能保证放入元素的顺序,它是无序的,和放入的顺序并不能相同。HashMap是线程不安全的。

        二话不说,一上来就点开源码,发现里面有一段介绍如下:

        Hash table based implementation of the Map interface. This implementation provides all of the optional map operations, and permits null values and the null key. (The HashMap class is roughly equivalent to Hashtable, except that it is unsynchronized and permits nulls.) This class makes no guarantees as to the order of the map; in particular, it does not guarantee that the order will remain constant over time.

        翻译一下大概就是在说,这个哈希表是基于 Map 接口的实现的,它允许 null 值和null 键,它不是线程同步的,同时也不保证有序。

        This implementation provides constant-time performance for the basic operations (get and put), assuming the hash function disperses the elements properly among the buckets. Iteration over collection views requires time proportional to the “capacity” of the HashMap instance (the number of buckets) plus its size (the number of key-value mappings). Thus, it’s very important not to set the initial capacity too high (or the load factor too low) if iteration performance is important.

        An instance of HashMap has two parameters that affect its performance: initial capacity and load factor. The capacity is the number of buckets in the hash table, and the initial capacity is simply the capacity at the time the hash table is created. The load factor is a measure of how full the hash table is allowed to get before its capacity is automatically increased. When the number of entries in the hash table exceeds the product of the load factor and the current capacity, the hash table is rehashed (that is, internal data 
structures are rebuilt) so that the hash table has approximately twice the number of buckets.

        再来看看这一段,讲的是 Map 的这种实现方式为 get(取)和 put(存)带来了比较好的性能。但是如果涉及到大量的遍历操作的话,就尽量不要把 capacity 设置得太高(或 load factor 设置得太低),否则会严重降低遍历的效率。影响 HashMap 性能的两个重要参数:“initial capacity”(初始化容量)和”load factor“(负载因子)。简单来说,容量就是哈希表桶的个数,负载因子就是键值对个数与哈希表长度的一个比值,当比值超过负载因子之后,HashMap 就会进行 rehash操作来进行扩容。

        HashMap 的大致结构如下图所示,其中哈希表是一个数组,我们经常把数组中的每一个节点称为一个桶,哈希表中的每个节点都用来存储一个键值对。在插入元素时,如果发生冲突(即多个键值对映射到同一个桶上)的话,就会通过链表的形式来解决冲突。因为一个桶上可能存在多个键值对,所以在查找的时候,会先通过 key的哈希值先定位到桶,再遍历桶上的所有键值对,找出 key 相等的键值对,从而来获取 value。

         (因为我这里主要介绍的是 Java 对 HashMap 的实现,而不是 hash 算法,所以如果对哈希算法不了解,可以自行去学习一下!)

JDK 7及以前版本:

        链表散列(数组+链表):首先我们要知道什么是链表散列?通过数组和链表结合在一起使用,就叫做链表散列。

        HashMap的内部存储结构其实是数组和链表的结合。当实例化一个HashMap时,系统会创建一个长度为Capacity的Entry数组,这个长度在哈希表中被称为容量(Capacity),在这个数组中可以存放元素的位置我们称之为“桶”(bucket),每个bucket都有自己的索引,系统可以根据索引快速的查找bucket中的元素。

        每个bucket中存储一个元素,即一个Entry对象,但每一个Entry对象可以带一个引用变量,用于指向下一个元素,因此,在一个桶中,就有可能生成一个Entry链。而且新添加的元素作为链表的head。

        添加元素的过程:
                向HashMap中添加entry1(key,value),需要首先计算entry1中key的哈希值(根据key所在类的hashCode()计算得到),此哈希值经过处理以后,得到在底层Entry[]数组中要存储的位置i。如果位置i上没有元素,则entry1直接添加成功。如果位置i上已经存在entry2(或还有链表存在的entry3,entry4),则需要通过循环的方法,依次比较entry1中key和其他的entry。如果彼此hash值不同,则直接添加成功。如果hash值不同,继续比较二者是否equals。如果返回值为true,则使用entry1的value去替换equals为true的entry的value。如果遍历一遍以后,发现所有的equals返回都为false,则entry1仍可添加成功。entry1指向原有的entry元素。

        HashMap的扩容:
                当HashMap中的元素越来越多的时候,hash冲突的几率也就越来越高,因为数组的长度是固定的。所以为了提高查询的效率,就要对HashMap的数组进行扩容,而在HashMap数组扩容之后,最消耗性能的点就出现了:原数组中的数据必须重新计算其在新数组中的位置,并放进去,这就是resize。

        那么HashMap什么时候进行扩容呢?
                当HashMap中的元素个数超过数组大小(数组总大小length,不是数组中个数size)*loadFactor时,就会进行数组扩容,loadFactor的默认值(DEFAULT_LOAD_FACTOR)为0.75,这是一个折中的取值。也就是说,默认情况下,数组大小(DEFAULT_INITIAL_CAPACITY)为16,那么当HashMap中元素个数超过16*0.75=12(这个值就是代码中的threshold值,也叫做临界值)的时候,就把数组的大小扩展为 2*16=32,即扩大一倍,然后重新计算每个元素在数组中的位置,而这是一个非常消耗性能的操作,所以如果我们已经预知HashMap中元素的个数,那么预设元素的个数能够有效的提高HashMap的性能。

 JDK 8版本发布以后:

        HashMap的内部存储结构其实是数组+链表+树的结合。当实例化一个HashMap时,会初始化initialCapacity和loadFactor,在put第一对映射关系时,系统会创建一个长度为initialCapacity的Node数组,这个长度在哈希表中被称为容量(Capacity),在这个数组中可以存放元素的位置我们称之为“桶”(bucket),每个bucket都有自己的索引,系统可以根据索引快速的查找bucket中的元素。

        每个bucket中存储一个元素,即一个Node对象,但每一个Node对象可以带一个引用变量next,用于指向下一个元素,因此,在一个桶中,就有可能生成一个Node链。也可能是一个一个TreeNode对象,每一个TreeNode对象可以有两个叶子结点left和right,因此,在一个桶中,就有可能生成一个TreeNode树。而新添加的元素作为链表的last,或树的叶子结点。
    
        那么HashMap什么时候进行扩容和树形化呢?
                当HashMap中的元素个数超过数组大小(数组总大小length,不是数组中个数size)*loadFactor 时 , 就会进行数组扩容 , loadFactor 的默认 值(DEFAULT_LOAD_FACTOR)为0.75,这是一个折中的取值。也就是说,默认情况下,数组大小(DEFAULT_INITIAL_CAPACITY)为16,那么当HashMap中元素个数超过16*0.75=12(这个值就是代码中的threshold值,也叫做临界值)时候,就把数组的大小扩展为 2*16=32,即扩大一倍,然后重新计算每个元素在数组中的位置,而这是一个非常消耗性能的操作,所以如果我们已经预知HashMap中元素的个数,那么预设元素的个数能够有效的提高HashMap的性能。

        当HashMap中的其中一个链的对象个数如果达到了8个,此时如果capacity没有达到64,那么HashMap会先扩容解决,如果已经达到了64,那么这个链会变成树,结点类型由Node变成TreeNode类型。当然,如果当映射关系被移除后,下次resize方法时判断树的结点个数低于6个,也会把树再转为链表。

        关于映射关系的key是否可以修改?答案:不要修改。
                映射关系存储到HashMap中会存储key的hash值,这样就不用在每次查找时重新计算每一个Entry或Node(TreeNode)的hash值了,因此如果已经put到Map中的映射关系,再修改key的属性,而这个属性又参与hashcode值的计算,那么会导致匹配不上。

        JDK1.8相较于之前的变化:
                1、HashMap map = new HashMap();//默认情况下,先不创建长度为16的数组。
    
                2、当首次调用map.put()时,再创建长度为16的数组;

                3、数组为Node类型,在jdk7中称为Entry类型;

                4、形成链表结构时,新添加的key-value对在链表的尾部(七上八下);

                5、当数组指定索引位置的链表长度>8时,且map中的数组的长度> 64时,此索引位置上的所有key-value对使用红黑树进行存储。

4.2 属性

        再来看看 HashMap 类中包含了哪些重要的属性,这对下面介绍 HashMap 方法的实现有一定的参考意义。

//默认的初始容量为 16
static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; // aka 16

//最大的容量上限为 2^30
static final int MAXIMUM_CAPACITY = 1 << 30;

//默认的负载因子为 0.75
static final float DEFAULT_LOAD_FACTOR = 0.75f;

//变成树型结构的临界值为 8
static final int TREEIFY_THRESHOLD = 8;

//恢复链式结构的临界值为 6
static final int UNTREEIFY_THRESHOLD = 6;

//哈希表
transient Node<K,V>[] table;

//哈希表中键值对的个数
transient int size;

//哈希表被修改的次数
transient int modCount;

//它是通过 capacity*load factor 计算出来的,当 size 到达这个值时,就会进行扩容操作
int threshold;

//负载因子
final float loadFactor;

//当哈希表的大小超过这个阈值,才会把链式结构转化成树型结构,否则仅采取扩容来尝试减少冲突
static final int MIN_TREEIFY_CAPACITY = 64;

        下面是 Node 类的定义,它是 HashMap 中的一个静态内部类,哈希表中的每一个节点都是 Node 类型。我们可以看到,Node 类中有 4 个属性,其中除了 key 和value 之外,还有 hash 和 next 两个属性。hash 是用来存储 key 的哈希值的,next 是在构建链表时用来指向后继节点的。

static class Node<K,V> implements Map.Entry<K,V> {
    final int hash;
    final K key;
    V value;
    Node<K,V> next;

    Node(int hash, K key, V value, Node<K,V> next) {
        this.hash = hash;
        this.key = key;
        this.value = value;
        this.next = next;
    }

    public final K getKey()        { return key; }
    public final V getValue()      { return value; }
    public final String toString() { return key + "=" + value; }

    public final int hashCode() {
        return Objects.hashCode(key) ^ Objects.hashCode(value);
    }

    public final V setValue(V newValue) {
        V oldValue = value;
        value = newValue;
        return oldValue;
    }

    public final boolean equals(Object o) {
        if (o == this)
            return true;
        if (o instanceof Map.Entry) {
            Map.Entry<?,?> e = (Map.Entry<?,?>)o;
            if (Objects.equals(key, e.getKey()) &&
                Objects.equals(value, e.getValue()))
                return true;
        }
        return false;
    }
}

4.3 方法

4.3.1 get 方法

//get 方法主要调用的是 getNode 方法,所以重点要看 getNode 方法的实现
public V get(Object key) {
    Node<K,V> e;
    return (e = getNode(hash(key), key)) == null ? null : e.value;
}

final Node<K,V> getNode(int hash, Object key) {
    // 临时变量储存tab数组
    Node<K,V>[] tab; 
    // 临时变量获取第一个元素
    // e为临时变量储存在first元素不是所需元素的下一个元素
    Node<K,V> first, e; 
    // n为table的长度
    int n; 
    
    //如果哈希表不为空 && key 对应的桶上不为空
    if ((tab = table) != null && (n = tab.length) > 0 &&
        (first = tab[(n - 1) & hash]) != null) {
        
        //是否直接命中
        if (first.hash == hash && // always check first node
            ((k = first.key) == key || (key != null && key.equals(k))))
            return first;
        
        //判断是否有后续节点
        if ((e = first.next) != null) {
            //如果当前的桶是采用红黑树处理冲突,则调用红黑树的 get 方法去获取节点
            if (first instanceof TreeNode)
                return ((TreeNode<K,V>)first).getTreeNode(hash, key);
            //不是红黑树的话,那就是传统的链式结构了,通过循环的方法判断链中是否存在该 key
            do {
                if (e.hash == hash &&
                    ((k = e.key) == key || (key != null && key.equals(k))))
                    return e;
            } while ((e = e.next) != null);
        }
    }
    return null;
}

实现步骤大致如下:
        1、通过 hash 值获取该 key 映射到的桶。
        2、桶上的 key 就是要查找的 key,则直接命中。
        3、桶上的 key 不是要查找的 key,则查看后续节点:
            (1)如果后续节点是树节点,通过调用树的方法查找该 key。
            (2)如果后续节点是链式节点,则通过循环遍历链查找该 key。

4.3.2 put 方法

//put 方法的具体实现也是在 putVal 方法中,所以我们重点看下面的 putVal 方法
public V put(K key, V value) {
    return putVal(hash(key), key, value, false, true);
}

final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
               boolean evict) {
    //声明了一个局部变量 tab,局部变量 Node 类型的数据 p,int 类型 n,i
    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;
        //如果桶上节点的 key 与当前 key 重复,那你就是我要找的节点了
        if (p.hash == hash &&
            ((k = p.key) == key || (key != null && key.equals(k))))
            e = p;
        //如果是采用红黑树的方式处理冲突,则通过红黑树的 putTreeVal 方法去插入这个键值对
        else if (p instanceof TreeNode)
            e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
        //否则就是传统的链式结构
        else {
            //采用循环遍历的方式,判断链中是否有重复的 key
            for (int binCount = 0; ; ++binCount) {
                //到了链尾还没找到重复的 key,则说明 HashMap 没有包含该键
                if ((e = p.next) == null) {
                    //创建一个新节点插入到尾部
                    p.next = newNode(hash, key, value, null);
                    //如果链的长度大于 TREEIFY_THRESHOLD 这个临界值,则把链变为红黑树
                    if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
                        treeifyBin(tab, hash);
                    break;
                }
                //找到了重复的 key
                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;
        }
    }
    ++modCount;
    //判断是否需要进行扩容
    if (++size > threshold)
        resize();
    afterNodeInsertion(evict);
    return null;
}

put 方法比较复杂,实现步骤大致如下:
        1、先通过 hash 值计算出 key 映射到哪个桶。
        2、如果桶上没有碰撞冲突,则直接插入。
        3、如果出现碰撞冲突了,则需要处理冲突:
            (1)如果该桶使用红黑树处理冲突,则调用红黑树的方法插入。
            (2)否则采用传统的链式方法插入。如果链的长度到达临界值,则把链转变为红黑树。
        4、如果桶中存在重复的键,则为该键替换新值。
        5、如果 size 大于阈值,则进行扩容。

4.3.3 remove 方法

        理解了 put 方法之后,remove 已经没什么难度了,所以重复的内容就不再做详细介绍了。

//remove 方法的具体实现在 removeNode 方法中,所以我们重点看下面的 removeNode 方法
public V remove(Object key) {
    Node<K,V> e;
    return (e = removeNode(hash(key), key, null, false, true)) == null ?
        null : e.value;
}

final Node<K,V> removeNode(int hash, Object key, Object value,
                           boolean matchValue, boolean movable) {
    Node<K,V>[] tab; Node<K,V> p; int n, index;
    
    //如果当前 key 映射到的桶不为空
    if ((tab = table) != null && (n = tab.length) > 0 &&
        (p = tab[index = (n - 1) & hash]) != null) {
        Node<K,V> node = null, e; K k; V v;
        
        //如果桶上的节点就是要找的 key,则直接命中
        if (p.hash == hash &&
            ((k = p.key) == key || (key != null && key.equals(k))))
            node = p;
        else if ((e = p.next) != null) {
            
            //如果是以红黑树处理冲突,则构建一个树节点
            if (p instanceof TreeNode)
                node = ((TreeNode<K,V>)p).getTreeNode(hash, key);
            
            //如果是以链式的方式处理冲突,则通过遍历链表来寻找节点
            else {
                do {
                    if (e.hash == hash &&
                        ((k = e.key) == key ||
                         (key != null && key.equals(k)))) {
                        node = e;
                        break;
                    }
                    p = e;
                } while ((e = e.next) != null);
            }
        }
        
        //比对找到的 key 的 value 跟要删除的是否匹配
        if (node != null && (!matchValue || (v = node.value) == value ||
                             (value != null && value.equals(v)))) {
            
            //通过调用红黑树的方法来删除节点
            if (node instanceof TreeNode)
                ((TreeNode<K,V>)node).removeTreeNode(this, tab, movable);
            
            //使用链表的操作来删除节点
            else if (node == p)
                tab[index] = node.next;
            else
                p.next = node.next;
            ++modCount;
            --size;
            afterNodeRemoval(node);
            return node;
        }
    }
    return null;
}

4.3.4 hash 方法

        在 get 方法和 put 方法中都需要先计算 key 映射到哪个桶上,然后才进行之后的操作,计算的主要代码如下:
                (n - 1) & hash

        上面代码中的 n 指的是哈希表的大小,hash 指的是 key 的哈希值,hash 是通过下面这个方法计算出来的,采用了二次哈希的方式,其中 key 的 hashCode 方法是一个native 方法:

static final int hash(Object key) {
    int h;
    return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}

        这个 hash 方法先通过 key 的 hashCode 方法获取一个哈希值,再拿这个哈希值与它的高 16 位的哈希值做一个异或操作来得到最后的哈希值,计算过程可以参考下图。

        为啥要这样做呢?注释中是这样解释的:如果当 n 很小,假设为 64 的话,那么 n-1即为 63(0x111111),这样的值跟 hashCode()直接做与操作,实际上只使用了哈希值的后 6 位。如果当哈希值的高位变化很大,低位变化很小,这样就很容易造成冲突了,所以这里把高低位都利用起来,从而解决了这个问题。

         正是因为与的这个操作,决定了 HashMap 的大小只能是 2 的幂次方,想一想,如果不是 2 的幂次方,会发生什么事情?即使你在创建 HashMap 的时候指定了初始大小,HashMap 在构建的时候也会调用下面这个方法来调整大小:

static final int tableSizeFor(int cap) {
    int n = cap - 1;
    n |= n >>> 1;
    n |= n >>> 2;
    n |= n >>> 4;
    n |= n >>> 8;
    n |= n >>> 16;
    return (n < 0) ? 1 : (n >= MAXIMUM_CAPACITY) ? MAXIMUM_CAPACITY : n + 1;
}

        这个方法的作用看起来可能不是很直观,它的实际作用就是把 cap 变成第一个大于等于 2 的幂次方的数。例如,16 还是 16,13 就会调整为 16,17 就会调整为 32。

4.3.5 resize 方法

        HashMap 在进行扩容时,使用的 rehash 方式非常巧妙,因为每次扩容都是翻倍,与原来计算(n-1)&hash 的结果相比,只是多了一个 bit 位,所以节点要么就在原来的位置,要么就被分配到“原位置+旧容量”这个位置。

        例如,原来的容量为 32,那么应该拿 hash 跟 31(0x11111)做与操作;在扩容扩到了 64 的容量之后,应该拿 hash 跟 63(0x111111)做与操作。新容量跟原来相比只是多了一个 bit 位,假设原来的位置在 23,那么当新增的那个 bit 位的计算结果为 0时,那么该节点还是在 23;相反,计算结果为 1 时,则该节点会被分配到 23+31 的桶上。

        正是因为这样巧妙的 rehash 方式,保证了 rehash 之后每个桶上的节点数必定小于等于原来桶上的节点数,即保证了 rehash 之后不会出现更严重的冲突。

final Node<K,V>[] resize() {
    // 当前table保存
    Node<K,V>[] oldTab = table;
    // 保存table大小
    int oldCap = (oldTab == null) ? 0 : oldTab.length;
    // 保存当前阈值 
    int oldThr = threshold;
    int newCap, newThr = 0;
    // 之前table大小大于0
    if (oldCap > 0) {
        // 之前table大于最大容量
        if (oldCap >= MAXIMUM_CAPACITY) {
            // 阈值为最大整形
            threshold = Integer.MAX_VALUE;
            return oldTab;
        }
        // 容量翻倍,使用左移,效率更高
        else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY &&
                 oldCap >= DEFAULT_INITIAL_CAPACITY)
            // 阈值翻倍
            newThr = oldThr << 1; // double threshold
    }
    // 之前阈值大于0
    else if (oldThr > 0)
        newCap = oldThr;
    // oldCap = 0并且oldThr = 0,使用缺省值(如使用HashMap()构造函数,之后再插入一个元素会调用resize函数,会进入这一步)
    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"})
    // 初始化table
    Node<K,V>[] newTab = (Node<K,V>[])new Node[newCap];
    table = newTab;
    // 之前的table已经初始化过
    if (oldTab != null) {
        // 复制元素,重新进行hash
        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);
                else { // preserve order
                    Node<K,V> loHead = null, loTail = null;
                    Node<K,V> hiHead = null, hiTail = null;
                    Node<K,V> next;
                    // 将同一桶中的元素根据(e.hash & oldCap)是否为0进行分割,分成两个不同的链表,完成rehash
                    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);
                    if (loTail != null) {
                        loTail.next = null;
                        newTab[j] = loHead;
                    }
                    if (hiTail != null) {
                        hiTail.next = null;
                        newTab[j + oldCap] = hiHead;
                    }
                }
            }
        }
    }
    return newTab;
}

        在这里有一个需要注意的地方,有些文章指出当哈希表的桶占用超过阈值时就进行扩容,这是不对的;实际上是当哈希表中的键值对个数超过阈值时,才进行扩容的。

4.4 总结

        按照原来的拉链法来解决冲突,如果一个桶上的冲突很严重的话,是会导致哈希表的效率降低至 O(n),而通过红黑树的方式,可以把效率改进至 O(logn)。相比链式结构的节点,树型结构的节点会占用比较多的空间,所以这是一种以空间换时间的改进方式.

五、LinkedHashMap 源码分析

5.1 概述

        按照惯例,先看一下源码里的第一段注释:

        Hash table and linked list implementation of the Map interface, with predictable iteration order. This implementation differs from HashMap in that it maintains a doubly-linked list running through all of its entries. This linked list defines the iteration ordering, which is normally the order in which keys were inserted into the map (insertion-order). Note that insertion order is not affected if a key is re-inserted into the map. (A key k is reinserted into a map m if m.put(k, v) is invoked when m.containsKey(k) would return true immediately prior to the invocation.)

        从注释中,我们可以先了解到 LinkedHashMap 是通过哈希表和链表实现的,它通过维护一个链表来保证对哈希表迭代时的有序性,而这个有序是指键值对插入的顺序。另外,当向哈希表中重复插入某个键的时候,不会影响到原来的有序性。也就是说,假设你插入的键的顺序为 1、2、3、4,后来再次插入 2,迭代时的顺序还是 1、2、3、4,而不会因为后来插入的 2 变成 1、3、4、2。(但其实我们可以改变它的规则,使它变成 1、3、4、2)。

        LinkedHashMap 的实现主要分两部分,一部分是哈希表,另外一部分是链表/红黑树。哈希表部分继承了HashMap,拥有了 HashMap 那一套高效的操作,所以我们要看的就是LinkedHashMap 中链表的部分,了解它是如何来维护有序性的。LinkedHashMap 的大致实现如下图所示,当然链表和哈希表中相同的键值对都是指向同一个对象,这里把它们分开来画只是为了呈现出比较清晰的结构。

5.2 属性

        在看属性之前,我们先来看一下 LinkedHashMap 的声明:

public class LinkedHashMap<K,V> extends HashMap<K,V> implements Map<K,V>{}

        从上面的声明中,我们可以看见 LinkedHashMap 是继承自 HashMap 的,所以它已经从 HashMap 那里继承了与哈希表相关的操作了,那么在 LinkedHashMap 中,它可以专注于链表实现的那部分,所以与链表实现相关的属性如下。

//LinkedHashMap 的链表节点继承了 HashMap 的节点,而且每个节点都包含了前指针和后指针,所以这里可以看出它是一个双向链表
static class Entry<K,V> extends HashMap.Node<K,V> {
    Entry<K,V> before, after;
    Entry(int hash, K key, V value, Node<K,V> next) {
        super(hash, key, value, next);
    }
}

//头指针
transient LinkedHashMap.Entry<K,V> head;

//尾指针
transient LinkedHashMap.Entry<K,V> tail;

//默认为 false。当为 true 时,表示链表中键值对的顺序与每个键的插入顺序一致,也就是说重复插入键,也会更新顺序
//简单来说,为 false 时,就是上面所指的 1、2、3、4 的情况;为 true 时,就是 1、3、4、2 的情况
final boolean accessOrder;

5.3 方法

        如果你有仔细看过 HashMap 源码的话,你会发现 HashMap 中有如下三个方法:

// Callbacks to allow LinkedHashMap post-actions
void afterNodeAccess(Node<K,V> p) { }
void afterNodeInsertion(boolean evict) { }
void afterNodeRemoval(Node<K,V> p) { }

        如果你没有注意到注释的解释的话,你可能会很奇怪为什么会有三个空方法,而且有不少地方还调用过它们。其实这三个方法表示的是在访问、插入、删除某个节点之后,进行一些处理,它们在 LinkedHashMap 都有各自的实现。LinkedHashMap 正是通过重写这三个方法来保证链表的插入、删除的有序性。

5.3.1 afterNodeAccess 方法

void afterNodeAccess(Node<K,V> e) { // move node to last
    LinkedHashMap.Entry<K,V> last;
    
    //当 accessOrder 的值为 true,且 e 不是尾节点
    if (accessOrder && (last = tail) != e) {
        LinkedHashMap.Entry<K,V> p =
            (LinkedHashMap.Entry<K,V>)e, b = p.before, a = p.after;
        p.after = null;
        if (b == null)
            head = a;
        else
            b.after = a;
        if (a != null)
            a.before = b;
        else
            last = b;
        if (last == null)
            head = p;
        else {
            p.before = last;
            last.after = p;
        }
        tail = p;
        ++modCount;
    }
}

        这段代码的意思简洁明了,就是把当前节点 e 移至链表的尾部。因为使用的是双向链表,所以在尾部插入可以以 O(1)的时间复杂度来完成。并且只有当 accessOrder设置为 true 时,才会执行这个操作。在 HashMap 的 putVal 方法中,就调用了这个方法。

5.3.2 afterNodeInsertion 方法

void afterNodeInsertion(boolean evict) { // possibly remove eldest
    LinkedHashMap.Entry<K,V> first;
    if (evict && (first = head) != null && removeEldestEntry(first)) {
        K key = first.key;
        removeNode(hash(key), key, null, false, true);
    }
}

        afterNodeInsertion 方法是在哈希表中插入了一个新节点时调用的,它会把链表的头节点删除掉,删除的方式是通过调用 HashMap 的 removeNode 方法。想一想,通过afterNodeInsertion 方法和 afterNodeAccess 方法,是不是就可以简单的实现一个基于最近最少使用(LRU)的淘汰策略了?当然,我们还要重写 removeEldestEntry 方法,因为它默认返回的是 false。

5.3.3 afterNodeRemoval 方法

void afterNodeRemoval(Node<K,V> e) { // unlink
    LinkedHashMap.Entry<K,V> p =
        (LinkedHashMap.Entry<K,V>)e, b = p.before, a = p.after;
    p.before = p.after = null;
    if (b == null)
        head = a;
    else
        b.after = a;
    if (a == null)
        tail = b;
    else
        a.before = b;
}

        这个方法是当 HashMap 删除一个键值对时调用的,它会把在 HashMap 中删除的那个键值对一并从链表中删除,保证了哈希表和链表的一致性。

5.3.4 get 方法

public V get(Object key) {
    Node<K,V> e;
    if ((e = getNode(hash(key), key)) == null)
        return null;
    if (accessOrder)
        afterNodeAccess(e);
    return e.value;
}

        你没看错,LinkedHashMap 的 get 方法就是这么简单,因为它调用的是 HashMap 的getNode 方法来获取结果的。并且,如果你把 accessOrder 设置为 true,那么在获取到值之后,还会调用 afterNodeAccess 方法。这样是不是就能保证一个 LRU 的算法了?

5.3.5 put 方法和 remove 方法

        我在 LinkedHashMap 的源码中没有找到 put 方法,这就说明了它并没有重写 put 方法,所以我们调用的 put 方法其实是 HashMap 的 put 方法。因为 HashMap 的 put 方法中调用了 afterNodeAccess 方法和 afterNodeInsertion 方法,已经足够保证链表的有序性了,所以它也就没有重写 put 方法了。remove 方法也是如此。

六、Hashtable 源码分析

6.1 概述

        介绍了 Java8 的 HashMap,接下来准备介绍一下 Hashtable。Hashtable 可以说已经具有一定的历史了,现在也很少使用到Hashtable 了,更多的是使用 HashMap 或 ConcurrentHashMap。HashTable 是一个线程安全的哈希表,它通过使用synchronized 关键字来对方法进行加锁,从而保证了线程安全。但这也导致了在单线程环境中效率低下等问题。Hashtable 与 HashMap 不同,它不允许插入 null 值和 null 键。

6.2 属性

        Hashtable 并没有像 HashMap 那样定义了很多的常量,而是直接写死在了方法里(看下去就知道了),所以它的属性相比 HashMap 来说,可以获取的信息还是比较少的。

//哈希表
private transient Entry<?,?>[] table;

//记录哈希表中键值对的个数
private transient int count;

//扩容的阈值
private int threshold;

//负载因子
private float loadFactor;

6.3 方法

6.3.1 构造方法

public Hashtable() {
    this(11, 0.75f);
}

public Hashtable(int initialCapacity) {
    this(initialCapacity, 0.75f);
}

public Hashtable(int initialCapacity, float loadFactor) {
    if (initialCapacity < 0)
        throw new IllegalArgumentException("Illegal Capacity: "+
                                           initialCapacity);
    if (loadFactor <= 0 || Float.isNaN(loadFactor))
        throw new IllegalArgumentException("Illegal Load: "+loadFactor);

    if (initialCapacity==0)
        initialCapacity = 1;
    this.loadFactor = loadFactor;
    table = new Entry<?,?>[initialCapacity];
    threshold = (int)Math.min(initialCapacity * loadFactor, MAX_ARRAY_SIZE + 1);
}

        二话不说,上来先丢了三个构造函数。从构造函数中,我们可以获取到这些信息:Hashtable 默认的初始化容量为 11(与 HashMap 不同),负载因子默认为 0.75(与 HashMap相同)。而正因为默认初始化容量的不同,同时也没有对容量做调整的策略,所以可以先推断出,Hashtable 使用的哈希函数跟 HashMap 是不一样的(事实也确实如此)。

6.3.2 get 方法

public synchronized V get(Object key) {
    Entry<?,?> tab[] = table;
    int hash = key.hashCode();
    
    //通过哈希函数,计算出 key 对应的桶的位置
    int index = (hash & 0x7FFFFFFF) % tab.length;
    
    //遍历该桶的所有元素,寻找该 key
    for (Entry<?,?> e = tab[index] ; e != null ; e = e.next) {
        if ((e.hash == hash) && e.key.equals(key)) {
            return (V)e.value;
        }
    }
    return null;
}

        跟 HashMap 相比,Hashtable 的 get 方法非常简单。我们首先可以看见 get 方法使用了synchronized 来修饰,所以它能保证线程安全。并且它是通过链表的方式来处理冲突的。另外,我们还可以看见 HashTable 并没有像 HashMap 那样封装一个哈希函数,而是直接把哈希函数写在了方法中。而哈希函数也是比较简单的,它仅对哈希表的长度进行了取模。

6.3.3 put 方法

public synchronized V put(K key, V value) {
    // Make sure the value is not null
    if (value == null) {
        throw new NullPointerException();
    }

    // Makes sure the key is not already in the hashtable.
    Entry<?,?> tab[] = table;
    int hash = key.hashCode();

    //计算桶的位置
    int index = (hash & 0x7FFFFFFF) % tab.length;
    @SuppressWarnings("unchecked")
    Entry<K,V> entry = (Entry<K,V>)tab[index];

    //遍历桶中的元素,判断是否存在相同的 key
    for(; entry != null ; entry = entry.next) {
        if ((entry.hash == hash) && entry.key.equals(key)) {
            V old = entry.value;
            entry.value = value;
            return old;
        }
    }

    //不存在相同的 key,则把该 key 插入到桶中
    addEntry(hash, key, value, index);
    return null;
}

private void addEntry(int hash, K key, V value, int index) {
    modCount++;

    Entry<?,?> tab[] = table;
    
    //哈希表的键值对个数达到了阈值,则进行扩容
    if (count >= threshold) {
        // Rehash the table if the threshold is exceeded
        rehash();

        tab = table;
        hash = key.hashCode();
        index = (hash & 0x7FFFFFFF) % tab.length;
    }

    // Creates the new entry.
    @SuppressWarnings("unchecked")
    Entry<K,V> e = (Entry<K,V>) tab[index];
    //把新节点插入桶中(头插法)
    tab[index] = new Entry<>(hash, key, value, e);
    count++;
}

        put 方法一开始就表明了不能有 null 值,否则就会向你抛出一个空指针异常。Hashtable的 put 方法也是使用 synchronized 来修饰。你可以发现,在 Hashtable 中,几乎所有的方法都使用了 synchronized 来保证线程安全。

6.3.4 remove 方法

public synchronized V remove(Object key) {
    Entry<?,?> tab[] = table;
    int hash = key.hashCode();
    int index = (hash & 0x7FFFFFFF) % tab.length;
    @SuppressWarnings("unchecked")
    Entry<K,V> e = (Entry<K,V>)tab[index];
    for(Entry<K,V> prev = null ; e != null ; prev = e, e = e.next) {
        if ((e.hash == hash) && e.key.equals(key)) {
            modCount++;
            if (prev != null) {
                prev.next = e.next;
            } else {
                tab[index] = e.next;
            }
            count--;
            V oldValue = e.value;
            e.value = null;
            return oldValue;
        }
    }
    return null;
}

        remove 方法我已经不想加注释了,跟 get 和 put 的原理差不多。如果看过上一篇的HashMap 的话,或者理解了上面的 put 方法的话,我相信 remove 方法看一眼就能懂了。

6.3.5 rehash 方法

protected void rehash() {
    int oldCapacity = table.length;
    Entry<?,?>[] oldMap = table;

    // overflow-conscious code
    //扩容扩为原来的两倍+1
    int newCapacity = (oldCapacity << 1) + 1;
    
    //判断是否超过最大容量
    if (newCapacity - MAX_ARRAY_SIZE > 0) {
        if (oldCapacity == MAX_ARRAY_SIZE)
            // Keep running with MAX_ARRAY_SIZE buckets
            return;
        newCapacity = MAX_ARRAY_SIZE;
    }
    Entry<?,?>[] newMap = new Entry<?,?>[newCapacity];

    modCount++;
    //计算下一次 rehash 的阈值
    threshold = (int)Math.min(newCapacity * loadFactor, MAX_ARRAY_SIZE + 1);
    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;
        }
    }
}

        Hashtable 的 rehash 方法相当于 HashMap 的 resize 方法。跟 HashMap 那种巧妙的 rehash方式相比,Hashtable 的 rehash 过程需要对每个键值对都重新计算哈希值,而比起异或和与操作,取模是一个非常耗时的操作,所以这也是导致效率较低的原因之一。

七、其他

7.1 modCount的作用

        https://www.cnblogs.com/zh-ch/p/12727250.html

        https://www.zhihu.com/question/24086463

        https://www.cnblogs.com/zuochengsi-9/p/7050351.html

7.2 transient关键字

        https://baijiahao.baidu.com/s?id=1636557218432721275&wfr=spider&for=pc

        https://blog.csdn.net/u012723673/article/details/80699029

  • 3
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

我是波哩个波

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值