Java集合源码分析(ArrayList、LinkedList、Vector和HashMap)

一、ArrayList源码分析

  • 继承结构与接口实现
    在这里插入图片描述
  1. JDK 8.0中ArrayList的变化:

     ArrayList list = new ArrayList();//底层Object[] elementData初始化为{}.并没创建长度为10的数组
     list.add(123);//第一次调用add()时,底层才创建了长度10的数组,并将数据123添加到elementData[0]
    

源码解析:

    //无参构造方法
    public ArrayList() {
    
    
        //创建一个 空的 ArrayList,此时其内数组缓冲区 elementData = {}, 长度为 0
        //当元素第一次被加入时,扩容至默认容量 10
        this.elementData = DEFAULTCAPACITY_EMPTY_ELEMENTDATA;
    }

    //创建一个初试容量的、空的ArrayList
    public ArrayList(int initialCapacity) {
    
    
        if (initialCapacity > 0) {
    
    //当初始容量值(小于0)时抛出IllegalArgumentException
            this.elementData = new Object[initialCapacity];
        } else if (initialCapacity == 0) {
    
    
            this.elementData = EMPTY_ELEMENTDATA;
        } else {
    
    
            throw new IllegalArgumentException("Illegal Capacity: "+
                    initialCapacity);
        }
    }


    public boolean add(E e) {
    
    
        确保对象数组elementData有足够的容量,可以将新加入的元素e加进去
        ensureCapacityInternal(size + 1);  // Increments modCount!!
        //在数据中正确的位置上放上元素e,并且size++
        elementData[size++] = e;
        return true;
    }
    //确定内部容量的方法 
    private void ensureCapacityInternal(int minCapacity) {
    
    
        if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA) {
    
    //判断初始化的elementData是不是空的数组,也就是没有长度
            //minCapacity = size+1,DEFAULT_CAPACITY = 10,所以将minCapacity变成10
            minCapacity = Math.max(DEFAULT_CAPACITY, minCapacity);
        }
        ensureExplicitCapacity(minCapacity);
    }
    //判断elementData是否够用
    private void ensureExplicitCapacity(int minCapacity) {
    
    
        modCount++;
        // overflow-conscious code
        if (minCapacity - elementData.length > 0)
            grow(minCapacity);//不够用,进行扩容(自动扩容的关键方法)
    }

    private void grow(int minCapacity) {
    
    
        // overflow-conscious code
        int oldCapacity = elementData.length;
        int newCapacity = oldCapacity + (oldCapacity >> 1);newCapacity就是1.5倍的oldCapacity
        if (newCapacity - minCapacity < 0)//如果扩大1.5倍还不行,则进行大容量分配
            newCapacity = minCapacity;
        if (newCapacity - MAX_ARRAY_SIZE > 0)
            newCapacity = hugeCapacity(minCapacity);
        // minCapacity is usually close to size, so this is a win:
        elementData = Arrays.copyOf(elementData, newCapacity);
    }

    private static int hugeCapacity(int minCapacity) {
    
    
        if (minCapacity < 0) // overflow
            throw new OutOfMemoryError();
        return (minCapacity > MAX_ARRAY_SIZE) ?
                Integer.MAX_VALUE :
                MAX_ARRAY_SIZE;
    }
  • 两个常用构造方法分析:
    在这里插入图片描述
  • add()方法
    在这里插入图片描述
  1. JDK 7.0情况下
  • ArrayList list = new ArrayList();//底层创建了长度是10的Object[]数组elementData
    在这里插入图片描述
  • 不再判断初始化的elementData是不是空的数组了(相比JDK 8.0)
    在这里插入图片描述
    后续的添加和扩容操作与JDK 8.0 无异

小结:

jdk7中的ArrayList的对象的创建类似于单例的饿汉式,而jdk8中的ArrayList的对象的创建类似于单例的懒汉式,延迟了数组的创建,节省内存。

二、LinkedList源码分析

  • 继承结构与接口实现
    在这里插入图片描述

      LinkedList 是一个继承于AbstractSequentialList的双向链表。它也可以被当作堆栈、队列或双端队列进行操作。
      LinkedList 实现 List 接口,能对它进行队列操作。
      LinkedList 实现 Deque 接口,即能将LinkedList当作双端队列使用。
      LinkedList 实现了Cloneable接口,即覆盖了函数clone(),能克隆。
      LinkedList 实现java.io.Serializable接口,这意味着LinkedList支持序列化,能通过序列化去传输。
      LinkedList 是非同步的
    
  1. LinkedList的属性
//实际元素个数
transient int size = 0;
//头结点
transient Node<E> first;
//尾结点
transient Node<E> last;
  • 静态内存类
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;
    }
}
  1. 两个构造方法
//无参构造方法
public LinkedList() {
    
    
 }
 //有参构造方法
 public LinkedList(Collection<? extends E> c) {
    
    
     this();
     //添加集合中的所有元素
     addAll(c);
 }
  1. 添加操作分析

1)add(E e)方法
add(E e)用于将元素添加到链表尾部

public boolean add(E e) {
    
    
    //添加到末尾
    linkLast(e);
    return true;
}
//Links e as last element.

void linkLast(E e) {
    
    
    final Node<E> l = last;//临时节点l保存last,也就是l指向了最后一个节点
    final Node<E> newNode = new Node<>(l, e, null);
    last = newNode;
    if (l == null)//如果链表为空,则newNode就成为了第一个节点,first和last都要指向它
        first = newNode;
    else //否则,在最后一个节点进行追加
        l.next = newNode;
    size++;//添加一个节点,size自增
    modCount++;
}

2)add(int index,E e)方法
add(int index,E e)用于在指定位置添加元素

public void add(int index, E element) {
    
    
    //检查索引是否处于[0-size]之间
    checkPositionIndex(index);

    if (index == size)//添加到链表尾部
        linkLast(element);
    else//添加到链表中间
        linkBefore(element, node(index));
}
//返回idx指定位置的节点
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;
    }
}

void linkBefore(E e, Node<E> succ) {
    
    
    // assert succ != null;
    final Node<E> pred = succ.prev;
    //新节点的前驱指向pred,后继为succ
    final Node<E> newNode = new Node<>(pred, e, succ);
    succ.prev = newNode;
    if (pred == null)//如果该节点插入在头结点之前,重置first头结点
        first = newNode;
    else
        pred.next = newNode;
    size++;
    modCount++;
}

总结:

add(int index,E e)方法:
	1. 检查index的范围,否则抛出异常
	2. 如果插入位置是链表尾部,那么调用linkLast方法
	3. 如果插入位置是链表中间,那么调用linkBefore方法
linkBefore()方法
	1. 创建newNode节点,将newNode的后继指针指向succ,前驱指针指向pred
	2. 将succ的前驱指针指向newNode
	3. 根据pred是否为null,进行不同操作。
	- 如果pred为null,说明该节点插入在头节点之前,要重置first头节点
	- 如果pred不为null,那么直接将pred的后继指针指向newNode即可
  • arrayList和LinkedList区别

arrayList底层是用数组实现的顺序表,是随机存取类型,可自动扩增,并且在初始化时,数组的长度是0,只有在增加元素时,长度才会增加。默认是10,不能无限扩增,有上限,在查询操作的时候性能更好
LinkedList底层是用链表来实现的,是一个双向链表,注意这里不是双向循环链表,顺序存取类型。在源码中,似乎没有元素个数的限制。应该能无限增加下去,直到内存满了在进行删除,增加操作时性能更好。

分析不够全面(addAll()待补)

三、Vector源码分析

  • 继承结构与接口实现
    在这里插入图片描述

Vector是一个古老的集合,JDK 1.0就有了。大多数操作与ArrayList相同,区别在于Vector是线程安全的
在各种list中,最好把ArrayList作为缺省选择。当插入、删除频繁时,使用LinkedList;Vector总是比ArrayList慢,所以尽量避免选择使用。
JDK 7.0和JDK 8.0中通过Vector()构造器创建对象时,底层都创建了长度为10的数组
在扩容方面,默认扩容为原来的数组长度的2倍

  1. 构造方法
public Vector() {
    
    
    this(10);
}

//设置初始化容量,默认当由于增加数据导致容量增加时,每次容量会增加一倍
public Vector(int initialCapacity) {
    
    
    this(initialCapacity, 0);
}
//capacity是Vector的默认容量大小,capacityIncrement是每次Vector容量增加时的增量值。
public Vector(int initialCapacity, int capacityIncrement) {
    
    
    super();
    if (initialCapacity < 0)
        throw new IllegalArgumentException("Illegal Capacity: "+
                initialCapacity);
    //新建一个数组,数组容量是initialCapacity
    this.elementData = new Object[initialCapacity];
    //设置增长容量增长系数,如果该值为0,那数组就变为两倍的原长度
    this.capacityIncrement = capacityIncrement;
}

public Vector(Collection<? extends E> c) {
    
    
    elementData = c.toArray();
    elementCount = elementData.length;
    // c.toArray might (incorrectly) not return Object[] (see 6260652)
    if (elementData.getClass() != Object[].class)
        elementData = Arrays.copyOf(elementData, elementCount, Object[].class);
}
  1. add()方法
public synchronized boolean add(E e) {
    
    
    modCount++;
    //增加元素前,检查容量是否够用
    ensureCapacityHelper(elementCount + 1);
    elementData[elementCount++] = e;
    return true;
}

private void ensureCapacityHelper(int minCapacity) {
    
    
    // overflow-conscious code
    if (minCapacity - elementData.length > 0)
        //容量不够,就扩增,核心方法
        grow(minCapacity);
}

//进行数组扩容
private void grow(int minCapacity) {
    
    
    // overflow-conscious code
    int oldCapacity = elementData.length;
    //如果capacityIncrement不为0,那么增长的长度就是capacityIncrement,如果为0,那么扩增为2倍的原容量
    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);
}

四、HashMap源码分析

在这里插入图片描述
JDK1.8之前,HashMap的底层数据结构是数组+链表,数组中的每个元素称为一个Entry,包含(hash,key,value,next)这四个元素,其中链表是用来解决碰撞(冲突)的,如果hash值相同即对应于数组中同一一个下标,此时会利用链表将元素插入链表的尾部,(JDK1.8是头插法)。在JDK1.8及之后,底层的数据结构是:数组+(链表,红黑树),引入红黑树是为了避免链表过长,影响元素值的查找,因此当整体的数组大小大于64时,并且链表的长度大于或等于8时,会把链表转化成红黑树。

JDK1.7源码分析

  1. 底层存储结构:
    在这里插入图片描述
static class Node<K,V> implements Map.Entry<K,V> {
    
    
        final int hash;
        final K key;
        V value;
        Node<K,V> next;
}
  1. HashMap属性和构造方法
//初始默认数组的大小
static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; // aka 16
//最大容量
static final int MAXIMUM_CAPACITY = 1 << 30;
//默认的负载因子,表示当map集合中存储的数据达到当前数组大小的75%则需要进行扩容
static final float DEFAULT_LOAD_FACTOR = 0.75f;
// 存储元素的数组,总是2的幂次倍
transient Node<k,v>[] table; 
// 存放元素的个数,注意这个不等于数组的长度。
transient int size;
// 每次扩容和更改map结构的计数器
transient int modCount;   
// 临界值 当实际大小(容量*填充因子)超过临界值时,会进行扩容
int threshold;
//构造函数 1
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();
}

//构造函数 2
public HashMap(int initialCapacity) {
    
    
    //调用构造函数 1
    this(initialCapacity, DEFAULT_LOAD_FACTOR);
}

//构造函数 3
public HashMap() {
    
    
    //调用构造函数 1
    this(DEFAULT_INITIAL_CAPACITY, DEFAULT_LOAD_FACTOR);  
}

分析:

对于HashMap对的构造函数有三种,我们通常使用的构造函数 3 ,载荷因子表示当存储容量达到75%时需要对数组进行扩容。当选择 构造函数 2 和 构造函数 3 时,最终都会走构造函数1。
        构造函数 1 :能够设置初始化数组长度,及载荷因子。
        构造函数 2 :能够设置初始化数组长度,对于载荷因子默认为0.75。 会去调构造函数 1
        构造函数 3 :默认初始化数组长度为16,载荷因子为0.75。   会去调构造函数 1
  1. put方法

1)put(K key,V value),注意一个问题,map是允许存储key=null且value=null的,而hashTable则不允许

public V put(K key, V value) {
    
    

    if (table == EMPTY_TABLE) {
    
    
        inflateTable(threshold);
    }
    //如果key==null,也放进去
    if (key == null)
        return putForNullKey(value);
    //计算key的hash值
    int hash = hash(key);
    int i = indexFor(hash, table.length);
    //遍历table[i]位置上的链表
    for (Entry<K,V> e = table[i]; e != null; e = e.next) {
    
    
        Object k;
        //如果元素相同,则将value更新并返回oldValue(元素相同:hash值相同并且equals相同)
        if (e.hash == hash && ((k = e.key) == key || key.equals(k))) {
    
    
            V oldValue = e.value;
            e.value = value;
            e.recordAccess(this);
            return oldValue;
        }
    }
    modCount++;
    //如果table[i]上没有对应的key,则进行新添一个entry对象
    addEntry(hash, key, value, i);
    return null;
}

2)void addEntry(int hash, K key, V value, int bucketIndex)

void addEntry(int hash, K key, V value, int bucketIndex) {
    
    
    //如果size>=临界值并且要存放的桶的位置非空时
    if ((size >= threshold) && (null != table[bucketIndex])) {
    
    
        //扩容操作,默认情况下扩容为原来的2倍,扩容的同时重现计算
        resize(2 * table.length);
        hash = (null != key) ? hash(key) : 0;
        bucketIndex = indexFor(hash, table.length);
    }
    createEntry(hash, key, value, bucketIndex);
}
void createEntry(int hash, K key, V value, int bucketIndex) {
    
    
        //头插法插入新元素
        Entry<K,V> e = table[bucketIndex];
        table[bucketIndex] = new Entry<>(hash, key, value, e);
        size++;
    }

JDK1.8源码分析

HashMap原理总结(重要)

    HashMap是基于拉链法实现的一个散列表,内部由数组和链表实现。
    数组的初始容量为16,而容量是以2的次方扩充的,一是为了提高性能使用足够大的数组,二是为了能使用位运算代替取模预算。
    数组是否需要扩充是通过负载因子判断的,如果当前元素个数为数组容量的0.75时,就会扩充数组。这个0.75就是默认的负载因子,可由构造传入。我们也可以设置大于1的负载因子,这样数组就不会扩充,牺牲性能,节省内存。
    为了解决碰撞,数组中的元素是单向链表类型。当链表长度到达一个阈值时(7或8),会将链表转换成红黑树提高性能。而当链表长度缩小到另一个阈值时(6),又会将红黑树转换回单向链表提高性能,这里是一个平衡点。
    对于第三点补充说明,检查链表长度转换成红黑树之前,还会先检测当前数组数组是否到达一个阈值(64),如果没有到达这个容量,会放弃转换,先去扩充数组。所以上面也说了链表长度的阈值是7或8,因为会有一次放弃转换的操作。

  1. 底层存储结构:
    在这里插入图片描述

  2. HashMap属性和构造方法:


// 当桶(bucket)上的结点数大于这个值时会转成红黑树
static final int TREEIFY_THRESHOLD = 8; 
// 当桶(bucket)上的结点数小于这个值时树转链表
static final int UNTREEIFY_THRESHOLD = 6;
// 桶中结构转化为红黑树对应的table的最小大小
static final int MIN_TREEIFY_CAPACITY = 64;

public HashMap(int initialCapacity, float loadFactor) {
    
    
    //校验 小于0报错
    if (initialCapacity < 0)
        throw new IllegalArgumentException("Illegal initial capacity: " +
                                           initialCapacity);
    //capacity大于最大值取最大值
    if (initialCapacity > MAXIMUM_CAPACITY)
        initialCapacity = MAXIMUM_CAPACITY;
    //负载因子不能小于等于0
    if (loadFactor <= 0 || Float.isNaN(loadFactor))
        throw new IllegalArgumentException("Illegal load factor: " +
                                           loadFactor);
    this.loadFactor = loadFactor;
    //tableSizeFor方法
    this.threshold = tableSizeFor(initialCapacity);
}

---------------------------------------------------------
//传入一个初始容量,默认负载因子0.75
public HashMap(int initialCapacity) {
    
    
    this(initialCapacity, DEFAULT_LOAD_FACTOR);
}
---------------------------------------------------------
//无参数,负载因子默认0.75
public HashMap() {
    
    
    this.loadFactor = DEFAULT_LOAD_FACTOR; // all other fields defaulted
}
---------------------------------------------------------
//传入一个map的对象
public HashMap(Map<? extends K, ? extends V> m) {
    
    
    this.loadFactor = DEFAULT_LOAD_FACTOR;
    putMapEntries(m, false);
}

TreeNode是Node是子类,继承关系如下:Node是单向链表节点,Entry是双向链表节点,TreeNode是红黑树节点
2. put方法

put()执行过程图解:
在这里插入图片描述
1)put(K key, V value)方法

public V put(K key, V value) {
    
    
	return putVal(hash(key), key, value, false, true);
}

2)putVal(int hash, K key, V value, boolean onlyIfAbsent, boolean evict)方法

final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
               boolean evict) {
    
    
    Node<K,V>[] tab; Node<K,V> p; int n, i;
    //如果当前table未初始化或者长度为0,进行扩容
    if ((tab = table) == null || (n = tab.length) == 0)
        n = (tab = resize()).length;//进行扩容,如果是首次添加,底层构建一个容量为16的数组
    //计算数组索引,获取该索引位置的首节点,如果为null,添加一个新的节点
    if ((p = tab[i = (n - 1) & hash]) == null)
        tab[i] = newNode(hash, key, value, null);
    else {
    
    
        //桶中已经存在元素
        Node<K,V> e; K k;
        // 如果首节点的key和要存入的key相同,那么直接覆盖value的值。
        if (p.hash == hash && ((k = p.key) == key || (key != null && key.equals(k))))
            e = p;
        // 如果首节点是红黑树的,将键值对插添加到红黑树
        else if (p instanceof TreeNode)
            e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
        else {
    
    
            // 此时首节点为链表,如果链表中存在该键值对,直接覆盖value。
            // 如果不存在,则在末端插入键值对。然后判断链表是否大于等于7,尝试转换成红黑树。
            // 注意此处使用“尝试”,因为在treeifyBin方法中还会判断当前数组容量是否到达64,
            // 否则会放弃次此转换,优先扩充数组容量。
            for (int binCount = 0; ; ++binCount) {
    
    
                if ((e = p.next) == null) {
    
    
                    //当p的next==null时,将新元素放到尾部元素的下一个
                    p.next = newNode(hash, key, value, null);
                    if (binCount >= TREEIFY_THRESHOLD - 1) //当链表上的元素个数大于等于7时,尝试转换成红黑树
                        treeifyBin(tab, hash);
                    break;
                }
                //如果在链表中找到相同的元素,跳出循环;
                if (e.hash == hash && ((k = e.key) == key || (key != null && key.equals(k))))
                    break;
                p = e;//p指向下一个元素
            }
        }
        if (e != null) {
    
    
            //对value进行更新操作,同时返回oldValue
            V oldValue = e.value;
            if (!onlyIfAbsent || oldValue == null)
                e.value = value;
            afterNodeAccess(e);
            return oldValue;
        }
    }
    ++modCount;// fail-fast机制
    // 如果元素个数大于阈值,扩充数组。
    if (++size > threshold)
        resize();
    afterNodeInsertion(evict);
    return null;
}

总结:

检查数组是否为空,执行resize()扩充;
通过hash值计算数组索引,获取该索引位的首节点。
如果首节点为null,直接添加节点到该索引位。
如果首节点不为null,那么有3种情况
	① key和首节点的key相同,覆盖value;否则执行②或③
	② 如果首节点是红黑树节点(TreeNode),将键值对添加到红黑树。
	③ 如果首节点是链表,将键值对添加到链表。添加之后会判断链表长度是否到达TREEIFY_THRESHOLD - 1这个阈值,“尝试”将链表转换成红黑树。
最后判断当前元素个数是否大于threshold,扩充数组。

3)resize() 方法

final HashMap.Node<K,V>[] resize() {
    
    
    HashMap.Node<K,V>[] oldTab = table;
    int oldCap = (oldTab == null) ? 0 : oldTab.length;
    int oldThr = threshold;
    int newCap, newThr = 0;
    if (oldCap > 0) {
    
    
        // 如果数组已经是最大长度,不进行扩充。
        if (oldCap >= MAXIMUM_CAPACITY) {
    
    
            threshold = Integer.MAX_VALUE;
            return oldTab;
        }
        // 否则数组容量扩充一倍。(2的N次方)
        else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY &&
                oldCap >= DEFAULT_INITIAL_CAPACITY)
            newThr = oldThr << 1; // double threshold
    }
    // 如果数组还没创建,但是已经指定了threshold(这种情况是带参构造创建的对象),threshold的值为数组长度
    // 在 "构造函数" 那块内容进行过说明。
    else if (oldThr > 0) // initial capacity was placed in threshold
        newCap = oldThr;
    // 这种情况是通过无参构造创建的对象
    else {
    
                   // zero initial threshold signifies using defaults
        newCap = DEFAULT_INITIAL_CAPACITY;
        newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY);
    }
    // 可能是上面newThr = oldThr << 1时,最高位被移除了,变为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"})
    HashMap.Node<K,V>[] newTab = (HashMap.Node<K,V>[])new HashMap.Node[newCap];
    table = newTab;
    
    // 下面代码是将原来数组的元素转移到新数组中。问题在于,数组长度发生变化。 
    // 那么通过hash%数组长度计算的索引也将和原来的不同。
    // jdk 1.7中是通过重新计算每个元素的索引,重新存入新的数组,称为rehash操作。
    // 这也是hashMap无序性的原因之一。而现在jdk 1.8对此做了优化,非常的巧妙。
    if (oldTab != null) {
    
    
        
        // 遍历原数组
        for (int j = 0; j < oldCap; ++j) {
    
    
            // 取出首节点
            HashMap.Node<K,V> e;
            if ((e = oldTab[j]) != null) {
    
    
                oldTab[j] = null;
                // 如果链表只有一个节点,那么直接重新计算索引存入新数组。
                if (e.next == null)
                    newTab[e.hash & (newCap - 1)] = e;
                // 如果该节点是红黑树,执行split方法,和链表类似的处理。
                else if (e instanceof HashMap.TreeNode)
                    ((HashMap.TreeNode<K,V>)e).split(this, newTab, j, oldCap);
                
                // 此时节点是链表
                else {
    
     // preserve order
                    // loHead,loTail为原链表的节点,索引不变。
                    HashMap.Node<K,V> loHead = null, loTail = null;
                    // hiHeadm, hiTail为新链表节点,原索引 + 原数组长度。
                    HashMap.Node<K,V> hiHead = null, hiTail = null;
                    HashMap.Node<K,V> next;
                    
                   // 遍历链表
                    do {
    
    
                        next = e.next;
                        // 新增bit为0的节点,存入原链表。
                        if ((e.hash & oldCap) == 0) {
    
    
                            if (loTail == null)
                                loHead = e;
                            else
                                loTail.next = e;
                            loTail = e;
                        }
                        // 新增bit为1的节点,存入新链表。
                        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;
}

对于扩充数组:

因为元素的索引是通过hash&(n - 1)得到的,那么数组的长度由n变为2n,重新计算的索引就可能和原来的不一样了。
在jdk1.7中,是通过遍历每一个元素,每一个节点,重新计算他们的索引值,存入新的数组中,称为rehash操作
而java1.8对此进行了一些优化l。

猜你喜欢

转载自blog.csdn.net/weixin_44630656/article/details/113485237