Java集合源码-List、Set、Map系列——读懂源码,不背八股~

目录

ArrayList源码分析

属性

构造方法

增加方法

add(E e)

ensureCapacityInternal(int minCapacity)

calculateCapacity方法

ensureExplicitCapacity方法

grow方法【核心扩容方法】

add(int index, E element)

addAll(Collection c )

addAll(int index,Collection c)

Vector

HashSet

TreeSet

TreeMap源码分析

属性【成员变量】

Entry具体定义

put方法——插入节点方法

fixAfterInsertion方法

HashMap源码分析

设计原理

成员变量

构造方法

HashMap()方法

HashMap(int initialCapacity)方法

HashMap(int initialCapacity, float loadFactor)方法

HashMap(Map m)方法

putMapEntries(m, false)方法

增加方法

put()方法源码

hash()方法

treeifyBin()方法——链表转红黑树

resize()方法——扩容

什么时候才需要扩容

HashMap 的扩容是什么

resize() 方法

删除方法——remove

获取元素方法——get


总览

ArrayList源码分析

属性

属性作用
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(int index,Collection<? extends E> c)将指定集合中的所有元素插入到此列表中,从指定的位置开始。

增加方法

add(E e)

public boolean add(E e) {
       // 校验内部容量
       ensureCapacityInternal(size + 1);  // Increments modCount!!
       elementData[size++] = e;
       return true;
}

ensureCapacityInternal(int minCapacity)
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) {
           // 通过最小容量和默认容量 求出较大值 (用于第一次扩容)
           return Math.max(DEFAULT_CAPACITY, minCapacity);
       }
       return minCapacity;
}

ensureExplicitCapacity方法
private void ensureExplicitCapacity(int minCapacity) { 
    //实际修改集合次数++ (在扩容的过程中没用,主要是用于迭代器中) 
    modCount++; 
    //判断最小容量 - 数组长度是否大于 0 
    if (minCapacity - elementData.length > 0) 
        //将第一次计算出来的容量传递给 核心扩容方法 
        grow(minCapacity); 
}

grow方法【核心扩容方法】
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(int 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

  • 与ArrayList的差别在于 锁

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

HashSet

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

TreeSet

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

TreeMap源码分析

TreeMap实现的本质就是红黑树(前面博客有详解)

属性【成员变量】

  // 比较器

private final Comparator<? super K> comparator;

// 根节点 Entry 相当于之前的 RBNode 类型

   private transient Entry<K,V> root;

// entry的个数

   private transient int size = 0;

// 修改次数

   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方法

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,才将链表转换为红黑树,变为红黑树的目的是为了高效的查询。

成员变量

  • serialVersionUID 序列化版本号

  • DEFAULT_INITIAL_CAPACITY 集合的初始化长度(必须是2的n次幂)

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

  • MAXIMUM_CAPACITY 集合最大容量(2的30次方)

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

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

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

  • Table 存储元素的数组 (必须是二的n次幂) table 就是 HashMap 中的数组

  • entrySet:存放缓存

  • size HashMap中存储元素的个数

  • modcount: 用来记录HashMap的修改次数

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

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

  • threshold

构造方法

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;
}

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

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

putMapEntries(m, false)方法

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;
                // 保证更大容量,更大的容量能够减少 resize 的调用次数
                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);
        }
    }
}

增加方法

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

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

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

    1. a 如果该桶使用红黑树处理冲突,则调用红黑树的方法插入数据;

    2. 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()方法
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。

treeifyBin()方法——链表转红黑树

/*
替换指定哈希表的索引处桶中的所有链接结点,除非表太小,否则将修改大小。
Node<K,V>[] tab = tab 数组名
int hash = hash表示哈希值
*/
final void treeifyBin(Node<K,V>[] tab, int hash) {
    int n, index; Node<K,V> e;
    /*
    如果当前数组为空或者数组的长度小于进行树形化的阈值(MIN_TREEIFY_CAPACITY = 64),就去扩容。而不是将结点变为红黑树。
    目的:如果数组很小,那么转换红黑树,然后遍历效率要低一些。这时进行扩容,那么重新计算哈希值,链表长度有可能就变短了,数据会放到数组中,这样相对来说效率高一些。
    */
    if (tab == null || (n = tab.length) < MIN_TREEIFY_CAPACITY)
        //扩容方法
        resize();
    else if ((e = tab[index = (n - 1) & hash]) != null) {
        /*
        1)执行到这里说明哈希表中的数组长度大于阈值64,开始进行树形化
        2)e = tab[index = (n - 1) & hash]表示将数组中的元素取出赋值给e,e是哈希表中指定位置桶里的链表结点,从第一个开始
        */
        // hd:红黑树的头结点   tl:红黑树的尾结点
        TreeNode<K,V> hd = null, tl = null;
        do {
            // 新创建一个树的结点,内容和当前链表结点e一致
            TreeNode<K,V> p = replacementTreeNode(e, null);
            if (tl == null)
                hd = p; // 将新创键的p结点赋值给红黑树的头结点
            else {
                p.prev = tl; // 将上一个结点p赋值给现在的p的前一个结点
                tl.next = p; // 将现在结点p作为树的尾结点的下一个结点
            }
            tl = p;
            /*
            e = e.next 将当前结点的下一个结点赋值给e,如果下一个结点不等于null
            则回到上面继续取出链表中结点转换为红黑树
            */
        } while ((e = e.next) != null);
        /*
        让桶中的第一个元素即数组中的元素指向新建的红黑树的结点,以后这个桶里的元素就是红黑树
        而不是链表数据结构了
        */
        if ((tab[index] = hd) != null)
            hd.treeify(tab);
    }
}

resize()方法——扩容

什么时候才需要扩容

当 HashMap 中的元素个数超过数组大小(数组长度)*loadFactor(负载因子)时,就会进行数组扩容,loadFactor 的默认值是 0.75。

HashMap 的扩容是什么

进行扩容,会伴随着一次重新 hash 分配,并且会遍历 hash 表中所有的元素,是非常耗时的。在编写程序中,要尽量避免 resize。HashMap 在进行扩容时,使用的 rehash 方式非常巧妙,因为每次扩容都是翻倍,与原来计算的 (n - 1) & hash 的结果相比,只是多了一个 bit 位,所以结点要么就在原来的位置,要么就被分配到 “原位置 + 旧容量” 这个位置。

resize() 方法

final Node<K,V>[] resize() {
    // 得到当前数组
    Node<K,V>[] oldTab = table;
    // 如果当前数组等于null长度返回0,否则返回当前数组的长度
    int oldCap = (oldTab == null) ? 0 : oldTab.length;
    //当前阀值点 默认是12(16*0.75)
    int oldThr = threshold;
    int newCap, newThr = 0;
    // 如果老的数组长度大于0
    // 开始计算扩容后的大小
    if (oldCap > 0) {
        // 超过最大值就不再扩充了,就只好随你碰撞去吧
        if (oldCap >= MAXIMUM_CAPACITY) {
            // 修改阈值为int的最大值
            threshold = Integer.MAX_VALUE;
            return oldTab;
        }
        /*
        没超过最大值,就扩充为原来的2倍
        1) (newCap = oldCap << 1) < MAXIMUM_CAPACITY 扩大到2倍之后容量要小于最大容量
        2)oldCap >= DEFAULT_INITIAL_CAPACITY 原数组长度大于等于数组初始化长度16
        */
        else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY &&
                 oldCap >= DEFAULT_INITIAL_CAPACITY)
            // 阈值扩大一倍
            newThr = oldThr << 1; // double threshold
    }
    // 老阈值点大于0 直接赋值
    else if (oldThr > 0) // 老阈值赋值给新的数组长度
        newCap = oldThr;
    else { // 直接使用默认值
        newCap = DEFAULT_INITIAL_CAPACITY;//16
        newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY);
    }
    // 计算新的resize最大上限
    if (newThr == 0) {
        float ft = (float)newCap * loadFactor;
        newThr = (newCap < MAXIMUM_CAPACITY && ft < (float)MAXIMUM_CAPACITY ?
                  (int)ft : Integer.MAX_VALUE);
    }
    // 新的阀值 默认原来是12 乘以2之后变为24
    threshold = newThr;
    // 创建新的哈希表
    @SuppressWarnings({"rawtypes","unchecked"})
    //newCap是新的数组长度--》32
    Node<K,V>[] newTab = (Node<K,V>[])new Node[newCap];
    table = newTab;
    // 判断旧数组是否等于空
    if (oldTab != null) {
        // 把每个bucket都移动到新的buckets中
        // 遍历旧的哈希表的每个桶,重新计算桶里元素的新位置
        for (int j = 0; j < oldCap; ++j) {
            Node<K,V> e;
            if ((e = oldTab[j]) != null) {
                // 原来的数据赋值为null 便于GC回收
                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 { // 采用链表处理冲突
                    Node<K,V> loHead = null, loTail = null;
                    Node<K,V> hiHead = null, hiTail = null;
                    Node<K,V> next;
                    // 通过上述讲解的原理来计算结点的新位置
                    do {
                        // 原索引
                        next = e.next;
                     // 这里来判断如果等于true e这个结点在resize之后不需要移动位置
                        if ((e.hash & oldCap) == 0) {
                            if (loTail == null)
                                loHead = e;
                            else
                                loTail.next = e;
                            loTail = e;
                        }
                        // 原索引+oldCap
                        else {
                            if (hiTail == null)
                                hiHead = e;
                            else
                                hiTail.next = e;
                            hiTail = e;
                        }
                    } while ((e = next) != null);
                    // 原索引放到bucket里
                    if (loTail != null) {
                        loTail.next = null;
                        newTab[j] = loHead;
                    }
                    // 原索引+oldCap放到bucket里
                    if (hiTail != null) {
                        hiTail.next = null;
                        newTab[j + oldCap] = hiHead;
                    }
                }
            }
        }
    }
    return newTab;
}

删除方法——remove

删除方法:首先先找到元素的位置,如果是链表就遍历链表找到元素之后删除。如果是用红黑树就遍历树然后找到之后做删除,树元素个数小于 6 的时候要转链表。

// remove方法的具体实现在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;
// 根据hash找到位置 
// 如果当前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,则将node指向该结点
        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 {
                // 判断是否以链表方式处理hash冲突,是的话则通过遍历链表来寻找要删除的结点
                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;
}

获取元素方法——get

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) {
    Node<K,V>[] tab; Node<K,V> first, e; int n; K k;
    // 如果哈希表不为空并且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) {
            // 判断是否是红黑树,是的话调用红黑树中的getTreeNode方法获取结点
            if (first instanceof TreeNode)
                return ((TreeNode<K,V>)first).getTreeNode(hash, key);
            do {
                // 不是红黑树的话,那就是链表结构了,通过循环的方法判断链表中是否存在该key
                if (e.hash == hash &&
                    ((k = e.key) == key || (key != null && key.equals(k))))
                    return e;
            } while ((e = e.next) != null);
        }
    }
    return null;
}

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值