HashMap

HashMap

变量

成员变量Filed

// HashMap最基本的成员标量table数组,对象是Node。其初始化是指向同一个地址(享元模式)
transient Node<K,V>[] table;
// Map.Entry是Map接口的内部接口(嵌套接口)
transient Set<Map.Entry<K,V>> entrySet;
//Note that AbstractMap fields are used for keySet() and values().
transient int size;
// 凡是我们做的增删改都会引发modCount值的变化,跟版本控制功能类似,适用于Fail-Fast机制。
// modCount实现了Fail-Fast机制可以抛出ConcurrentModificationException异常。
transient int modCount;
// 阈值非常重要的一个概念,影响put,resize等操作。 threshold = initialCapacity*loadFactor
int threshold;
final float loadFactor;

静态变量

关于map

// 序列化id
private static final long serialVersionUID = 362498820763181265L;
// 默认初始化map容量为16
static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; // aka 16
// 最大容量2的30次方
static final int MAXIMUM_CAPACITY = 1 << 30;
// 默认装载因子 0.75
static final float DEFAULT_LOAD_FACTOR = 0.75f;

关于list<--->tree

// list转化为tree阈值为8,设置为8,是系统根据泊松分布的数据分布图来设定的。
static final int TREEIFY_THRESHOLD = 8;
// tree退化为list阈值为6,当在扩容(resize())时(此时HashMap的数据存储位置会重新计算),在重新计算存储位置后,当原有的红黑树内数量 < 6时,则将 红黑树转换成链表
static final int UNTREEIFY_THRESHOLD = 6;
// 最小树形化容量阈值:即只有Map容量大于64才允许treeify,否则若桶内元素太多时,则直接扩容,而不是树形化
static final int MIN_TREEIFY_CAPACITY = 64;

注释:resize()有两种情况:1.当前map的容量大于阈值 2. 容量小于64 但是一个bin达到了8

内部类

Node

table使用的就是Node数组,每个Node节点包括 hash, key, value,next。这个类其实是一个链表,而他的本质就是对Map.Entry的实现。

    static class Node<K,V> implements Map.Entry<K,V> {
        final int hash;
        final K key;
        V value;
        Node<K,V> next;
        // ...   
    }

补充:TreeNode和Node的关系。

视图内部类

包括KeySetValuesEntrySet三种视图。其关系入下图所示:

image-20200619205938663

以KeySet为例:

final class KeySet extends AbstractSet<K> {
    public final int size()                 { return size; }
    public final void clear()               { HashMap.this.clear(); }
    public final Iterator<K> iterator()     { return new KeyIterator(); }
    public final boolean contains(Object o) { return containsKey(o); }
    public final boolean remove(Object key) {
        return removeNode(hash(key), key, null, false, true) != null;
    }
    public final Spliterator<K> spliterator() {
        return new KeySpliterator<>(HashMap.this, 0, -1, 0, 0);
    }
    public final void forEach(Consumer<? super K> action){//...}

迭代器内部类

包括抽象类父类HashIterator。和继承抽象类的子类KeyIteratorValueIteratorEntryIterator

其中有一点HashIterator居然没有实现Iterator接口。而剩下的三个子类则实现了Iterator接口。

abstract class HashIterator {
    Node<K,V> next;        // next entry to return
    Node<K,V> current;     // current entry
    int expectedModCount;  // for fast-fail
    int index;     
    HashIterator() {
        //...
    }
	public final boolean hasNext(){
        //...
    }        
    final Node<K,V> nextNode() {
		//...
        }

    public final void remove() {
        //...
    }
}
final class KeyIterator extends HashIterator
    implements Iterator<K> {
    public final K next() { return nextNode().key; }
}

final class ValueIterator extends HashIterator
    implements Iterator<V> {
    public final V next() { return nextNode().value; }
}

final class EntryIterator extends HashIterator
    implements Iterator<Map.Entry<K,V>> {
    public final Map.Entry<K,V> next() { return nextNode(); }
}

分割迭代器内部类

包括:HashMapSpliterator,KeySpliterator,ValueSpliterator,EntrySpliterator。类似于迭代器。

方法

成员方法

初始化🙂

初始化可以分两种方式,虽然很简单,但这两种方式对于后面put,resize()都有极其重要的影响。后面的很多判断条件都是根据这里的情况来的。因此要分清楚~

初始化方法一
// 对于 HashMap map = new HashMap<>(); 这种方式
public HashMap() {
    this.loadFactor = DEFAULT_LOAD_FACTOR; // all other fields defaulted
}

// 第二种实现了Map接口addAll的初始化本质上分两步,第一步初始化,第二步放entry
public HashMap(Map<? extends K, ? extends V> m) {
    this.loadFactor = DEFAULT_LOAD_FACTOR;
    putMapEntries(m, false);
}

很显然这种方式,啥都没设置。其实系统也啥也没给你,只给你确定了装载因子为0.75。其他的重要变量(主要是在resize()中会用到的):

table = null;  // table.length更无从谈起
threshold = 0;  // 阈值也是默认值 为0
modCount = 0; //本就应该是0
size = 0;  // 也是一个很重要的量 判断当前容量中实际使用的空间大小
entrySet = null;  // keySet() and values()同理
初始化方法二
public HashMap(int initialCapacity) {
    this(initialCapacity, DEFAULT_LOAD_FACTOR);
}
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);
}

这两个初始化方法可以算一个, 都是必须填写初始化容量大小的,如果不填装载因子就是默认为0.75。既然你填写了初始化容量大小,就可能与DEFAULT_INITIAL_CAPACITY不一样。

如果是用方法二创建了一个容量大小为31的HashMap,实际上 你是只是设置了你的初始阈值为32,而这个时候你的table依然为null。

情况一:你第一次put,这个时候你的table==null 你需要一个newCap

情况二:你put的时候连续命中同一个Bin,也就是说你有8个对象的HashCode相同,而这个时候你的size又小于treeify的最小值64.你resize,需要一个newCap

情况三: 在你添加了一个后,你的size大于threshold了,这个时候你需要resize()。即你需要newCap。

get

put🙂

  1. 继承Map的put接口:

    public V put(K key, V value) {
        return putVal(hash(key), key, value, false, true);
    }
    
  2. HashMap真正的实现方法是putVal:

    1. 假设我们创建了一个空的HashMap,而目前是第一次put即:

      Map<String,String> map = new HashMap<>();
      map.put("123","123");
      
    2. 这个时候的table肯定是null,因此需要调整resize()而默认就是容量就是16。

    3. 因此是新建的map,table里面的数组中每个Node都是空的,因此在put的时候需要创建一个新的node(newNode),

    4. 否则,table中该下标的头节点是存在的。创建两个变量Node e 和key k,使用k进行存在性判断,使用e保存put进来的结果。

      1. 判断头结点的hash值是和要put的节点的hash值相同。
      2. 如果p等于put进来的key,则使用e保存p
      3. 如果p不等于key,则说明该节点不等于头结点,则继续判断是否存在List或者tree当中。因此先判断head是不是TreeNode。如果是TreeNode则放入黑红树当中进行判断或者添加(putTreeVal
      4. 如果不是TreeNode则在链表中判断(一个for循环遍历即可)
      5. 如果bin是一个链表,并且节点不是该bin下面的任何一个节点,则创建一个节点添加在后面的链表中。注意这个时候的链表是包含新添加进来的节点的!!!通过更新binCount并判断是否大于等于8,进而执行treeifyBin。然后完成添加。而这个时候刚好就把e赋值为了null。
      6. 其中在treeifyBin先进行判断,是否达到了化成树的最小容量(8*8 = 64),如果达到,则变成树,没有则resize()
      7. 最后再次判断size有没有超过阈值,如果超过则resize()这个resize则是因为size的resize而不是上面因此单个链表超过8可能导致的resize。
      8. 最最后,调用回调函数afterNodeInsertion
     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)
                // 初始化为table==null的时候,第一次put才真正的创建空间为16的容量map
                n = (tab = resize()).length;
            if ((p = tab[i = (n - 1) & hash]) == null)
                // 如果数组table在下标为i的地方为空,则创建一个新的节点(相当于链表头)
                tab[i] = newNode(hash, key, value, null);
            else {
                Node<K,V> e; K k;
                // 判断头结点的hash值是和要put的节点的hash值相同,将key赋值给临时遍历
                if (p.hash == hash &&
                    ((k = p.key) == key || (key != null && key.equals(k))))
                    e = p;
                // 判断头结点是不是树节点
                else if (p instanceof TreeNode)
                    e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
               // 否则就是链表节点
                else {
                    // 判断key是否存在于Node这个链表中,如果不存在则添加进去,添加的时候看是否达到treeify的阈值,如果达到还要看整体容量是否达到,如果没有达到则resize否则treeify。
                    for (int binCount = 0; ; ++binCount) {
                        if ((e = p.next) == null) {
                            p.next = newNode(hash, key, value, null);
                            if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
                                treeifyBin(tab, hash);
                            break;
                        }
                        if (e.hash == hash &&
                            ((k = e.key) == key || (key != null && key.equals(k))))
                            break;
                        p = e;
                    }
                }
                
    			// 如果不存在则上面最后e会指向null,如果存在e则会指向对应的key,这里相当于修改key的value
                if (e != null) { // existing mapping for key
                    V oldValue = e.value;
                    if (!onlyIfAbsent || oldValue == null)
                        e.value = value;
                    // 调用回调函数afterNodeAccess,里面其实是一个空的函数体。
                    afterNodeAccess(e);
                    return oldValue;
                }
            }
         // 最后再次判断size有没有超过阈值,如果超过则resize()这个resize则是因为size的resize而不是上面因此单个链表超过8可能导致的resize。
            ++modCount;
            if (++size > threshold)
                resize();
         // hashMap自带的几个回调函数
            afterNodeInsertion(evict);
            return null;
        }
    

resize()🙂

什么情况会resize()?
  1. oldCap>0的情况,什么情况是oldCap大于0的,就是已经存在table的时候又需要resize()的情况:
    1. size > threshold,那如何设置阈值=>resize()的时候或者实例化的时候。这个时候的oldCap是大于0的
    2. 想要treeifyBin但able.length<MIN_TREEIFY_CAPACITY的时候。这个时候的oldCap也是大于0的
  2. table.length==0 || table==null且需要put的时候。这个时候根据初始化的方式不同,分为两种情况resize()。
    1. 默认容量大小和装载因子的。
    2. 自定义容量大小的,装载因子可能是默认可能不是默认的情况的。
// 情况1.1  和情况1.2 :直接按照cap为2的n次方。thr正常翻倍即可。
    if (oldCap > 0) {
        if (oldCap >= MAXIMUM_CAPACITY) {
            threshold = Integer.MAX_VALUE;
            return oldTab;
        }
        else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY &&
                 oldCap >= DEFAULT_INITIAL_CAPACITY)
            newThr = oldThr << 1; // double threshold
    }
// 情况2.1: 
// else if 可以理解为(else => oldCap = 0) && oldThr>0 这表明 这个是刚初始化没有table没有oldCap情况下的 自己设置阈值的初始化方式!!!根据前面的tableSizeFor设置阈值为2^n,这里直接将他拿来作为新容量,同时计算在该容量下自定义的阈值(因为装载因子可能是自定义的)
    else if (oldThr > 0) // initial capacity was placed in threshold
        newCap = oldThr;
// 情况2.2:
// else 可以理解为 oldCap = 0 && oldThr = 0 这表明使用了只设置了装载因子为0.75的初始化条件。
    else {          
        newCap = DEFAULT_INITIAL_CAPACITY;
        newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY);
    }
// 这个是上面else if(oldThr>0)的继续内容。不知道为啥没有写在一起。
    if (newThr == 0) {
        float ft = (float)newCap * loadFactor;
        newThr = (newCap < MAXIMUM_CAPACITY && ft < (float)MAXIMUM_CAPACITY ?
                  (int)ft : Integer.MAX_VALUE);
    }
如何resize()?
  1. 创建并赋值两个临时变量newCap和newThr。(赋值算法见上 什么情况会resize()

  2. 根据newCap创建数组 newTab 并让成员变量table 指向newTab。同时让newThr赋值给静态成员变量threshold

  3. 如果oldTab不为空,则将oldTsab的内容深拷贝到newTab中。

    1. 创建临时变量e = oldTab[j],oldTab[j] = null后一句是为了帮助GC?
    2. rehash=> newTab[e.hash & (newCap - 1)] = e;
    3. 如果oldTab[index].next == null则只需要把这个head rehash了。
    4. 如果他的next不为null,再判断这个节点是TreeNode还是Node。
      1. 如果是TreeNode,则((TreeNode<K,V>)e).split(this, newTab, j, oldCap);
      2. 如果是Node,则判断该Node在hash table扩容前后位置index是否改变。【见补充1】
        1. 如果与运算为0使用loHeadloTail保存,然后直接放到低位table[j]下面。
        2. 如果与运算为1则使用hiHeadhiTail链表保存,然后存放在高位。
	// 已经拿到了newThr 和newCap的值以后
    // 静态变量设置为新阈值
        threshold = newThr;
    
    // 根据newCap创建数组 newTab 并让成员变量table 指向newTab
        @SuppressWarnings({"rawtypes","unchecked"})
        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; // 用于保存put后不移位的链表
                        Node<K,V> hiHead = null, hiTail = null; // 用于保存put后移位的链表
                        Node<K,V> next;
                        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;
                            newTab[j] = loHead;
                        }
                        if (hiTail != null) {
                            hiTail.next = null;
                            newTab[j + oldCap] = hiHead;
                        }
                    }
                }
            }
        }
        return newTab;
    }

进一步阅读补充2:HashMap在并发情况下可能出现的问题。

静态方法(类方法)

hash🙂

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

确认hashmap的索引位置,h表示key的hashCode值(类型是int,一共4Byte 4*8 = 32位),而hash值则使用hashCode()的高16位异或低16位实现的。主要是从速度、功效、质量来考虑的,这么做可以在数组table的length比较小的时候,也能保证考虑到高低Bit都参与到Hash的计算中,同时不会有太大的开销。

而在存入table的时候则使用hash值对 len求余:h&(len-1)(hash%length==hash&(length-1)的前提是length是2的n次方)。举个例子,当n为默认值16的时候,该hashcode值的最小四位为1010,但是通过异或变成了0101

tableSizeFor

该方法是用于计算当前容量下对应的map的容量值(map的容量是2的次方)

// 找到距离他最近的2的阶乘的那个值比如17 返回32, 16 返回16, 15 返回16。
static final int tableSizeFor(int cap) {
    // -1的二进制反码表示为 11111111 11111111 11111111 11111111
    // -1 >>> 这么多位 表示 前面有多少个0然后后面全是1。
    // 也就是说 n 表示 cap当前对应的二进制数 -1
    int n = -1 >>> Integer.numberOfLeadingZeros(cap - 1);  // numberOfLeadingZeros判断二进制条件下这个数值前面有多少个0,比如15前面就有28个0。
    //        System.out.println("n:"+n); // 16 - 1 = 15 -> 前面有 28个0 -> n=(1 >> (32 - 28))-1 =(1 >> 4 )-1= 15
    return (n < 0) ? 1 : (n >= MAXIMUM_CAPACITY) ? MAXIMUM_CAPACITY : n + 1;  // 15 + 1 = 16
}

比较方法

put:表示添加或者修改。如果key存在,则修改value,如果key不存在,则直接添加这一对ky。

e

补充

newCap下的ReHash问题

背景:如果一个Node在扩充前的table中位于table[j],那么再扩容两倍后,他应该在table的那个下标中?
假设:oldCap = 16 newCap = 32; j = 7(即hashCode应该为 7 23 39 55 71 ... )

native方法:
* 验证两个 7 和 23 在oldCap是同一个位置
	j = (oldCap - 1) & hashCode = 0000 1111 & 0000 0111 = 0000 0111 = 7
	j = (oldCap - 1) & hashCode = 0000 1111 & 0001 0111 = 0000 0111 = 7
* 求解newCap下的位置(试验对象7 23 39 55):
	j = (newCap - 1) & hashCode = 0001 1111 & 0000 0111 = 0000 0111 = 7
	j = (newCap - 1) & hashCode = 0001 1111 & 0001 0111 = 0001 0111 = 23
	j = (newCap - 1) & hashCode = 0001 1111 & 0010 0111 = 0000 0111 = 7
	j = (newCap - 1) & hashCode = 0001 1111 & 0011 0111 = 0011 0111 = 23

很明显可以看出来当oldCap -> newCap的时候,一定会有一半的(从概率的角度)Node位于原来所在的head一半的去了新的Head,源代码中使用loHead和hiHead来表示。而这里就是7和23。其下标很明显得:
	loHead -> index = j;
	hiHead -> index = j + oldCap;

* 代码中求解newCap下的位置的方法:
我们很明显会发现,只要是同一个下标j的bin中,那么oldCap范围内(低位)的二进制数是一样的,比如上述例子都是0111。区别在于高位(前1位)是1还是0的问题。这个规律一发现,就会算法就会容易理解很多,这个判断语句应该这么写:
	if(oldCap & hashCode == 0) 
	// 0001 0000 & 0001 0111 = 1  -> 23 高位
    // 0001 0000 & 0000 0111 = 0  ->  7 低位
    // 这样就实现了只比较高位部分是否为1,而后续其实不用管的算法了!

HashMap在并发情况下可能出现的问题

JDK8已经很好的解决了JDK7HashMap代码中resize()死循环的问题。但是依然不是线程安全的,以put过程为例,其中有这么一句:

if ((p = tab[i = (n - 1) & hash]) == null) // 如果没有hash碰撞则直接插入元素
    tab[i] = newNode(hash, key, value, null);

如果使用多线程,这个地方就可能出现数据覆盖的问题。

背景2: 假设有两个线程对一个大小size=10的map添加元素。

而在++size的时候,这也是三个步骤,分别是取值,+1和赋值的操作。假设取值后被打断,然后线程2也size+1,完成后,你size取值为10,然后+1 结果还是11。这样两个线程都+1但是最终的结果size却是11。这样会由于数据覆盖又导致了线程不安全。

Node和TreeNode的关系

我们知道Node是HashMap中对于Map接口中的Entry子接口的(内部)实现类。而TreeNode则是继承了超类LinkedHashMap.Entry的子类。

static class Node<K,V> implements Map.Entry<K,V> {
    // ...
}
static final class TreeNode<K,V> extends LinkedHashMap.Entry<K,V> {
    // ...
}

那么HashMap.NodeHashMap.TreeNodeLinkedHashMap.Entry之间是什么关系呢?通过点入LinkedHashMap可以发现:Node又是HashMap的超类:

static class Entry<K,V> extends HashMap.Node<K,V> {
    Entry<K,V> before, after;
    Entry(int hash, K key, V value, Node<K,V> next) {
        super(hash, key, value, next);
    }
}

所以HashMap.Node是LinkedHashMap.Entry的超类,而LinkedHashMap.Entry又是TreeNode的超类。

阅读源码我们会发现,HashMap.Node是单向链表的节点,而LinkedHashMap.Entry是双向链表的节点。TreeNode是包含前驱节点的,红黑树节点。

image-20200619204222316

JDK7中的hashMap

  • 添加元素
void addEntry(int hash, K key, V value, int bucketIndex) {
    Entry<K,V> e = table[bucketIndex];
    table[bucketIndex] = new Entry<>(hash, key, value, e);
    if (size++ >= threshold)  // 和jdk8一样,他是先添加,再扩容的。
        resize(2 * table.length);
}

// 明显就把新添加的元素放在head的位置。 head.next = table[bucketIndex]
Entry(int h, K k, V v, Entry<K,V> n) {
    value = v;
    next = n;
    key = k;
    hash = h;
}
// resize主要内容包括创建newCap的数组,迁移并赋值到新的table,新的thr
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];
    transfer(newTable);
    table = newTable;
    threshold = (int)(newCapacity * loadFactor);
}
// 关键函数,迁移函数。原始函数就是直接从类成员变量中得到。
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; // 帮助GC?
            do {
                Entry<K,V> next = e.next;  // e 表当前链表指向的位置,next是下一个位置
                int i = indexFor(e.hash, newCapacity); // 求当前Entry的index
                e.next = newTable[i]; // 当前Entry的next = newTable[i] (头插法)
                newTable[i] = e; // 然后newTable[i] = e
                e = next;
            } while (e != null);
        }
    }
}

该代码中线程不安全举例:

假设现在有线程一和线程二,都同时希望对一个bucket进行扩容,那么扩容步骤如下所示:

image-20200619200944774

假设我们线程一创建了遍历e和next并指向了对应Node,这个时候线程二执行了:

image-20200619201058362

线程二如上所所示已经完成了resize(),但是线程一resize()只完成了一半,于是线程一继续resize():

初始状态:

image-20200619201228604

第一步:先执行 newTable[i] = e; 然后让e = next 并让他进行头插法,插入线程一中的头部(把7插入了头部,同时7的下一个指向了3)。

image-20200619201245168

第二步:让3插入头部,然后让3指向7。这个时候 循环链表 成了!~

image-20200619201258527

posted @ 2020-07-10 15:16  SsoZh  阅读(193)  评论(0编辑  收藏  举报