HashMap

1.7:

数组 + 链表:  数组就是为了存数值,链表是在数组的同一下表下存在的数值,就是在数组的同一个下标的同一个位置上,插入的数据位置上会冲突,所以在这个位置上会添加上链表,向下延申,在每次添加新的数值时,新的数值会采用头插法的方式在这条链表的第一位,这样插入的速度会快点,每次调用数值的时候,根据生成的hashcode 来找出这个数值的下表位置,再到这个链表上找对应的值。

解析源码

  • 无参构造:280行

 有个public 修饰的方法,说明这个方法我们可以直接用,里面调用了有参构造方法,里面定义的参数是 常量:参数1:默认初始化大小;参数2: 加载因子

  • 有一个 初始化方法,是空的,329行

 这个init() 方法在LinkedHashMap 中用到,因为LinkedHashMap 继承了 HashMap,在LinkedHashMap 中到时会看

  •  有参构造:250行
public HashMap(int initialCapacity, float loadFactor) {
        if (initialCapacity < 0)  // 如果设置的初始化容量小于0,就抛异常
            throw new IllegalArgumentException("Illegal initial capacity: " +
                                               initialCapacity);
        if (initialCapacity > MAXIMUM_CAPACITY)  // 如果最大容量大于内置设定的最大容量,就把HashMap 里面设置的最大容量赋值,覆盖给自己设定的最大容量
            initialCapacity = MAXIMUM_CAPACITY;
        if (loadFactor <= 0 || Float.isNaN(loadFactor))  // 如果给的不是数字,抛异常
            throw new IllegalArgumentException("Illegal load factor: " +
                                               loadFactor);

        this.loadFactor = loadFactor;  // 赋值覆盖到HashMap的最大容量,加载因子
        threshold = initialCapacity;  // 赋值链表大小
        init();
    }
  • 重载有参构造:272行
public HashMap(int initialCapacity) {
    this(initialCapacity, DEFAULT_LOAD_FACTOR);  // 设置加载因子0.75(DEFAULT_LOAD_FACTOR)
}
  •  put方法486行
public V put(K key, V value) {
    if (table == EMPTY_TABLE) {  // 判断table是不是等于空的,是空的调用下面的这个方法去初始化,解释1:下方看解释
        inflateTable(threshold);  // 初始化:下方解释2:
    }
    if (key == null)    // 这里判断key为null,走下面分支,说明key可以为null,不然的话,下面肯定会抛异常
        return putForNullKey(value);
    int hash = hash(key);  // 对key调用hash方法,算出hashCode,解释3
    int i = indexFor(hash, table.length);  // 得到hashCode以后,算出数组下表 解释4
    for (Entry<K,V> e = table[i]; e != null; e = e.next) {  // 这个循环主要是作用是,在我们用map去put多个值时候,key一样的情况下,put方法返回的值是旧的值,不是get方法,get方法是会覆盖的,第一次put进去的value,这个逻辑遍历了链表
        Object k;
        if (e.hash == hash && ((k = e.key) == key || key.equals(k))) {  // 在遍历链表时判断,hashCode相不相等,
            V oldValue = e.value;  // 如果相等,就把之前链表上存在的覆盖赋值给新定义的
            e.value = value;
            e.recordAccess(this);
            return oldValue;
        }
    }

    modCount++;
    addEntry(hash, key, value, i);  // 这个方法主要是扩容的逻辑,解释5
    return null;
}

解释1:

 调用的是这个数组的表格,这个Entry是在 801行 定义的,

 解释2:

参数 在有参构造方法看,250行

inflateTable方法:311行
private void inflateTable(int toSize) {
    // Find a power of 2 >= toSize    // 去找2的幂次方数,
    int capacity = roundUpToPowerOf2(toSize);  //比如toSize是5,那2的幂次方数就是8,如果toSize是10,2的幂次方就最少是16,就会找16这个数字
threshold = (int) Math.min(capacity * loadFactor, MAXIMUM_CAPACITY + 1);  // 重新计算阈值,扩容用的 table = new Entry[capacity]; initHashSeedAsNeeded(capacity);  // hash种子 }
roundUpToPowerOf2()方法: 301行
private static int roundUpToPowerOf2(int number) {
    // assert number >= 0 : "number must be non-negative";
    return number >= MAXIMUM_CAPACITY
            ? MAXIMUM_CAPACITY
            : (number > 1) ? Integer.highestOneBit((number - 1) << 1) : 1;  // 这个方法里面进行了多次右移位运算
}
highestOneBit(int i):这个在Integer.java类里面的一个方法,在1047行
 public static int highestOneBit(int i) {
        // HD, Figure 3-1
        i |= (i >>  1);
        i |= (i >>  2);
        i |= (i >>  4);
        i |= (i >>  8);
        i |= (i >> 16);
        return i - (i >>> 1);
    }
为什么进行这么多次位运算,在后面解释4 位置的末尾可以知道

解释3:356行

final int hash(Object k) {
    int h = hashSeed;
    if (0 != h && k instanceof String) {
        return sun.misc.Hashing.stringHash32((String) k);
    }

    h ^= k.hashCode();  // 调用这个k的hashCode方法,这个k是Object对象,可以是任意参数,拿到这个hashCode和hash种子进行异或运算

    // This function ensures that hashCodes that differ only by
    // constant multiples at each bit position have a bounded
    // number of collisions (approximately 8 at default load factor).
    h ^= (h >>> 20) ^ (h >>> 12);  // 异或运算完了以后,在这里再进行右移和异或运算,原因在解释4 末尾得知
    return h ^ (h >>> 7) ^ (h >>> 4);  // 返回一个hashCode
}

解释4:374行

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);  // 这里不是取余,而是一个与操作,算出一个数,不得大于length这个参数的值
}

其实本身与运算是不能算出数组下标的,但是为什么可以算出,在计算机底层都是使用的二进制,

一个int是4个字节,一个字节是8位,用一个随便写的数模拟短的hashCode

其实我们不难发现,高位数不管多少,跟二进制15的数(0000 1111)与运算比起来,结果绝对都在0-15之间,正好符合数组下表,不会越界,

这样的算法有一个限制,就是要先把给的数组长度减1,并且一定要是2的幂次方

如果给的初始数组长度是17的话,17 - 1 = 16,(0001 0000)

 这样显然是不好的,把拿到的数组下表给固定死了,要么是0位置,要么是16位置。

这就是为什么,默认数组长度HashMap给的是16,为什么要是2的幂次数的原因了。

这样用与操作,也会比取余的方法快很多。基于位运算的方式计算,都不会慢。

这也是为什么要进行那么多次位运算,如果不进行位运算,因为int类型的高位28位数不管怎么变,与运算最后的结果都是0,低运算位会有大量的一样的,太容易冲突了。所以要进行右移位运算。

解释5:877行

void addEntry(int hash, K key, V value, int bucketIndex) {
    if ((size >= threshold) && (null != table[bucketIndex])) {    // threshold 扩容,说明这段是扩容的逻辑,判断当前hashmap大小是否大于阈值,null != table[bucketIndex] 判断的是,当前元素所在的下表有没有值,如果不为空,说明有值,才会延申链表
     resize(2 * table.length);    // 扩容的方法,生成一个新的数组,大小是之前的数组大小的双倍,看下面方法
     hash
= (null != key) ? hash(key) : 0;
     bucketIndex
= indexFor(hash, table.length);
   }
createEntry(hash, key, value, bucketIndex);  
// 重要的是这个方法, 找下面方法
}

resize方法在 572行

void resize(int newCapacity) {
    Entry[] oldTable = table;
    int oldCapacity = oldTable.length;
    if (oldCapacity == MAXIMUM_CAPACITY) {  // 判断之前数组的大小
        threshold = Integer.MAX_VALUE;
        return;
    }
  
    Entry[] newTable = new Entry[newCapacity];  // new 出来一个新的容量的数组,也就是旧的数组大小 * 2 的大小的一个新的数组
    transfer(newTable, initHashSeedAsNeeded(newCapacity));  // 对之前的数组上的元素进行转移到新的数组上,看下面方法
    table = newTable;
    threshold = (int)Math.min(newCapacity * loadFactor, MAXIMUM_CAPACITY + 1);
}
transfer方法在589行
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);  // 再次去调用indexFor方法
            e.next = newTable[i];  
            newTable[i] = e;  // 把数组下表给到新的数组的下标位置去
            e = next;
        }
    }
}

895行

void createEntry(int hash, K key, V value, int bucketIndex) {
    Entry<K,V> e = table[bucketIndex];  // 取数组当前数组链表上面的元素
    table[bucketIndex] = new Entry<>(hash, key, value, e);  // 把得到的元素e,放到新的Entry对象里的next属性,就是把新加的元素头插法,放在数组的链表上了
    size++;  // size代表链表存多少个元素,因为多存了一个,所以就++
}

所以size方法,返回的就是size了,在384行

public int size() {
        return size;
    }

这样效率就非常高了,我们调用size方法时候,根本不是取循环遍历大小得到的,而是这样实现就已经知道了它的大小。

 

  • get方法 在414行
public V get(Object key) {
    if (key == null)  // 判断key是否是null
        return getForNullKey();
    Entry<K,V> entry = getEntry(key);  // 最主要是这个getEntry方法

    return null == entry ? null : entry.getValue();  // 不是null,就返回entry对象
}

getEntry方法,在457行

final Entry<K,V> getEntry(Object key) {
    if (size == 0) {
        return null;
    }

    int hash = (key == null) ? 0 : hash(key);  // 通过key 得到hashCode值
    for (Entry<K,V> e = table[indexFor(hash, table.length)];  // 通过indexFor方法得到所对应的数组下表,遍历下表下的这个链表
         e != null;
         e = e.next) {
        Object k;
        if (e.hash == hash &&
            ((k = e.key) == key || (key != null && key.equals(k))))  // 先比较hash值,再比较key,就得到了对应的元素
            return e;
    }
    return null;
}

 

1.8:

在1.8时候的HashMap中,又加入了一个红黑树,当链表上的元素达到8个以后,就会开启红黑树;

为什么加入的是红黑树?为什么不是其他的树?因为在1.7时候只有链表,链表在插入的时候效率高,在拿数据的时候效率就没那么高了,所以在各种树的结构选型时候,红黑树比较均衡,因为我们不管要考虑插入的效率,还要考虑get时拿到数据的效率。红黑树在这两者方面都还挺不错的,比较折中,所以选择红黑树。

447行:有参构造方法: 和之前1.7一样的判断、传参,主要还是看put方法

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

红黑树的设置的值:

  •  8的阈值:代表链表上的元素达到8个,就开启红黑树。  
  •  6的阈值:反向的,转为链表时的阈值。代表红黑树上的元素只有6个的时候,就会转成链表。因为数组当前下表的元素不多的时候,为了提高插入效率,比较折中。

        为什么在方向转为链表时候不是8的这个阈值?

        如果在8的大小这个元素数量的时候,一直删除一个,添加一个,这样子不停的进行红黑树转换,非常影响HashMap的效率。

 

put方法:611行

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

第一点:先对key进行hash值运算

 337行:hash方法:还是1.7一样,对hashCode这个hash值进行右移,然后异或运算,只是没有1.7里面写的那么复杂,为什么?

    1.7时候那么复杂只是为了提高hash运算结果的散列性,提高散列性是为了插入和查询效率,而1.8时候用了红黑树,所以就没必要进行那么复杂的运算了。不过右移和异或运算还是要有的。

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

 625行:putVal方法:

 1 final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
 2                boolean evict) {
 3     Node<K,V>[] tab; Node<K,V> p; int n, i;      // 申明tab; 申明一些数组下表、节点
 4     if ((tab = table) == null || (n = tab.length) == 0)    // 判断当前table是不是空,数组长度是不是0,是不是空的
 5         n = (tab = resize()).length;            // 如果是空的,初始化,用resize() 这个方法,得到n
 6     if ((p = tab[i = (n - 1) & hash]) == null)      // 用n-1 与运算这个hash值,得到数组下表,和1.7原理一样的,取出下标得到对应的元素,赋值,看这个元素是不是空
 7         tab[i] = newNode(hash, key, value, null);    // 如果当前数组的这个位置上是空的,就把这个新的节点元素放在这个对应的数组下标上,这个Node就和1.7里面的Entry是一样的;
 8     else {            // 如果在这个数组当前位置上找的下标位置不为空,走下面逻辑:29行
 9         Node<K,V> e; K k;
10         if (p.hash == hash &&
11             ((k = p.key) == key || (key != null && key.equals(k))))    // 判断key的hash值是否等于要插入的新的这个hash值,
12             e = p;                              // 如果相等就赋值,不等就直接走下面分支
13         else if (p instanceof TreeNode)                 // 如果遍历出来的数组上的元素p,判断p是不是树节点
14             e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);  // 如果是树,就去遍历这棵树
15         else {                              // 如果不是树,就执行下面的逻辑,遍历链表
16             for (int binCount = 0; ; ++binCount) {
17                 if ((e = p.next) == null) {            // 判断遍历到的是不是链表的尾节点了
18                     p.next = newNode(hash, key, value, null);    // 如果是尾节点,就把新的Node节点加到尾部,尾插法。。。
19                     if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st  插入到尾部之后,在判断是不是大于设定的最大阈值-1
20                         treeifyBin(tab, hash);              // 如果大于,再去树化
21                     break;
22                 }
23                 if (e.hash == hash &&      // 在遍历链表时候,判断hash值有没有相等的,key有没有相等的
24                     ((k = e.key) == key || (key != null && key.equals(k))))
25                     break;
26                 p = e;
27             }
28         }
29         if (e != null) { // existing mapping for key    这里代码和1.7的是一模一样了,返回OldValue
30             V oldValue = e.value;
31             if (!onlyIfAbsent || oldValue == null)
32                 e.value = value;
33             afterNodeAccess(e);
34             return oldValue;
35         }
36     }
37     ++modCount;
38     if (++size > threshold)    // 判断是否大于阈值,大的话就去扩容,执行resize() 方法
39         resize();
40     afterNodeInsertion(evict);
41     return null;
42 }

* 就算这链表长度大于8时候,也不一定转红黑树:

树化的方法:755行

final void treeifyBin(Node<K,V>[] tab, int hash) {
    int n, index; Node<K,V> e;
    if (tab == null || (n = tab.length) < MIN_TREEIFY_CAPACITY)    // MIN_TREEIFY_CAPACITY 64, 转红黑树之前,先判断tab是不是空,或者tab的长度小不小于64,再去扩容,执行下面resize 方法去扩容,没有去树化
        resize();
    else if ((e = tab[index = (n - 1) & hash]) != null) {
        TreeNode<K,V> hd = null, tl = null;    // 树化
        do {
            TreeNode<K,V> p = replacementTreeNode(e, null);
            if (tl == null)
                hd = p;
            else {
                p.prev = tl;
                tl.next = p;
            }
            tl = p;
        } while ((e = e.next) != null);
        if ((tab[index] = hd) != null)
            hd.treeify(tab);
    }
}

为什么?

因为扩容的话,也可以大范围的缩短这个链表,扩容一举两得,有更多的存储空间,并且基于更长远的路,来解决这个问题。

那为什么不一直扩容,就像1.7那样?

因为如果链表是在太大的话,要用红黑树,节省内存空间,这样做是为了中恒一下。

 

posted @ 2020-11-25 11:15  aBiu--  阅读(54)  评论(0编辑  收藏  举报