hashmap分析

3.HashMap

组成关系
image

3.1在哪个包下

package java.util;

3.2类的继承关系

image

  • Cloneable空接口,表示可以克隆,创建并返回HashMap对象的一个副本;
  • Serializable序列化接口,属于标记性接口,HashMap对象可以被序列化和反序列化;
  • AbstractMap父类提供了Map实现接口,以最大限度地减少实现此接口所需的工作;

3.2成员变量

 static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; // aka 16		//默认初始容量为16 (1左移4位)。
 static final int MAXIMUM_CAPACITY = 1 << 30;		//最大容量。
 static final float DEFAULT_LOAD_FACTOR = 0.75f;	//构造函数中未进行指定的使用的一个负载因子。
 static final int TREEIFY_THRESHOLD = 8;			//链表节点转换为红黑树节点的一个阈值。
 static final int UNTREEIFY_THRESHOLD = 6;			//红黑树节点转为链表节点的阈值。
 static final int MIN_TREEIFY_CAPACITY = 64;		//树化的最小容量为64,也就是哈希表的长度。

3.3为什么默认加载因子为0.75f

为了提高空间利用率和增加查询效率的折中。主要是泊松分布,0.75的话碰撞最小。
如果负载因子比较小,因为阈值等于哈希表的长度*负载因子。(阈值是哈希表中可以存储元素的个数)。哈希表能存的元素个数就更少,那么相应的发生哈希冲突的概率也就降低了,所以查询效率也就提高。但是由于增加相同数量的元素,需要的哈希桶的个数就要进行增加。于是就多次调用扩容方法,扩容会重构哈希结构,这个过程很慢,空间利用率就降低了。为了在时间和空间上有一个折中的结果,满足泊松分布取0.75是一个比较理想的取值。

3.4为什么哈希桶中的节点超过8才转为红黑树?

将链表转为红黑树是为了提高查询的效率,但是这样也带来了一些问题,树节点的大小是普通节点的2倍。
在节点数量较低时,维护红黑树结构的成本是不低于查询成本的,所以此时不值得进行转换。
源码中也有解释 :
在使用分布良好的hashcode时,很少使用红黑树结构,理想情况下,在随机哈希码下,箱子中节点的频率服从泊松分布,从下面源码的概率值可以看出,一个哈希桶中链表长度达到8个元素的概率为0.00000006,这几乎是一个不可能事件。因此选择8,不是随便就决定的,而是根据概率统计决定的。还是得学好数学啊。
 * Because TreeNodes are about twice the size of regular nodes, we
     * use them only when bins contain enough nodes to warrant use
     * (see TREEIFY_THRESHOLD). And when they become too small (due to
     * removal or resizing) they are converted back to plain bins.  In
     * usages with well-distributed user hashCodes, tree bins are
     * rarely used.  Ideally, under random hashCodes, the frequency of
     * nodes in bins follows a Poisson distribution
     * (http://en.wikipedia.org/wiki/Poisson_distribution) with a
     * parameter of about 0.5 on average for the default resizing
     * threshold of 0.75, although with a large variance because of
     * resizing granularity. Ignoring variance, the expected
     * occurrences of list size k are (exp(-0.5) * pow(0.5, k) /
     * factorial(k)). The first values are:
     *
     * 0:    0.60653066
     * 1:    0.30326533
     * 2:    0.07581633
     * 3:    0.01263606
     * 4:    0.00157952
     * 5:    0.00015795
     * 6:    0.00001316
     * 7:    0.00000094
     * 8:    0.00000006
     * more: less than 1 in ten million

3.5构造方法

3.5.1指定初始容量和指定负载因子

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

//返回给定目标容量的 2 次方。
static final int tableSizeFor(int cap) {
        int n = cap - 1;	//对传入的cap进行减1操作,是为了防止cap已经是2的幂次方。
        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;
    }

假设输入的值为cap=10
    int n = cap - 1;//cap=10  n=9
n |= n >>> 1;
	00000000 00000000 00000000 00001001 //9
	00000000 00000000 00000000 00000100 //9 >>> 1 = 4
-------------------------------------------------
	00000000 00000000 00000000 00001101 //9 | (9 >>> 1) = 13
	
 n |= n >>> 2;//n = 13
	00000000 00000000 00000000 00001101  //13
    00000000 00000000 00000000 00000011  //13 >>> 2 = 3
-------------------------------------------------
	00000000 00000000 00000000 00001111 //13 | (13 >>> 2) = 15
        
//接下去的移位也是类似,移4位、8位、16位,保证在2 ^ 30内能达到目标即可
//此时已经得到我们想要的结果,所以就不继续下去了。这里我做下补充,如果在继续15 | 15>>>4 得到的结果还是15,最后的几步就相当于是没有变化,最后按下面的巧妙的方法得到的值就是16了。不得不说真的很巧妙。

//判断最终结果,总之就是一个很巧妙的方法
(n < 0) ? 1 : (n >= MAXIMUM_CAPACITY) ? MAXIMUM_CAPACITY : n + 1;

3.5.2指定一个初始容量和默认加载因子【推荐使用】

 public HashMap(int initialCapacity) {
        this(initialCapacity, DEFAULT_LOAD_FACTOR);
    }
//这里默认会去调用3.5.3中的构造方法

3.5.3空参构造方法【经常使用】

public HashMap() {
        this.loadFactor = DEFAULT_LOAD_FACTOR; // 默认16 和 0.75f
    }

3.5.4构造参数是map的构造方法

public HashMap(Map<? extends K, ? extends V> m) {
        this.loadFactor = DEFAULT_LOAD_FACTOR;	//默认为0.75f
        putMapEntries(m, false);
    }
//----------------------------------------------------------------------------
final void putMapEntries(Map<? extends K, ? extends V> m, boolean evict) {
        int s = m.size();
        if (s > 0) {
            if (table == null) { // pre-size	如果原来的同还没有初始化
                float ft = ((float)s / loadFactor) + 1.0F;	//计算需要的桶的大小
                int t = ((ft < (float)MAXIMUM_CAPACITY) ?
                         (int)ft : MAXIMUM_CAPACITY);
                if (t > threshold)			//如果计算出来的大于原来的计算出来的扩容大小,则把新计算出来的大小进行赋值,其实就是重新赋值
                    threshold = tableSizeFor(t);
            }
            else if (s > threshold)	//判断当前的桶的大小可不可以存下m的所有的元素
                resize();		//不能存下则从新扩容
            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);
            }
        }
    }

3.6 hash()方法

该方法是hashmap用来定位元素存储的位置的一个重要方法。我们希望HashMap里面的元素位置尽量分布均匀,因为如果很多元素都在某一个位置,那么我们定位到哈希桶后,还需要继续遍历该位置下的链表或者红黑树,导致效率降低,所以应尽量使得每个哈希桶内的元素只有一个,那么当我们用hash算法求得这个位置的时候,马上就可以知道对应位置的元素就是我们要的,而不用遍历链表或者红黑树,大大优化了查询的效率。

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

我来继续讲解这个return里面的三目运算。这里的hashCode()是直接调用的本地方法,没有进行重写,得到的h再与h无符号右移16的结果进行异或运算,让高位参与运算,会让得到的hashcode的值更加的散列。

但是为什么进行无符号右移16呢?

 //	>>> 无符号右移,忽略符号位,空位都以0补齐   
//	按二进制形式把所有的数字向右移动对应位数,低位移出(舍弃),高位的空位补零。对于正数来说和带符号右移相同,对于负数来说不同。

为什么要采用^,不是&或者|呢?

我们来看看这三种情况:

image

​ 使用异或就会让返回的hashCode的值更加的分散,使用与运算(&)会让hashCode的值偏向于0,或运算(|)会让hashCode的值偏向于1。我们的目的不就是获得一个比较分散的值吗? 那肯定使用亦或呀(o゚▽゚)o

3.7 put()方法

public V put(K key, V value) {
        return putVal(hash(key), key, value, false, true);
    }
//----------------------上面的主要是调用这个方法-----------------------------------------
// onlyIfAbsent:true代表不更改现有的值,否则覆盖;默认是覆盖
// evict:如果为false表示table为创建状态,默认是非创建状态
final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
                   boolean evict) {
        Node<K,V>[] tab; 			//存储table的引用
    	Node<K,V> p; int n, i;		//n是桶的个数,i是桶的下标
        if ((tab = table) == null || (n = tab.length) == 0)		//如果是第一次插入,则一定为null或者桶的个数为0,对table进行初始化
            n = (tab = resize()).length;	//初始化完成后赋值n
        if ((p = tab[i = (n - 1) & hash]) == null)	//将目标桶下第一个元素赋值给p,若p == null,则代表目标桶还未存储过元素,直接初始化桶并插入即可
            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))))
                e = p;	//如果第一个桶的元素的key是重复的,那么就使用value覆盖原来的值
            else if (p instanceof TreeNode)	//否则往下插入,此情况下说明桶下存储结构为红黑树,树节点插入
                e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
            else {		//否则存储结构为链表,链表遍历判断插入
                for (int binCount = 0; ; ++binCount) { //计数器代表当前为桶下链表中的第几个元素
                    if ((e = p.next) == null) {		//若遇到null,直接插入节点
                        p.next = newNode(hash, key, value, null);
                        //判断此时链表长度是否达到转化红黑树的阈值
                        if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st	从-1开始所以这里进行-1	7
                            treeifyBin(tab, hash);	//若是则进行转化
                        break;
                    }
                     //若找到key相同的节点,则进行覆盖
                    if (e.hash == hash &&
                        ((k = e.key) == key || (key != null && key.equals(k))))
                        break;
                    p = e;
                }
            }
            //此情况代表,插入键值对是进行了覆盖操作,此时返回被覆盖的值
            if (e != null) { // existing mapping for key
                V oldValue = e.value;
                if (!onlyIfAbsent || oldValue == null)
                    e.value = value;
                afterNodeAccess(e);	 // 访问后回调
                return oldValue;
            }
        }
        ++modCount;
            if (++size > threshold)	//如果此时表中元素个数大于阈值threshold,则进行扩容,
            resize();
        afterNodeInsertion(evict);	//插入后回调
        return null;
    }

整个过程的流程如下图

1.先通过哈希值计算出key映射到哪个桶;
2.如果桶上没有碰撞冲突,则直接插入;
3.如果遇上了冲突,则需要冲突处理:
(1)如果该桶使用红黑树处理冲突,则调用红黑树的方法插入数据;
(2)否则采用传统的链式方法插入。如果链的长度达到临界值,则把链转变为红黑树;
4.如果桶中存在重复的key,则用新值替换老值并返回老值;
5.如果size大于阈值threshold,则进行扩容。

image

3.8 remove()方法

public V remove(Object key) {
    Node<K,V> e;
    return (e = removeNode(hash(key), key, null, false, true)) == null ? null : e.value;
}

//----------------------------------------------------------------------------
// matchValue 如果为true,表示只在值相等的时候进行删除,但是默认是false。
// movable 如果为false,表示删除的时候不移动其他节点,默认是true
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;
    	//找目标节点
        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;
            if (p.hash == hash && ((k = p.key) == key || (key != null && key.equals(k))))
                node = p;	//如果目标桶中第一个元素为要删除的目标元素,则赋值给node
            else if ((e = p.next) != null) {	//判断该桶下的其他元素
                if (p instanceof TreeNode)	//若元素为树节点,则调用方法去树中查询,查询结果赋值给node
                    node = ((TreeNode<K,V>)p).getTreeNode(hash, key);
                else {	//否则即为链表结构
                    do {
                        if (e.hash == hash &&((k = e.key) == key ||(key != null && key.equals(k)))) {
                            node = e;	//找到节点则赋值给node并break
                            break;
                        }
                        p = e;
                    } while ((e = e.next) != null);
                }
            }
            //接下来对于找到的节点从哈希表中删除
            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;	 //无目标元素则返回null
    }

流程如下

1.先找到元素的目标桶,即存储位置;
2。如果该桶下的存储结构是链表的话,则遍历找到并删除即可;
3。如果是红黑树结构,则遍历找到并删除,如果此时节点数少于红黑树转链表的阈值6时,调用untreeify方法将红黑树转化成链表

3.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;	//无目标元素则返回null
    }

3.10 treeifyBin()链表转为红黑树

这是在3.7 的put方法中的putval()的方法,如果桶里面的链表的长度大于8的时候就调用的方法。

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)	//传入的tab肯定是不是null,直接看后面的,当桶的个数不足64的时候,选择的是扩容。
        resize();
    //桶的数量大于64了,则进行转为红黑树
    else if ((e = tab[index = (n - 1) & hash]) != null) {	//使用e定位到链表的第一个节点
        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);
         //不难发现上述while虽然是一颗红黑树,但是是链状的
        if ((tab[index] = hd) != null)	//将红黑树头节点放进桶中
            hd.treeify(tab);	//将上述链状红黑树进行平衡处理
    }
}

//----------------------------将原来的节点转为数节点----------------
 TreeNode<K,V> replacementTreeNode(Node<K,V> p, Node<K,V> next) {
        return new TreeNode<>(p.hash, p.key, p.value, next);
    }

//-----------------------------平衡处理----------------------------
final void treeify(Node<K,V>[] tab) {
            TreeNode<K,V> root = null;
            for (TreeNode<K,V> x = this, next; x != null; x = next) {
                next = (TreeNode<K,V>)x.next;
                x.left = x.right = null;
                if (root == null) {
                    x.parent = null;
                    x.red = false;
                    root = x;
                }
                else {
                    K k = x.key;
                    int h = x.hash;
                    Class<?> kc = null;
                    for (TreeNode<K,V> p = root;;) {
                        int dir, ph;
                        K pk = p.key;
                        if ((ph = p.hash) > h)
                            dir = -1;
                        else if (ph < h)
                            dir = 1;
                        else if ((kc == null &&
                                  (kc = comparableClassFor(k)) == null) ||
                                 (dir = compareComparables(kc, k, pk)) == 0)
                            dir = tieBreakOrder(k, pk);

                        TreeNode<K,V> xp = p;
                        if ((p = (dir <= 0) ? p.left : p.right) == null) {
                            x.parent = xp;
                            if (dir <= 0)
                                xp.left = x;
                            else
                                xp.right = x;
                            root = balanceInsertion(root, x);
                            break;
                        }
                    }
                }
            }
            moveRootToFront(tab, root);
        }

总体的步骤就是:如下

1.根据哈希表中元素个数确定是扩容还是树形化;
2.如果是树形化遍历桶中的元素,创建相同个数的树形节点,复制内容,建立起联系;
3.然后让桶中的第一个元素指向新创建的树根节点,替换桶的链表内容为树形化内容;
4.最后处理红黑树,红黑树中比较大小的依据为hash值。

3.11 reseize()方法【扩容】

hashmap的扩容,每次扩容都会将表的大小扩容为原来的2倍

3.11.1扩容的时机

1.当容器中的元素数量大于阈值的时候就会触发扩容。(阈值就是表的大小乘负载因子)

2.当某一个桶中的元素数量大于8的时候,并且哈希表的大小小于64的时候,触发扩容。

final Node<K,V>[] resize() {
        Node<K,V>[] oldTab = table;		//存下原来的hash表
        int oldCap = (oldTab == null) ? 0 : oldTab.length;	//记录原hash表的大小
        int oldThr = threshold;	//记录原来的阈值
        int newCap, newThr = 0;	//声明扩容后的大小和阈值
        if (oldCap > 0) {	//如果原哈希表大小大于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
        }	//原哈希表大小为0,但是阈值大于0,此时便是之前提到的修正位置
        else if (oldThr > 0) 
            newCap = oldThr;	//赋值给新大小
        else {             	//否则使用默认大小16和默认加载因子0.75计算出的阈值12 
            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;		//赋值
        @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) {	//当前桶中有元素,并赋值给节点e
                    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;	//声明两条链表l和h
                        Node<K,V> hiHead = null, hiTail = null;
                        Node<K,V> next;
                        do {	//用循环将所有节点取出并放入l或h中
                            next = e.next;
                            if ((e.hash & oldCap) == 0) {	//重点方法,需比较一位二进制为即可判断它在新哈希表中的目标桶位置
                                if (loTail == null)	//为0代表位置不变
                                    loHead = e;
                                else
                                    loTail.next = e;
                                loTail = e;
                            }
                            else {	//为1则代表变为 原位置+旧容量
                                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;
    }

这里再简单描述一下关于桶中元素为红黑树的情况:

else if (e instanceof TreeNode) //如果是红黑树结构,则调用相关方法把树分开
	((TreeNode<K,V>)e).split(this, newTab, j, oldCap);

split方法是先将当前这棵树按照上述位运算结论,分解成类似上述l、h两条链表l、h两颗红黑树,然后分别对两颗红黑树进行节点数量判断,若小于UNTREEIFY_THRESHOLD(红黑树转链表的阈值),则将其转变成链表,然后分别存储新哈希表的目标桶中。

3.12其他注意事项

hashmap初始化,推荐使用指定集合初始值的大小的构造方法,目的是为了减少扩容,扩容操作是十分耗时的。如果暂时不知道扩容的大小是多大,就设置为默认的16就好ヾ(๑╹◡╹)ノ"。

如果知道有多大,那么就设置为下面计算出来的公式的大小

image

这里的加载因子为0.75f,举个例子如果你想存100,那么就可以设置大小为100/0.75 约等于133.3,在进行+1,就是134.3,取整后为135

posted @ 2022-03-20 23:26  程序员hg  阅读(23)  评论(0编辑  收藏  举报