集合源码详解

集合源码详解

集合API源码分析

程序=数据结构+算法

集合概述

集合是存储数据的容器

image.png

image.png

ArrayList详解

类图结构

首先我们来看下ArrayList的类图结构

image.png

源码分析

声明的属性
属性作用
DEFAULT_CAPACITY默认的数组容量
EMPTY_ELEMENTDATA用于共享的空实例数组
DEFAULTCAPACITY_EMPTY_ELEMENTDATA也是一个空的实例数组
elementData这个是ArrayList中真实存储数据的容器
size记录集合中的元素的个数
构造方法
public ArrayList() {
        this.elementData = DEFAULTCAPACITY_EMPTY_ELEMENTDATA;
    }

public ArrayList(int initialCapacity) {
    if (initialCapacity > 0) {
        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) {
    // 将传递的集合转换为数组 然后赋值给了 elementData
    elementData = c.toArray();
    if ((size = elementData.length) != 0) {
        // c.toArray might (incorrectly) not return Object[] (see 6260652)
        if (elementData.getClass() != Object[].class)
            elementData = Arrays.copyOf(elementData, size, Object[].class);
    } else {
        // replace with empty array.
        this.elementData = EMPTY_ELEMENTDATA;
    }
}
添加方法
方法名描述
public boolean add(E e)将指定的元素追加到此列表的末尾。
public void add(int index, E element)在此列表中的指定位置插入指定的元素。
public boolean addAll(Collection<?> c )按指定集合的Iterator返回的顺序将指定集合中的所有元素追加到此列表的末尾。
public boolean addAll(i nt index,Collection<? extends E> c)将指定集合中的所有元素插入到此列表中,从指定的位置开始。

add(E e)

    public boolean add(E e) {
        // 校验内部容量,判断是否需要扩容
        ensureCapacityInternal(size + 1);  // Increments modCount!!
        elementData[size++] = e;//size++位置上添加数据
        return true;
    }
    private void ensureCapacityInternal(int minCapacity) {
        // calculateCapacity计算容量
        // ensureExplicitCapacity 继续校验
        ensureExplicitCapacity(calculateCapacity(elementData, minCapacity));
    }

calculateCapacity方法

    private static int calculateCapacity(Object[] elementData, int minCapacity) {
        // 判断集合存数据的数组是否等于空容量的数组,通过无参构造器创建的对象,插入第一个元素
        if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA) {
            // 通过最小容量[第一次就是1]和默认容量[10] 求出较大值 (用于第一次扩容)
            return Math.max(DEFAULT_CAPACITY, minCapacity);//返回10
        }
        return minCapacity;
    }

ensureExplicitCapacity方法

private void ensureExplicitCapacity(int minCapacity) { 
    //实际修改集合次数++ (在扩容的过程中没用,主要是用于迭代器中) 
    modCount++; 
    //判断最小容量 - 数组长度是否大于 0 
    if (minCapacity - elementData.length > 0) 
        //将第一次计算出来的容量传递给 核心扩容方法 
        grow(minCapacity); 
}
private void grow(int minCapacity) { 
    //记录数组的实际长度,此时由于没有存储元素,长度为0 
    int oldCapacity = elementData.length; 
    //核心扩容算法 原容量的1.5倍 
    int newCapacity = oldCapacity + (oldCapacity >> 1); 
    //判断新容量 - 最小容量 是否小于 0, 如果是第一次调用add方法必然小于 
    if (newCapacity - minCapacity < 0) 
        //还是将最小容量赋值给新容量 
        newCapacity = minCapacity; 
    //判断新容量-最大数组大小 是否>0,如果条件满足就计算出一个超大容量 
    if (newCapacity - MAX_ARRAY_SIZE > 0) newCapacity = hugeCapacity(minCapacity); 
    // 调用数组工具类方法,创建一个新数组,将新数组的地址赋值给
    elementData elementData = Arrays.copyOf(elementData, newCapacity); 
}

add(int index, E element)

    public void add(int index, E element) {
        // 检查index的合法性
        rangeCheckForAdd(index);
		 // 校验内部容量
        ensureCapacityInternal(size + 1);  // Increments modCount!!
        // index = 3
        // [1,2,3,4,5] --> [1,2,3, ,4,5]
        // 将 elementData index=3后的元素复制到 elementData 的 index+1=4后的位置
        // 也就是空出来了 index 的位置
        System.arraycopy(elementData, index, elementData, index + 1,
                         size - index);
        elementData[index] = element;
        size++;
    }

addAll(Collection<?> c )

public boolean addAll(Collection<? extends E> c) {
    //把集合的元素转存到Object类型的数组中
    Object[] a = c.toArray();
    //记录数组的长度
    int numNew = a.length;
    //调用方法检验是否要扩容,且让增量++
    ensureCapacityInternal(size + numNew);  // Increments modCount
    //调用方法将a数组的元素拷贝到elementData数组中
    System.arraycopy(a, 0, elementData, size, numNew);
    //集合的长度+=a数组的长度
    size += numNew;
    //只要a数组的长度不等于0,即说明添加成功
    return numNew != 0;
}

addAll(i nt index,Collection<? extends E> c)

    public boolean addAll(int index, Collection<? extends E> c) {
        // 校验index是否合法
        rangeCheckForAdd(index);
		// 传递进来的集合转换为数组
        Object[] a = c.toArray();
        // 记录数组的长度
        int numNew = a.length;
        // 校验是否要扩容
        ensureCapacityInternal(size + numNew);  // Increments modCount
		// 计算 数组要插入到 集合的index 下标
        int numMoved = size - index;
        if (numMoved > 0)
            // 调整 elementData 中的数据的位置 给新插入的数据流出空间
            System.arraycopy(elementData, index, elementData, index + numNew,
                             numMoved);
		// 将数据中的数据插入到新的数组对应的位置中即可
        System.arraycopy(a, 0, elementData, index, numNew);
        // 集合容量增加
        size += numNew;
        return numNew != 0;
    }

Vector

类图结构

image.png

和ArrayList的区别

本质上也是通过数组的形式来实现的,只是在每个实现的方法中都添加了一个synchronized关键字,所以Vector是数据安全的。

image.png

HashSet

类图结构

image.png

HashSet的本质

HashSet的本质其实就是HashMap,这个我们可以通过HashSet的构造方法可以看到

image.png

所以我们要搞清楚HashSet本质上搞清楚了HashMap就可以了

TreeSet

类图结构

image.png

TreeSet的本质

和HashSet一样,TreeSet的功能也不是自己来实现的,而是借助TreeMap来实现的,在TreeSet的构造方法中我们可以看到

image.png

TreeMap

类图结构

image.png

源码分析

成员变量
   	// 比较器
	private final Comparator<? super K> comparator;
	// 根节点 Entry 相当于之前的 RBNode 类型
    private transient Entry<K,V> root;

    /**
     * The number of entries in the tree
     */
    private transient int size = 0;

    /**
     * The number of structural modifications to the tree.
     */
    private transient int modCount = 0;

Entry的具体声明

    static final class Entry<K,V> implements Map.Entry<K,V> {
        K key;
        V value;
        Entry<K,V> left;
        Entry<K,V> right;
        Entry<K,V> parent;
        boolean color = BLACK;
    }
put方法
   public V put(K key, V value) {
       // 1.查找插入的位置
        Entry<K,V> t = root;
       // 第一次插入  插入的节点设置为 root 节点
        if (t == null) {
            compare(key, key); // type (and possibly null) check

            root = new Entry<>(key, value, null);
            size = 1; // 记录集合中的元素个数
            modCount++; // 记录操作的次数
            return null;
        }
        int cmp; // 比较的值
        Entry<K,V> parent; // 记录插入节点的父节点
        // split comparator and comparable paths
        Comparator<? super K> cpr = comparator;
        if (cpr != null) { // 比较器不为空
            // 遍历找到插入的位置
            do {
                parent = t;
                cmp = cpr.compare(key, t.key);
                if (cmp < 0)
                    t = t.left;
                else if (cmp > 0)
                    t = t.right;
                else
                     // 插入的值在容器中有相同的值 直接覆盖
                    return t.setValue(value);
            } while (t != null);
        }
        else {
            if (key == null)
                throw new NullPointerException();
            // 比较器为空就创建一个比较器
            @SuppressWarnings("unchecked")
                Comparable<? super K> k = (Comparable<? super K>) key;
            // 找到插入的位置
            do {
                parent = t;
                cmp = k.compareTo(t.key);
                if (cmp < 0)
                    t = t.left;
                else if (cmp > 0)
                    t = t.right;
                else
                    return t.setValue(value);
            } while (t != null);
        }
       // 需要插入的 k v 对封装为 Entry对象
        Entry<K,V> e = new Entry<>(key, value, parent);
        if (cmp < 0)
            parent.left = e;
        else
            parent.right = e;
       // 变色和调整操作
        fixAfterInsertion(e);
        size++;
        modCount++;
        return null;
    }

fixAfterInsertion(e)方法:

   private void fixAfterInsertion(Entry<K,V> x) {
       // 插入节点 默认就是红色节点
        x.color = RED;
		// 只有 插入节点的 父节点是 黑色节点才需要 调整平衡
        while (x != null && x != root && x.parent.color == RED) {
            if (parentOf(x) == leftOf(parentOf(parentOf(x)))) {
                // 插入节点的 父亲节点是 爷爷节点的左节点
                // 获取叔叔节点
                Entry<K,V> y = rightOf(parentOf(parentOf(x)));
                if (colorOf(y) == RED) {
                    // 存在叔叔节点
                    // 父亲和叔叔节点 变为黑色
                    setColor(parentOf(x), BLACK);
                    setColor(y, BLACK);
                    // 爷爷节点变为红色
                    setColor(parentOf(parentOf(x)), RED);
                    // 将插入节点调整为 爷爷节点 然后递归处理
                    x = parentOf(parentOf(x));
                } else {
                    // 没有叔叔节点
                    if (x == rightOf(parentOf(x))) {
                        // 插入节点是父节点的右子节点 需要先根据父亲节点左旋一次
                        //    6           6
                        //   /           /
                        //  4   ==>     5
                        //   \         /
                        //    5       4
                        x = parentOf(x);
                        rotateLeft(x);
                    }
                    // 然后父亲节点设置为 黑色 爷爷节点设置为红色
                    setColor(parentOf(x), BLACK);
                    setColor(parentOf(parentOf(x)), RED);
                    // 右旋一次即可
                    rotateRight(parentOf(parentOf(x)));
                }
            } else {
                // 和上面的情况刚好相反
                Entry<K,V> y = leftOf(parentOf(parentOf(x)));
                if (colorOf(y) == RED) {
                    setColor(parentOf(x), BLACK);
                    setColor(y, BLACK);
                    setColor(parentOf(parentOf(x)), RED);
                    x = parentOf(parentOf(x));
                } else {
                    if (x == leftOf(parentOf(x))) {
                        x = parentOf(x);
                        rotateRight(x);
                    }
                    setColor(parentOf(x), BLACK);
                    setColor(parentOf(parentOf(x)), RED);
                    rotateLeft(parentOf(parentOf(x)));
                }
            }
        }
       // 将 root 节点设置为 黑色节点
        root.color = BLACK;
    }

HashMap

设计原理

  HashMap 基于哈希表的 Map 接口实现,是以 key-value 存储形式存在,即主要用来存放键值对。HashMap 的实现不是同步的,这意味着它不是线程安全的。它的 key、value 都可以为 null,此外,HashMap 中的映射不是有序的。

  jdk1.8 之前 HashMap 由 数组 + 链表 组成,数组是 HashMap 的主体,链表则是主要为了解决哈希冲突(两个对象调用的 hashCode 方法计算的哈希值经哈希函数算出来的地址被别的元素占用)而存在的(“拉链法”解决冲突)。jdk1.8 以后在解决哈希冲突时有了较大的变化,当链表长度大于阈值(或者红黑树的边界值,默认为 8 )并且当前数组的长度大于 64 时,此时此索引位置上的所有数据改为使用红黑树存储。

补充:将链表转换成红黑树前会判断,即便阈值大于 8,但是数组长度小于 64,此时并不会将链表变为红黑树,而是选择逬行数组扩容。

  这样做的目的是因为数组比较小,尽量避开红黑树结构,这种情况下变为红黑树结构,反而会降低效率,因为红黑树需要逬行左旋,右旋,变色这些操作来保持平衡。同时数组长度小于64时,搜索时间相对要快些。所以结上所述为了提高性能和减少搜索时间,底层阈值大于8并且数组长度大于64时,链表才转换为红黑树,具体可以参考 treeifyBin() 方法。

  当然虽然增了红黑树作为底层数据结构,结构变得复杂了,但是阈值大于 8 并且数组长度大于 64 时,链表转换为红黑树时,效率也变的更高效。

HashMap 特点:

  1. 存储无序的。
  2. 键和值位置都可以是 null,但是键位置只能存在一个 null。
  3. 键位置是唯一的,是底层的数据结构控制的。
  4. jdk1.8 前数据结构是链表+数组,jdk1.8 之后是链表+数组+红黑树。
  5. 阈值(边界值)> 8 并且数组长度大于 64,才将链表转换为红黑树,变为红黑树的目的是为了高效的查询。
7.1.1 JDK1.7

image.png

7.1.2 JDK1.8

image.png

7.1.3 扩容

image.png

image.png

7.2 源码分析

7.2.1 成员变量

image.png

serialVersionUID

序列化版本号

DEFAULT_INITIAL_CAPACITY

集合的初始化容量(必须是 2 的 n 次幂)

// 默认的初始容量是16	1 << 4 相当于 1*2的4次方
static final int DEFAULT_INITIAL_CAPACITY = 1 << 4;

DEFAULT_LOAD_FACTOR

默认的负载因子(默认值 0.75)

static final float DEFAULT_LOAD_FACTOR = 0.75f;

MAXIMUM_CAPACITY

集合最大容量

static final int MAXIMUM_CAPACITY = 1 << 30; // 2的30次幂

TREEIFY_THRESHOLD

当链表的值超过8则会转为红黑树(jdk1.8新增)

// 当桶(bucket)上的结点数大于这个值时会转为红黑树
static final int TREEIFY_THRESHOLD = 8;

UNTREEIFY_THRESHOLD

当链表的值小于 6 则会从红黑树转回链表

// 当桶(bucket)上的结点数小于这个值,树转为链表 
static final int UNTREEIFY_THRESHOLD = 6;

MIN_TREEIFY_CAPACITY

当 Map 里面的数量超过这个值时,表中的桶才能进行树形化,否则桶内元素太多时会扩容,而不是树形化为了避免进行扩容、树形化选择的冲突,这个值不能小于4*TREEIFY_THRESHOLD(8)

// 桶中结构转化为红黑树对应的数组长度最小的值 
static final int MIN_TREEIFY_CAPACITY = 64;

table

table 用来初始化(必须是二的n次幂)(重点)

// 存储元素的数组 
transient Node<K,V>[] table;

在 jdk1.8 中我们了解到 HashMap 是由数组加链表加红黑树来组成的结构,其中 table 就是 HashMap 中的数组,jdk8 之前数组类型是 Entry<K,V> 类型。从 jdk1.8 之后是 Node<K,V> 类型。只是换了个名字,都实现了一样的接口:Map.Entry<K,V>。负责存储键值对数据的。

entrySet

用来存放缓存

// 存放具体元素的集合
transient Set<Map.Entry<K,V>> entrySet;

size

HashMap 中存放元素的个数(重点)

// 存放元素的个数,注意这个不等于数组的长度
 transient int size;

size 为 HashMap 中 K-V 的实时数量,不是数组 table 的长度。

modCount

用来记录 HashMap 的修改次数

// 每次扩容和更改 map 结构的计数器
 transient int modCount;  

threshold

用来调整大小下一个容量的值计算方式为(容量*负载因子)

// 临界值 当实际大小(容量*负载因子)超过临界值时,会进行扩容
int threshold;

loadFactor

哈希表的负载因子(重点)

// 负载因子
final float loadFactor;
构造方法

HashMap()

构造一个空的HashMap,默认初始容量(16)和默认负载因子(0.75)

public HashMap() {
    // 将默认的负载因子0.75赋值给loadFactor,并没有创建数组
   this.loadFactor = DEFAULT_LOAD_FACTOR; 
}

HashMap(int initialCapacity)

构造一个具有指定的初始容量和默认负载因子(0.75)HashMap 。

 // 指定“容量大小”的构造函数
public HashMap(int initialCapacity) {
    this(initialCapacity, DEFAULT_LOAD_FACTOR);
}

HashMap(int initialCapacity, float loadFactor)

构造一个具有指定的初始容量和负载因子的 HashMap。

/*
	 指定“容量大小”和“负载因子”的构造函数
	 initialCapacity:指定的容量
	 loadFactor:指定的负载因子
*/
public HashMap(int initialCapacity, float loadFactor) {
    	// 判断初始化容量initialCapacity是否小于0
        if (initialCapacity < 0)
            // 如果小于0,则抛出非法的参数异常IllegalArgumentException
            throw new IllegalArgumentException("Illegal initial capacity: " + initialCapacity);
    	// 判断初始化容量initialCapacity是否大于集合的最大容量MAXIMUM_CAPACITY
        if (initialCapacity > MAXIMUM_CAPACITY)
            // 如果超过MAXIMUM_CAPACITY,会将MAXIMUM_CAPACITY赋值给initialCapacity
            initialCapacity = MAXIMUM_CAPACITY;
    	// 判断负载因子loadFactor是否小于等于0或者是否是一个非数值
        if (loadFactor <= 0 || Float.isNaN(loadFactor))
            // 如果满足上述其中之一,则抛出非法的参数异常IllegalArgumentException
            throw new IllegalArgumentException("Illegal load factor: " + loadFactor);
     	// 将指定的负载因子赋值给HashMap成员变量的负载因子loadFactor
        this.loadFactor = loadFactor;
        this.threshold = tableSizeFor(initialCapacity);
    }
// 最后调用了tableSizeFor,来看一下方法实现:
     /*
     	返回比指定初始化容量大的最小的2的n次幂
     */
    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;
    }

image.png

HashMap(Map<? extends K, ? extends V> m)

包含另一个 “Map” 的构造函数

// 构造一个映射关系与指定 Map 相同的新 HashMap。
public HashMap(Map<? extends K, ? extends V> m) {
    	// 负载因子loadFactor变为默认的负载因子0.75
         this.loadFactor = DEFAULT_LOAD_FACTOR;
         putMapEntries(m, false);
 }

最后调用了 putMapEntries(),来看一下方法实现:

final void putMapEntries(Map<? extends K, ? extends V> m, boolean evict) {
    //获取参数集合的长度
    int s = m.size();
    if (s > 0) {
        //判断参数集合的长度是否大于0,说明大于0
        if (table == null) { // 判断table是否已经初始化
                // 未初始化,s为m的实际元素个数
                float ft = ((float)s / loadFactor) + 1.0F;
                int t = ((ft < (float)MAXIMUM_CAPACITY) ? (int)ft : MAXIMUM_CAPACITY);
                // 计算得到的t大于阈值,则初始化阈值
                if (t > threshold)
                    threshold = tableSizeFor(t);
        }
        // 已初始化,并且m元素个数大于阈值,进行扩容处理
        else if (s > threshold)
            resize();
        // 将m中的所有元素添加至HashMap中
        for (Map.Entry<? extends K, ? extends V> e : m.entrySet()) {
            K key = e.getKey();
            V value = e.getValue();
            putVal(hash(key), key, value, false, evict);
        }
    }
}

注意:

float ft = ((float)s / loadFactor) + 1.0F; 这一行代码中为什么要加 1.0F ?

s/loadFactor 的结果是小数,加 1.0F 与 (int)ft 相当于是对小数做一个向上取整以尽可能的保证更大容量,更大的容量能够减少 resize 的调用次数。所以 + 1.0F 是为了获取更大的容量。

例如:原来集合的元素个数是 6 个,那么 6/0.75 是8,是 2 的n次幂,那么新的数组大小就是 8 了。然后原来数组的数据就会存储到长度是 8 的新的数组中了,这样会导致在存储元素的时候,容量不够,还得继续扩容,那么性能降低了,而如果 +1 呢,数组长度直接变为16了,这样可以减少数组的扩容。

put方法

put方法的实现文字说明:

  1. 先通过 hash 值计算出 key 映射到哪个桶;

  2. 如果桶上没有碰撞冲突,则直接插入;

  3. 如果出现碰撞冲突了,则需要处理冲突:

    • a 如果该桶使用红黑树处理冲突,则调用红黑树的方法插入数据;
    • b 否则采用传统的链式方法插入。如果链的长度达到临界值,则把链转变为红黑树;
  4. 如果桶中存在重复的键,则为该键替换新值 value;

  5. 如果 size 大于阈值 threshold,则进行扩容;

put方法的源码说明

public V put(K key, V value) {
	return putVal(hash(key), key, value, false, true);
}
/**
 * hash:key 的 hash 值
 * key:原始 key
 * value:要存放的值
 * onlyIfAbsent:如果 true 代表不更改现有的值
 * evict:如果为false表示 table 为创建状态
 */
final V putVal(int hash, K key, V value, boolean onlyIfAbsent, boolean evict) {
    Node<K,V>[] tab; Node<K,V> p; int n, i;
    /*
    	1)transient Node<K,V>[] table; 表示存储Map集合中元素的数组。
    	2)(tab = table) == null 表示将空的table赋值给tab,然后判断tab是否等于null,第一次肯定是null。
    	3)(n = tab.length) == 0 表示将数组的长度0赋值给n,然后判断n是否等于0,n等于0,由于if判断使用双或,满足一个即可,则执行代码 n = (tab = resize()).length; 进行数组初始化,并将初始化好的数组长度赋值给n。
    	4)执行完n = (tab = resize()).length,数组tab每个空间都是null。
    */
    if ((tab = table) == null || (n = tab.length) == 0)
        n = (tab = resize()).length;
    /*
    	1)i = (n - 1) & hash 表示计算数组的索引赋值给i,即确定元素存放在哪个桶中。
    	2)p = tab[i = (n - 1) & hash]表示获取计算出的位置的数据赋值给结点p。
    	3) (p = tab[i = (n - 1) & hash]) == null 判断结点位置是否等于null,如果为null,则执行代码:tab[i] = newNode(hash, key, value, null);根据键值对创建新的结点放入该位置的桶中。
        小结:如果当前桶没有哈希碰撞冲突,则直接把键值对插入空间位置。
    */ 
    if ((p = tab[i = (n - 1) & hash]) == null)
        // 创建一个新的结点存入到桶中
        tab[i] = newNode(hash, key, value, null);
    else {
         // 执行else说明tab[i]不等于null,表示这个位置已经有值了
        Node<K,V> e; K k;
        /*
        	比较桶中第一个元素(数组中的结点)的hash值和key是否相等
        	1)p.hash == hash :p.hash表示原来存在数据的hash值  hash表示后添加数据的hash值 比较两个hash值是否相等。
                 说明:p表示tab[i],即 newNode(hash, key, value, null)方法返回的Node对象。
                    Node<K,V> newNode(int hash, K key, V value, Node<K,V> next) {
                        return new Node<>(hash, key, value, next);
                    }
                    而在Node类中具有成员变量hash用来记录着之前数据的hash值的。
             2)(k = p.key) == key :p.key获取原来数据的key赋值给k  key 表示后添加数据的key比较两个key的地址值是否相等。
             3)key != null && key.equals(k):能够执行到这里说明两个key的地址值不相等,那么先判断后添加的key是否等于null,如果不等于null再调用equals方法判断两个key的内容是否相等。
        */
        if (p.hash == hash &&
            ((k = p.key) == key || (key != null && key.equals(k))))
                /*
                	说明:两个元素哈希值相等,并且key的值也相等,将旧的元素整体对象赋值给e,用e来记录
                */ 
                e = p;
        // hash值不相等或者key不相等;判断p是否为红黑树结点
        else if (p instanceof TreeNode)
            // 放入树中
            e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
        // 说明是链表结点
        else {
            /*
            	1)如果是链表的话需要遍历到最后结点然后插入
            	2)采用循环遍历的方式,判断链表中是否有重复的key
            */
            for (int binCount = 0; ; ++binCount) {
                /*
                	1)e = p.next 获取p的下一个元素赋值给e。
                	2)(e = p.next) == null 判断p.next是否等于null,等于null,说明p没有下一个元素,那么此时到达了链表的尾部,还没有找到重复的key,则说明HashMap没有包含该键,将该键值对插入链表中。
                */
                if ((e = p.next) == null) {
                    /*
                    	1)创建一个新的结点插入到尾部
                    	 p.next = newNode(hash, key, value, null);
                    	 Node<K,V> newNode(int hash, K key, V value, Node<K,V> next) {
                                return new Node<>(hash, key, value, next);
                         }
                         注意第四个参数next是null,因为当前元素插入到链表末尾了,那么下一个结点肯定是null。
                         2)这种添加方式也满足链表数据结构的特点,每次向后添加新的元素。
                    */
                    p.next = newNode(hash, key, value, null);
                    /*
                    	1)结点添加完成之后判断此时结点个数是否大于TREEIFY_THRESHOLD临界值8,如果大于则将链表转换为红黑树。
                    	2)int binCount = 0 :表示for循环的初始化值。从0开始计数。记录着遍历结点的个数。值是0表示第一个结点,1表示第二个结点。。。。7表示第八个结点,加上数组中的的一个元素,元素个数是9。
                    	TREEIFY_THRESHOLD - 1 --》8 - 1 ---》7
                    	如果binCount的值是7(加上数组中的的一个元素,元素个数是9)
                    	TREEIFY_THRESHOLD - 1也是7,此时转换红黑树。
                    */
                    if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
                        // 转换为红黑树
                        treeifyBin(tab, hash);
                    // 跳出循环
                    break;
                }
       
                /*
                	执行到这里说明e = p.next 不是null,不是最后一个元素。继续判断链表中结点的key值与插入的元素的key值是否相等。
                */
                if (e.hash == hash &&
                    ((k = e.key) == key || (key != null && key.equals(k))))
                    // 相等,跳出循环
                    /*
                		要添加的元素和链表中的存在的元素的key相等了,则跳出for循环。不用再继续比较了
                		直接执行下面的if语句去替换去 if (e != null) 
                	*/
                    break;
                /*
                	说明新添加的元素和当前结点不相等,继续查找下一个结点。
                	用于遍历桶中的链表,与前面的e = p.next组合,可以遍历链表
                */
                p = e;
            }
        }
        /*
        	表示在桶中找到key值、hash值与插入元素相等的结点
        	也就是说通过上面的操作找到了重复的键,所以这里就是把该键的值变为新的值,并返回旧值
        	这里完成了put方法的修改功能
        */
        if (e != null) { 
            // 记录e的value
            V oldValue = e.value;
            // onlyIfAbsent为false或者旧值为null
            if (!onlyIfAbsent || oldValue == null)
                // 用新值替换旧值
                // e.value 表示旧值  value表示新值 
                e.value = value;
            // 访问后回调
            afterNodeAccess(e);
            // 返回旧值
            return oldValue;
        }
    }
    // 修改记录次数
    ++modCount;
    // 判断实际大小是否大于threshold阈值,如果超过则扩容
    if (++size > threshold)
        resize();
    // 插入后回调
    afterNodeInsertion(evict);
    return null;
}

上面的源码中使用到了hash方法,我们来看下hash方法的源码

static final int hash(Object key) {
	int h;
	/*
	1)如果key等于null:返回的是0.
	2)如果key不等于null:首先计算出key的hashCode赋值给h,然后与h无符号右移16位后的
		二进制进行按位异或得到最后的hash值
	*/
	return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}

从上面可以得知 HashMap 是支持 key 为空的,而 HashTable 是直接用 Key 来获取hashCode 所以 key 为空会抛异常。

(n - 1) & hash 的实现介绍:

  • key.hashCode();返回散列值也就是 hashcode,假设随便生成的一个值。
  • n 表示数组初始化的长度是 16。
  • &(按位与运算):运算规则:相同的二进制数位上,都是 1 的时候,结果为 1,否则为0。
  • ^(按位异或运算):运算规则:相同的二进制数位上,数字相同,结果为 0,不同为 1。

img

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值