HashMap底层原理

一、JDK1.7 HashMap底层实现

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

数据结构

HashMap底层是基于动态数组+链表组成。
在这里插入图片描述

成员变量

/** 初始容量,默认16 */
static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; // aka 16
/** 最大初始容量,2^30 */
static final int MAXIMUM_CAPACITY = 1 << 30;
/** 负载因子,默认0.75,负载因子越小,hash冲突机率越低 */
static final float DEFAULT_LOAD_FACTOR = 0.75f;
/** 初始化一个Entry的空数组 */
static final Entry<?,?>[] EMPTY_TABLE = {};
 
/** 将初始化好的空数组赋值给table,table数组是HashMap实际存储数据的地方,并不在EMPTY_TABLE数组中 */
transient Entry<K,V>[] table = (Entry<K,V>[]) EMPTY_TABLE;
 
/** HashMap实际存储的元素个数 */
transient int size;
 
/** 临界值(HashMap 实际能存储的大小),公式为(threshold = capacity * loadFactor) */
int threshold;
 
/** 负载因子 */
final float loadFactor;
 
/** HashMap的结构被修改的次数,用于迭代器 */
transient int modCount;

这是HashMap中比较核心的几个参数。分别看看是什么意思?
1、初始化数组的容量
2、桶的最大值(元素的最大个数)
3、默认的负载因子(0.75)
4、table正在存放数据的数组
5、Map存放元素数量的大小。
6、阈值,(阈值 = 容量 * 负载因子),当Map中的元素大小大于阈值时,进行扩容。
7、负载因子,可在初始化时显式指定

构造方法

public HashMap(int initialCapacity, float loadFactor) {
        // 判断设置的容量和负载因子合不合理
        if (initialCapacity < 0)
            throw new IllegalArgumentException("Illegal initial capacity: " +
                                               initialCapacity);
        if (initialCapacity > MAXIMUM_CAPACITY)
            initialCapacity = MAXIMUM_CAPACITY;
        if (loadFactor <= 0 || Float.isNaN(loadFactor))
            throw new IllegalArgumentException("Illegal load factor: " +
                                               loadFactor);
        // 设置负载因子,临界值此时为容量大小,后面第一次put时由inflateTable(int toSize)方法计算设置
        this.loadFactor = loadFactor;
        threshold = initialCapacity;
        init();
    }

    public HashMap(int initialCapacity) {
        this(initialCapacity, DEFAULT_LOAD_FACTOR);
    }

    public HashMap() {
        this(DEFAULT_INITIAL_CAPACITY, DEFAULT_LOAD_FACTOR);
    }

    public HashMap(Map<? extends K, ? extends V> m) {
        this(Math.max((int) (m.size() / DEFAULT_LOAD_FACTOR) + 1,
                      DEFAULT_INITIAL_CAPACITY), DEFAULT_LOAD_FACTOR);
        inflateTable(threshold);
        putAllForCreate(m);
    }

重点解释下负载因子:

由于给定的 HashMap 的容量大小是固定的,比如默认初始化:

给定的默认容量为 16,负载因子为 0.75。Map 在使用过程中不断的往里面存放数据,当数量达到了 16 * 0.75 = 12 就需要将当前 16 的容量进行扩容,而扩容这个过程涉及到 rehash、复制数据等操作,所以非常消耗性能。

因此通常建议能提前预估 HashMap 的大小最好,尽量的减少扩容带来的性能损耗。

根据代码可以看到其实真正存放数据的是
transient Entry<K,V>[] table = (Entry<K,V>[]) EMPTY_TABLE;

Entry 是 HashMap 中的一个内部类,从他的成员变量很容易看出:
1、key 就是写入时的键。
2、value 自然就是值。
3、开始的时候就提到 HashMap 是由数组和链表组成,所以这个 next 就是用于实现链表结构。
4、hash 存放的是当前 key 的 hashcode。

PUT方法

public V put(K key, V value) {  
    // 如果table引用指向成员变量EMPTY_TABLE,那么初始化HashMap(设置容量、临界值,新的Entry数组引用)
    if (table == EMPTY_TABLE) {
        inflateTable(threshold);
    }
    // 若“key为null”,则将该键值对添加到table[0]处,遍历该链表,如果有key为null,则将value替换。没有就创建新Entry对象放在链表表头
    // 所以table[0]的位置上,永远最多存储1个Entry对象,形成不了链表。key为null的Entry存在这里 
    if (key == null)  
        return putForNullKey(value);  
    // 若“key不为null”,则计算该key的哈希值
    int hash = hash(key);  
    // 搜索指定hash值在对应table中的索引
    int i = indexFor(hash, table.length);  
    // 循环遍历table数组上的Entry对象,判断该位置上key是否已存在
    for (Entry<K,V> e = table[i]; e != null; e = e.next) {  
        Object k;  
        // 哈希值相同并且对象相同
        if (e.hash == hash && ((k = e.key) == key || key.equals(k))) {  
            // 如果这个key对应的键值对已经存在,就用新的value代替老的value,然后退出!
            V oldValue = e.value;  
            e.value = value;  
            e.recordAccess(this);  
            return oldValue;  
        }  
    }  
    // 修改次数+1
    modCount++;
    // table数组中没有key对应的键值对,就将key-value添加到table[i]处 
    addEntry(hash, key, value, i);  
    return null;  
}

可以看到,当我们给put()方法传递键和值时,HashMap会由key来调用hash()方法,返回键的hash值,计算Index后用于找到bucket(哈希桶)的位置来储存Entry对象。

如果两个对象key的hash值相同,那么它们的bucket位置也相同,但equals()不相同,添加元素时会发生hash碰撞,也叫hash冲突,HashMap使用链表来解决碰撞问题。

分析源码可知,put()时,HashMap会先遍历table数组,用hash值和equals()判断数组中是否存在完全相同的key对象, 如果这个key对象在table数组中已经存在,就用新的value代替老的value。如果不存在,就创建一个新的Entry对象添加到table[ i ]处。

如果该table[ i ]已经存在其他元素,那么新Entry对象将会储存在bucket链表的表头,通过next指向原有的Entry对象,形成链表结构(hash碰撞解决方案)。

Entry数据结构源码如下(HashMap内部类):

static class Entry<K,V> implements Map.Entry<K,V> {
        final K key;
        V value;
        /** 指向下一个元素的引用 */
        Entry<K,V> next;
        int hash;

        /**
         * 构造方法为Entry赋值
         */
        Entry(int h, K k, V v, Entry<K,V> n) {
            value = v;
            next = n;
            key = k;
            hash = h;
        }
        ...
        ...
 }

形成单链表的核心代码如下:如果数组长度大于等于容量×负载因子,并且要添加的位置为null才会进行扩容

    /**
     * 将Entry添加到数组bucketIndex位置对应的哈希桶中,并判断数组是否需要扩容
     */
    void addEntry(int hash, K key, V value, int bucketIndex) {
        // 如果数组长度大于等于容量×负载因子,并且要添加的位置为null
        if ((size >= threshold) && (null != table[bucketIndex])) {
            // 长度扩大为原数组的两倍,代码分析见下面扩容机制
            resize(2 * table.length);
            hash = (null != key) ? hash(key) : 0;
            bucketIndex = indexFor(hash, table.length);
        }
        createEntry(hash, key, value, bucketIndex);
    }

    /**
     * 在链表中添加一个新的Entry对象在链表的表头
     */
    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++;
    }

GET方法

如果两个不同的key的hashcode相同,两个值对象储存在同一个bucket位置,要获取value,我们调用get()方法,HashMap会使用key的hashcode找到bucket位置,因为HashMap在链表中存储的是Entry键值对,所以找到bucket位置之后,会调用key的equals()方法,按顺序遍历链表的每个 Entry,直到找到想获取的 Entry 为止——如果恰好要搜索的 Entry 位于该 Entry 链的最末端(该 Entry 是最早放入该 bucket 中),那HashMap必须循环到最后才能找到该元素。
get()方法源码如下:

public V get(Object key) {
        // 若key为null,遍历table[0]处的链表(实际上要么没有元素,要么只有一个Entry对象),取出key为null的value
        if (key == null)
            return getForNullKey();
        // 若key不为null,用key获取Entry对象
        Entry<K,V> entry = getEntry(key);
        // 若链表中找到的Entry不为null,返回该Entry中的value
        return null == entry ? null : entry.getValue();
    }

    final Entry<K,V> getEntry(Object key) {
        if (size == 0) {
            return null;
        }
        // 计算key的hash值
        int hash = (key == null) ? 0 : hash(key);
        // 计算key在数组中对应位置,遍历该位置的链表
        for (Entry<K,V> e = table[indexFor(hash, table.length)];
             e != null;
             e = e.next) {
            Object k;
            // 若key完全相同,返回链表中对应的Entry对象
            if (e.hash == hash &&
                ((k = e.key) == key || (key != null && key.equals(k))))
                return e;
        }
        // 链表中没找到对应的key,返回null
        return null;
    }

Hash算法

我们可以看到在HashMap中要找到某个元素,需要根据key的hash值来求得对应数组中的位置。如何计算这个位置就是hash算法。前面说过HashMap的数据结构是数组和链表的结合,所以我们当然希望这个HashMap里面的元素位置尽量的分布均匀些,尽量使得每个位置上的元素数量只有一个,那么当我们用hash算法求得这个位置的时候,马上就可以知道对应位置的元素就是我们要的,而不用再去遍历链表。

源码分析:

/**
     * Returns index for hash code h.
     */
    static int indexFor(int h, int length) {
        // assert Integer.bitCount(length) == 1 : "length must be a non-zero power of 2";
        return h & (length-1);
    }

扩容resize方法

当HashMap的长度超出了加载因子与当前容量的乘积(默认16*0.75=12)时,通过调用resize方法重新创建一个原来HashMap大小的两倍的newTable数组,最大扩容到2^30+1,并将原先table的元素全部移到newTable里面,重新计算hash,然后再重新根据hash分配位置。这个过程叫作rehash,因为它调用hash方法找到新的bucket位置。

扩容机制源码分析:

void resize(int newCapacity) {
        Entry[] oldTable = table;
        int oldCapacity = oldTable.length;
        // 如果之前的HashMap已经扩充打最大了,那么就将临界值threshold设置为最大的int值
        if (oldCapacity == MAXIMUM_CAPACITY) {
            threshold = Integer.MAX_VALUE;
            return;
        }

        // 根据新传入的newCapacity创建新Entry数组
        Entry[] newTable = new Entry[newCapacity];
        // 用来将原先table的元素全部移到newTable里面,重新计算hash,然后再重新根据hash分配位置
        transfer(newTable, initHashSeedAsNeeded(newCapacity));
        // 再将newTable赋值给table
        table = newTable;
        // 重新计算临界值,扩容公式在这儿(newCapacity * loadFactor)
        threshold = (int)Math.min(newCapacity * loadFactor, MAXIMUM_CAPACITY + 1);
    }
   void transfer(Entry[] newTable, boolean rehash) {
        int newCapacity = newTable.length;
        for (Entry<K,V> e : table) {
            while(null != e) {
                Entry<K,V> next = e.next;
                if (rehash) {
                    e.hash = null == e.key ? 0 : hash(e.key);
                }
                int i = indexFor(e.hash, newCapacity);
                e.next = newTable[i];
                newTable[i] = e;
                e = next;
            }
        }
    }

JDK7扩容时导致的死循环问题

/**
* Transfers all entries from current table to newTable.
*/
void transfer(Entry[] newTable) {
 Entry[] src = table;
 int newCapacity = newTable.length;
 for (int j = 0; j < src.length; j++) {
   Entry<K,V> e = src[j];
   if (e != null) {
       src[j] = null;
       do {
           // B线程执行到这里之后就暂停了
           Entry<K,V> next = e.next;
           int i = indexFor(e.hash, newCapacity);
           e.next = newTable[i];
           newTable[i] = e;
           e = next;
       } while (e != null);
   }
 }
}

扩容时上面的代码容易导致死循环,是怎样导致的呢?假设有两个线程A和B都在执行这一段代码,数组大小由2扩容到4,在扩容前tab[1]=1–>5–>9。
在这里插入图片描述
扩容前

当B线程执行到 next = e.next时让出时间片,A线程执行完整段代码但是还没有将内部的table设置为新的newTable时,线程B继续执行。

此时A线程执行完成之后,挂载在tab[1]的元素是9–>5–>1,注意这里的顺序被颠倒了。此时e = 1, next = 5;
在这里插入图片描述
线程A执行完成后
同样B线程我们也按照循环次数来分析
1、第一次循环执行完成后,newTable[i]=1, e = 5

2、第二次循环完成后: newTable[i]=5-->1, e = 1。

3、第三次循环,e没有next,所以next指向null。当执行e.next = newTable[i](1-->5)的时候,就形成了 1-->5-->1的环,再执行newTable[i]=e,此时newTable[i] = 1-->5-->1。

当在数组该位置get寻找对应的key的时候,就发生了死循环,引起CPU 100%问题。
在这里插入图片描述
线程B执行扩容过程
而JDK8就不会出现这个问题,它在这里就有一个优化,它使用了两个指针来分别指向头节点和尾节点,而且还保证了元素原本的顺序。

二、JDK1.8 HashMap底层实现

数据结构

HashMap的数据结构为动态数组+链表/红黑树的底层结构。

在这里插入图片描述
Hash表是一个数组+链表的结构,这种结构能够保证在遍历与增删的过程中,如果不产生hash碰撞,仅需一次定位就可完成,时间复杂度能保证在O(1)。 在jdk1.7中,只是单纯的数组+链表的结构,但是如果散列表中的hash碰撞过多时,会造成效率的降低,所以在JKD1.8中对这种情况进行了控制,当一个hash值上的链表长度大于8时,该节点上的数据就不再以链表进行存储,而是转成了一个红黑树

成员变量

static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; // aka 16
/**
 * The maximum capacity, used if a higher value is implicitly specified
 * by either of the constructors with arguments.
 * MUST be a power of two <= 1<<30.
 */
static final int MAXIMUM_CAPACITY = 1 << 30;
/**
 * The load factor used when none specified in constructor.
 */
static final float DEFAULT_LOAD_FACTOR = 0.75f;
static final int TREEIFY_THRESHOLD = 8;
transient Node<K,V>[] table;
/**
 * Holds cached entrySet(). Note that AbstractMap fields are used
 * for keySet() and values().
 */
transient Set<Map.Entry<K,V>> entrySet;
/**
 * The number of key-value mappings contained in this map.
 */
transient int size;

和 1.7 大体上都差不多,还是有几个重要的区别:
1、TREEIFY_THRESHOLD 用于判断是否需要将链表转换为红黑树的阈值。
2、HashEntry 修改为 Node。

Node 的核心组成其实也是和 1.7 中的 HashEntry 一样,存放的都是 key value hashcode next 等数据。

构造方法

 //initialCapacity为初始容量,loadFactor为负载因子
public HashMap(int initialCapacity, float loadFactor) {
        //初始容量小于0,抛出非法数据异常
        if (initialCapacity < 0)
            throw new IllegalArgumentException("Illegal initial capacity: " +
                                               initialCapacity);
        //初始容量最大为MAXIMUM_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;
        //将初始容量转成2次幂
        this.threshold = tableSizeFor(initialCapacity);
    }
 
    //tableSizeFor的作用就是,如果传入A,当A大于0,小于定义的最大容量时,
  //  如果A是2次幂则返回A,否则将A转化为一个比A大且差距最小的2次幂。  
    //例如传入7返回8,传入8返回8,传入9返回16
  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;
    }
 
 
    //调用上面的构造方法,自定义初始容量,负载因子为默认的0.75
 public HashMap(int initialCapacity) {
        this(initialCapacity, DEFAULT_LOAD_FACTOR);
    }
 
 
    //默认构造方法,负载因子为0.75,初始容量为DEFAULT_INITIAL_CAPACITY=16,初始容量在第一次put时才会初始化
 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);
    }
 

HashMap的初始容量永远为2的幂次方,如果给定的初始容量不为2的幂次方,初始化的时候自动扩展成最接近给定值并大于给定值的2的幂次方。

PUT方法

final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
               boolean evict) {
    Node<K,V>[] tab; Node<K,V> p; int n, i;
    if ((tab = table) == null || (n = tab.length) == 0)   //1
        n = (tab = resize()).length;
    if ((p = tab[i = (n - 1) & hash]) == null)            //2
        tab[i] = newNode(hash, key, value, null);
    else {
        Node<K,V> e; K k;
        if (p.hash == hash &&
            ((k = p.key) == key || (key != null && key.equals(k))))  //3
            e = p;
        else if (p instanceof TreeNode)                   //4
            e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
        else {
            for (int binCount = 0; ; ++binCount) {        //5
                if ((e = p.next) == null) {
                    p.next = newNode(hash, key, value, null);
                    if (binCount >= TREEIFY_THRESHOLD - 1)//6 -1 for 1st
                        treeifyBin(tab, hash);
                    break;
                }
                if (e.hash == hash &&                     //7
                    ((k = e.key) == key || (key != null && key.equals(k))))
                    break;
                p = e;
            }
        }
        if (e != null) { // existing mapping for key      //8
            V oldValue = e.value;
            if (!onlyIfAbsent || oldValue == null)
                e.value = value;
            afterNodeAccess(e);
            return oldValue;
        }
    }
    ++modCount;
    if (++size > threshold)                               //9
        resize();
    afterNodeInsertion(evict);
    return null;
}

逻辑如下:
1、判断当前数组是否为空,空的话就需要进行初始化(resize中会判断是否进行初始化)。

2、根据当前key的hashCode定位到具体的数组下标元素中并判断当前下标元素是否为空,为空,则表明没有hash冲突就直接在当前位置创建一个Node对象即可。

3、如果当前位置有值(Hash冲突),那么就比较当前桶中的key,key的equals与写入的key是否相同,相同就赋值给e,在第8步统一进行赋值并返回。

4、如果当前桶为红黑树,那么就要按照红黑树的方式写入数据。

5、如果当前是个链表,就需要将当前的key、value封装成一个新节点写入到当前链表的后面。

6、接着判断当前链表的大小是否大于预设的阈值,大于时就要转换为红黑树。

7、如果在遍历过程中找到 key 相同时直接退出遍历。

8、如果 e != null 就相当于存在相同的 key,那就需要将值覆盖。

9、最后判断是否需要进行扩容。

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;
    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) {
            if (first instanceof TreeNode)
                return ((TreeNode<K,V>)first).getTreeNode(hash, 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、首先根据key hash之后取得所定位的桶。

2、如果桶为空则直接返回null。

3、否则判断桶的第一个位置(有可能是链表、红黑树)的 key 是否为查询的 key,是就直接返回 value。

4、如果第一个不匹配,则判断它的下一个是红黑树还是链表。

5、红黑树就按照数的查找方式返回值。

6、不然就按照链表的方式遍历匹配返回值。

Hash算法

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

hashcode的高16位和低16位进行异或,保存hashcode的高低位特征,减少hash冲突,使元素分布更加均匀。

扩容resize方法

resize的源码详解,扩容机制,单元素如何散列到新的数组中,链表中的元素如何散列到新的数组中,红黑树中的元素如何散列到新的数组中?

//上图中说了默认构造方法与自定义构造方法第一次执行resize的过程,这里再说一下扩容的过程   
 final Node<K,V>[] resize() {
        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) {   //当容量超过最大值时,临界值设置为int最大值
                threshold = Integer.MAX_VALUE;
                return oldTab;
            }
            else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY &&
                     oldCap >= DEFAULT_INITIAL_CAPACITY) //扩容容量为2倍,临界值为2倍
                newThr = oldThr << 1;
        }
        else if (oldThr > 0) // 不执行
            newCap = oldThr;
        else {                // 不执行
            newCap = DEFAULT_INITIAL_CAPACITY;
            newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY);
        }
        if (newThr == 0) {  // 不执行
            float ft = (float)newCap * loadFactor;
            newThr = (newCap < MAXIMUM_CAPACITY && ft < (float)MAXIMUM_CAPACITY ?
                      (int)ft : Integer.MAX_VALUE);
        }
        threshold = newThr;    //将新的临界值赋值赋值给threshold
        @SuppressWarnings({"rawtypes","unchecked"})
            Node<K,V>[] newTab = (Node<K,V>[])new Node[newCap];
        table = newTab;   //新的数组赋值给table
 
        //扩容后,重新计算元素新的位置
        if (oldTab != null) {   //原数组
            for (int j = 0; j < oldCap; ++j) {   //通过原容量遍历原数组
                Node<K,V> e;
                if ((e = oldTab[j]) != null) {   //判断node是否为空,将j位置上的节点
                //保存到e,然后将oldTab置为空,这里为什么要把他置为空呢,置为空有什么好处吗??
                //难道是吧oldTab变为一个空数组,便于垃圾回收?? 这里不是很清楚
                    oldTab[j] = null;
                    if (e.next == null)          //判断node上是否有链表
                        newTab[e.hash & (newCap - 1)] = e; //无链表,确定元素存放位置,
//扩容前的元素地址为 (oldCap - 1) & e.hash ,所以这里的新的地址只有两种可能,一是地址不变,
//二是变为 老位置+oldCap
                    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;
 
                      
/* 这里如果判断成立,那么该元素的地址在新的数组中就不会改变。因为oldCap的最高位的1,在e.hash对应的位上为0,所以扩容后得到的地址是一样的,位置不会改变 ,在后面的代码的执行中会放到loHead中去,最后赋值给newTab[j];
如果判断不成立,那么该元素的地址变为 原下标位置+oldCap,也就是lodCap最高位的1,在e.hash对应的位置上也为1,所以扩容后的地址改变了,在后面的代码中会放到hiHead中,最后赋值给newTab[j + oldCap]
             举个栗子来说一下上面的两种情况:
            设:oldCap=16 二进制为:0001 0000
                oldCap-1=15 二进制为:0000 1111
                e1.hash=10 二进制为:0000 1010
                e2.hash=26 二进制为:0101 1010
            e1在扩容前的位置为:e1.hash & oldCap-1  结果为:0000 1010 
            e2在扩容前的位置为:e2.hash & oldCap-1  结果为:0000 1010 
            结果相同,所以e1和e2在扩容前在同一个链表上,这是扩容之前的状态。
            
    现在扩容后,需要重新计算元素的位置,在扩容前的链表中计算地址的方式为e.hash & oldCap-1
    那么在扩容后应该也这么计算呀,扩容后的容量为oldCap*2=32 0010 0000 newCap=32,新的计算
    方式应该为
    e1.hash & newCap-1 
    即:0000 1010 & 0001 1111 
    结果为0000 1010与扩容前的位置完全一样。
    e2.hash & newCap-1 
    即:0101 1010 & 0001 1111 
    结果为0001 1010,为扩容前位置+oldCap。
    而这里却没有e.hash & newCap-1 而是 e.hash & oldCap,其实这两个是等效的,都是判断倒数第五位
    是0,还是1。如果是0,则位置不变,是1则位置改变为扩容前位置+oldCap。
            再来分析下loTail loHead这两个的执行过程(假设(e.hash & oldCap) == 0成立):
            第一次执行:
            e指向oldTab[j]所指向的node对象,即e指向该位置上链表的第一个元素
            loTail为空,所以loHead指向与e相同的node对象,然后loTail也指向了同一个node对象。
            最后,在判断条件e指向next,就是指向oldTab链表中的第二个元素
            第二次执行:
            lotail不为null,所以lotail.next指向e,这里其实是lotail指向的node对象的next指向e,
            也可以说是,loHead的next指向了e,就是指向了oldTab链表中第二个元素。此时loHead指向        
            的node变成了一个长度为2的链表。然后lotail=e也就是指向了链表中第二个元素的地址。
            第三次执行:
            与第二次执行类似,loHead上的链表长度变为3,又增加了一个node,loTail指向新增的node
               ......
            hiTail与hiHead的执行过程与以上相同,这里就不再做解释了。
            由此可以看出,loHead是用来保存新链表上的头元素的,loTail是用来保存尾元素的,直到遍            
            历完链表。   这是(e.hash & oldCap) == 0成立的时候。
            (e.hash & oldCap) == 0不成立的情况也相同,其实就是把oldCap遍历成两个新的链表,
            通过loHead和hiHead来保存链表的头结点,然后将两个头结点放到newTab[j]与 
            newTab[j+oldCap]上面去      
*/
                              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;   //尾节点的next设置为空
                            newTab[j] = loHead;
                        }
                        if (hiTail != null) {
                            hiTail.next = null;   //尾节点的next设置为空
                            newTab[j + oldCap] = hiHead;
                        }
                    }
                }
            }
        }
        return newTab;
    }

JDK1.8的HashMap底层使用的是动态数组,数组中的元素存放的是 链表或者红黑树。核心源码如下:

public class HashMap<K,V> extends AbstractMap<K,V> implements Map<K,V>, 
        Cloneable, Serializable {

    /**
     * 初始化容量16
     */
    static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; // aka 16

    /**
     * 最大容量
     */
    static final int MAXIMUM_CAPACITY = 1 << 30;

    /**
     * 扩容因子,使用的容量达到 当前容量的 75% 就扩容
     */
    static final float DEFAULT_LOAD_FACTOR = 0.75f;

    /**
     * 当前 HashMap 所能容纳键值对数量的最大值,超过这个值,则需扩容
     */
    int threshold;

    /**
     * 已使用的容量
     */
    transient int size;

    /**
     * Node数组,实际存放 键值对 的地方
     */
    transient Node<K,V>[] table;

    /**
     * 链表转红黑树的阈值,链表长度达到此值,会进化成红黑树
     */
    static final int TREEIFY_THRESHOLD = 8;

    /**
     * 系列构造方法,推荐在初始化时根据实际情况设置好初始容量,用好了可以显著减少 resize,提升效率
     */
    public HashMap(int initialCapacity, float loadFactor) {
        if (initialCapacity < 0)
            throw new IllegalArgumentException("Illegal initial capacity: " +
                                               initialCapacity);
        if (initialCapacity > MAXIMUM_CAPACITY)
            initialCapacity = MAXIMUM_CAPACITY;
        if (loadFactor <= 0 || Float.isNaN(loadFactor))
            throw new IllegalArgumentException("Illegal load factor: " +
                                               loadFactor);
        this.loadFactor = loadFactor;
        this.threshold = tableSizeFor(initialCapacity);
    }

    public HashMap(int initialCapacity) {
        this(initialCapacity, DEFAULT_LOAD_FACTOR);
    }

    public HashMap() {
        this.loadFactor = DEFAULT_LOAD_FACTOR; // all other fields defaulted
    }

    public HashMap(Map<? extends K, ? extends V> m) {
        this.loadFactor = DEFAULT_LOAD_FACTOR;
        putMapEntries(m, false);
    }

    //设置元素
    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) {
        Node<K,V>[] tab; Node<K,V> p; int n, i;
        // 初始化桶数组 table,table 被延迟到插入新数据时再进行初始化
        if ((tab = table) == null || (n = tab.length) == 0)
            n = (tab = resize()).length;
        // 如果桶中不包含键值对节点引用,则将新键值对节点的引用存入桶中即可,利用(n - 1) & hash来定位元素在桶中的位置
        if ((p = tab[i = (n - 1) & hash]) == null)
            tab[i] = newNode(hash, key, value, null);
        else {
            Node<K,V> e; K k;
            // 如果键的值以及节点 hash 等于链表中的第一个键值对节点时,则将 e 指向该键值对
            if (p.hash == hash && ((k = p.key) == key || (key != null && key.equals(k))))
                e = p;
            // 如果桶中的引用类型为 TreeNode,则调用红黑树的插入方法
            else if (p instanceof TreeNode)  
                e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
            else {
                // 对链表进行遍历,并统计链表长度
                for (int binCount = 0; ; ++binCount) {
                    // 链表中不包含要插入的键值对节点时,则将该节点接在链表的最后
                    // !!! JDK1.7中 新增的Node节点采用头插入,而JDK1.8中改成了尾插入 !!!
                    if ((e = p.next) == null) {
                        p.next = newNode(hash, key, value, null);
                        // 如果链表长度达到阈值,则进化成红黑树
                        if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
                            treeifyBin(tab, hash);
                        break;
                    }

                    // 条件为 true,表示当前链表包含要插入的键值对,终止遍历
                    if (e.hash == hash &&
                        ((k = e.key) == key || (key != null && key.equals(k))))
                        break;
                    p = e;
                }
            }

            // 判断要插入的键值对是否存在 HashMap 中
            if (e != null) { // existing mapping for key
                V oldValue = e.value;
                // onlyIfAbsent 表示是否仅在 oldValue 为 null 的情况下更新键值对的值
                if (!onlyIfAbsent || oldValue == null)
                    e.value = value;
                afterNodeAccess(e);
                return oldValue;
            }
        }
        ++modCount;
        // 键值对数量超过阈值时,则进行扩容
        if (++size > threshold)
            resize();
        afterNodeInsertion(evict);
        return null;
    }

    /**
     * 扩容为原容量的两倍,并将存在的元素 放到新的数组上
     */
    final Node<K,V>[] resize() {
        Node<K,V>[] oldTab = table;
        int oldCap = (oldTab == null) ? 0 : oldTab.length;
        int oldThr = threshold;
        int newCap, newThr = 0;
        // 如果 table 不为空,表明已经初始化过了
        if (oldCap > 0) {
            // 当 table 容量超过容量最大值,则不再扩容
            if (oldCap >= MAXIMUM_CAPACITY) {
                threshold = Integer.MAX_VALUE;
                return oldTab;
            } 
            // 按旧容量和阈值的2倍计算新容量和阈值的大小
            else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY &&
                     oldCap >= DEFAULT_INITIAL_CAPACITY)
                newThr = oldThr << 1; // double threshold
        } else if (oldThr > 0) // initial capacity was placed in threshold
            // 初始化时,将 threshold 的值赋值给 newCap,
            // HashMap 使用 threshold 变量暂时保存 initialCapacity 参数的值
            newCap = oldThr;
        else {               // zero initial threshold signifies using defaults
            // 调用无参构造方法时,桶数组容量为默认容量,
            // 阈值为默认容量与默认负载因子乘积
            newCap = DEFAULT_INITIAL_CAPACITY;
            newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY);
        }

        // newThr 为 0 时,按阈值计算公式进行计算
        if (newThr == 0) {
            float ft = (float)newCap * loadFactor;
            newThr = (newCap < MAXIMUM_CAPACITY && ft < (float)MAXIMUM_CAPACITY ?
                      (int)ft : Integer.MAX_VALUE);
        }
        threshold = newThr;
        // 创建新的桶数组,桶数组的初始化也是在这里完成的
        Node<K,V>[] newTab = (Node<K,V>[])new Node[newCap];
        table = newTab;
        if (oldTab != null) {
            // 如果旧的桶数组不为空,则遍历桶数组,并将键值对映射到新的桶数组中
            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;
                        // 遍历链表,并将链表节点按原顺序进行分组
                        do {
                            next = e.next;
                            //链表中的元素重hash后,会留在原来的桶的链表中,或者迁移到原下标+新增容量的下标下。
                            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;
    }

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

    /**
     * 根据 hash 和 key 获取相应的 Node节点
     */
    final Node<K,V> getNode(int hash, Object key) {
        Node<K,V>[] tab; Node<K,V> first, e; int n; K k;
        // 1. 定位键值对所在桶的位置,如果该位置有元素,则获取第一个元素
        if ((tab = table) != null && (n = tab.length) > 0 &&
            (first = tab[(n - 1) & hash]) != null) {
            // 如果hash和key都与 第一个元素相同,则第一个元素就是我们要获取的,直接返回
            if (first.hash == hash && 
                    ((k = first.key) == key || (key != null && key.equals(k))))
                return first;
            if ((e = first.next) != null) {
                // 2. 如果 first 是 TreeNode 类型,则调用黑红树查找方法
                if (first instanceof TreeNode)
                    return ((TreeNode<K,V>)first).getTreeNode(hash, key);
                // 3. 对链表进行查找
                do {
                    if (e.hash == hash &&
                        ((k = e.key) == key || (key != null && key.equals(k))))
                        return e;
                } while ((e = e.next) != null);
            }
        }
        return null;
    }

    /**
     * 还记HashMap底层的动态数组的定义吗 transient Node<K,V>[] table
     * 这里很明显是一个单向链表结构
     */
    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;
        }
    }

    /**
     * JDK8 加入的 红黑树TreeNode内部类,红黑树的方法比较复杂,这里只展示一些重要的
     * 属性结构代码
     */
    static final class TreeNode<K,V> extends LinkedHashMap.Entry<K,V> {
        TreeNode<K,V> parent;  // red-black tree links
        TreeNode<K,V> left;
        TreeNode<K,V> right;
        TreeNode<K,V> prev;    // needed to unlink next upon deletion
        // 颜色,true红,false黑
        boolean red;
        TreeNode(int hash, K key, V val, Node<K,V> next) {
            super(hash, key, val, next);
        }
    }
}

红黑数

红黑树是一种自平衡的二叉查找树,比普通的二叉查找树效率更高,它可在 O(logN) 时间内完成查找、增加、删除等操作。

普通的二叉查找树在极端情况下可退化成链表,导致 增、删、查 效率低下。红黑树通过定义一些性质,将任意节点的左右子树高度差控制在规定范围内,以达到平衡状态,红黑树的性质定义如下。
1、节点是红色或者黑色。
2、根是黑色的。
3、所有叶子节点都是黑色的(叶子是NIL节点)。
4、每个红色节点必须有两个黑色的子节点(从每个叶子节点到根的路径上不能有两个连续的红色节点)。
5、从任一节点到其每个节点的所有简单路径都包含相同数目的黑色节点。
红黑树的操作和其他树一样,包括查找、插入、删除等,其查找过程和二叉查找树一样简单,但插入和删除操作要复杂的多,这也是其 为保持平衡性 不会退化成链表 所付出的代价。红黑树为保持平衡性 所进行的操作主要有 旋转(左旋、右旋)和变色。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值