java源码分析(五)---HashMap源码

吐槽

今天天气好冷啊,真的是冻死了,自己下午出去吃了一顿羊肉泡,美滋滋。回来啃下HashMap的源码。

前置条件

在看HashMap源码之前我们有两个前置条件:

  • Hash的概念,Hash函数的概念,Hash表的概念
  • Map接口的源码分析

当我们看完前置条件,然后再去看下HashMap的源码,解决以下问题。

  • 什么时候使用HashMap?简单的介绍下HashMap
  • HashMap的工作原理
  • HashMap里面的get()和Put的原理?equals()和hashCode()的都有什么作用?
  • hash的实现吗?为什么要这样实现?
  • 如果HashMap的大小超过了负载因子(load factor)定义的容量,怎么办?

哈希 哈希函数 哈希表

之前大二数据结构课上自己还学过这块,然后自己现在已经忘完了233,现在重新看下这块。

哈希

hash又称为散列
散列是什么啊233
当然就是不经过排序,然后把一类数据放在一起的东西,和排序是刚好相反的操作。

然后这块散列到底是什么意思啊,举个简单的例子

从前你要去一个班里面找个小孩子,你只知道他叫狗蛋,然后你去教室之后问班主任,班主任拿着班级学生的花名册挨个看,发现没有狗蛋这个学生的名字,告诉你他们班没有狗蛋这个人。但是你坚信狗蛋在这个班,你找个小屁孩问下他们有没有狗蛋这个人啊,他们就会告诉你狗蛋在哪里,因为狗蛋因为在班里傻乎乎的,人们就只叫他的外号狗蛋,他的本名叫王二哈233

分析下上面的故事,我们发现
老师是通过花名册进行查找的,类似顺序表查找,依赖的是名字的匹配,挨个挨个进行查找。
但是我们去问小孩子的时候,没有遍历没有比较,直接就根据王二哈的外号狗蛋直接找到他,效率高。

散列技术是记录的存储位置和它的关键字(外号)之间确定一个对应的关系f,使得每个关键字(外号)key对应一个存储位置。当我们进行查找的时候,找到key,根据f(key)就找到存储位置了。
这个里面f是散列函数,又称为哈希函数

再举个例子
下面有4 个数 {2,5,9,13},需要查找 13 是否存在。
第一种方式,建立一个数组,然后遍历进行查找,判断其是否存在。
第二种方式,存储时先使用哈希函数进行计算,这里我随便用个函数H[key] = key % 3;
然后在存的时候
H[2] = 2 % 3 = 2;
H[5] = 5 % 3 = 2;
H[9] = 9 % 3 = 0;
H[13] = 13 % 3 = 1;
2存在2的位置,5存在2的位置(冲突了),9存在0号位置,13存在1号位置
当我们要查询13在不在的时候,去他的1号位置看下是否存在

哈希 其实是随机存储的一种优化,先进行分类,然后查找时按照这个对象的分类去找。
哈希通过一次计算大幅度缩小查找范围,自然比从全部数据里查找速度要快

哈希函数

如何给别人起外号呢,还不能重复,要突出特色哈哈哈
哈希函数就是干这个的

哈希的过程中需要使用哈希函数进行计算。
哈希函数是一种映射关系,根据数据的关键词 key ,通过一定的函数关系,计算出该元素存储位置的函数。
表示为:
address = H [key]

常见的哈希函数的构造方法

  • 直接定制法 关键字或关键字的某个线性函数值为散列地址 类似H(key) = key 或 H(key) = a*key + b,其中a和b为常数。
  • 除留余数法 取关键字被某个不大于散列表长度 m 的数 p 求余,得到的作为散列地址。 H(key) = key % p
  • 数字分析法 键字的位数大于地址的位数,对关键字的各位分布进行分析,选出分布均匀的任意几位作为散列地址。
  • 平方取中法 先计算出关键字值的平方,然后取平方值中间几位作为散列地址。
  • 随机数法 选择一个随机函数,把关键字的随机函数值作为它的哈希值

但是我们不论怎么样用什么规则给别人取外号,都有一定几率会起到重复的名字233,那么我们又如何去解决这块呢?
解决hash冲突的四种办法

哈希表//散列表

哈希表是实现关联数组(associative array)的一种数据结构,广泛应用于实现数据的快速查找。

链地址法的原理时如果遇到冲突,他就会在原地址新建一个空间,然后以链表结点的形式插入到该空间。我感觉业界上用的最多的就是链地址法。下面从百度上截取来一张图片,可以很清晰明了反应下面的结构。比如说我有一堆数据{1,12,26,337,353…},而我的哈希算法是H(key)=key mod 16,第一个数据1的哈希值f(1)=1,插入到1结点的后面,第二个数据12的哈希值f(12)=12,插入到12结点,第三个数据26的哈希值f(26)=10,插入到10结点后面,第4个数据337,计算得到哈希值是1,遇到冲突,但是依然只需要找到该1结点的最后链结点插入即可,同理353
《java源码分析(五)---HashMap源码》

Map接口的源码

Map是集合的另一大帮派的老大
看下他的继承关系
《java源码分析(五)---HashMap源码》

它有四大得力干将:

  • Hashtable 古老,线程安全
  • HashMap 速度快,没顺序
  • TreeMap 有序的,效率比HashMap低
  • LinkedHashMap 有序的,结合上面两个的特点

然后我们去看下这块Map的源码:

public interface Map<K, V> {
    int size();

    boolean isEmpty();

    boolean containsKey(Object var1);

    boolean containsValue(Object var1);

    V get(Object var1);

    V put(K var1, V var2);

    V remove(Object var1);

    void putAll(Map<? extends K, ? extends V> var1);

    void clear();

    Set<K> keySet();

    Collection<V> values();

    Set<Map.Entry<K, V>> entrySet();
    ------
    public interface Entry<K, V> {
    K getKey();

    V getValue();

    V setValue(V var1);

    boolean equals(Object var1);

    int hashCode();

    static <K extends Comparable<? super K>, V> Comparator<Map.Entry<K, V>> comparingByKey() {
        return (Comparator)((Serializable)((var0x, var1x) -> {
            return ((Comparable)var0x.getKey()).compareTo(var1x.getKey());
        }));
    }

    static <K, V extends Comparable<? super V>> Comparator<Map.Entry<K, V>> comparingByValue() {
        return (Comparator)((Serializable)((var0x, var1x) -> {
            return ((Comparable)var0x.getValue()).compareTo(var1x.getValue());
        }));
    }

    static <K, V> Comparator<Map.Entry<K, V>> comparingByKey(Comparator<? super K> var0) {
        Objects.requireNonNull(var0);
        return (Comparator)((Serializable)((var1x, var2x) -> {
            return var0.compare(var1x.getKey(), var2x.getKey());
        }));
    }

    static <K, V> Comparator<Map.Entry<K, V>> comparingByValue(Comparator<? super V> var0) {
        Objects.requireNonNull(var0);
        return (Comparator)((Serializable)((var1x, var2x) -> {
            return var0.compare(var1x.getValue(), var2x.getValue());
        }));
    }
    

里面看了下就三个重要的东西

KeySet

是map里面所有的key的集合,以set的形式保存,不允许重复

Values

是一个 Map 中值 (value) 的集合,以 Collection 的形式保存,因此可以重复

Entry

Entry 是 Map 接口中的静态内部接口,表示一个键值对的映射
通过 Map.entrySet() 方法获得的是一组 Entry 的集合,保存在 Set 中,所以 Map 中的 Entry 也不能重复。

public Set<Map.Entry<K,V>> entrySet();

Map的遍历方式

看下别人的博客
Map的遍历方式

HashMap源码分析

《java源码分析(五)---HashMap源码》
上面介绍的时候也讲过为了防止哈希碰撞,HashMap采用拉链法,就是上面这种样子保存数据的
所以我们看到他存储的时候就是底层实现用的是数组+链表的方式

成员变量

public class HashMap<K, V> extends AbstractMap<K, V> implements Map<K, V>, Cloneable, Serializable {
    //又是序列号
    private static final long serialVersionUID = 362498820763181265L;
    //默认的初始容量
    static final int DEFAULT_INITIAL_CAPACITY = 16;
    //最大容量
    static final int MAXIMUM_CAPACITY = 1073741824;
    //加载因子
    static final float DEFAULT_LOAD_FACTOR = 0.75F;
    //树形阈值 当使用 树 而不是列表来作为桶时使用。必须必 2 大
    static final int TREEIFY_THRESHOLD = 8;
    //非树形阈值:也是 1.8 新增的,扩容时分裂一个树形桶的阈值
    static final int UNTREEIFY_THRESHOLD = 6;
    //数的最小容量 避免冲突
    static final int MIN_TREEIFY_CAPACITY = 64;
    //链表数组
    transient HashMap.Node<K, V>[] table;
    //缓存的键值对集合
    transient Set<Entry<K, V>> entrySet;
    //键值对的数量
    transient int size;
    //修改次数
    transient int modCount;
    //这块是阈值 用于判断是否需要调整HashMap的容量(threshold = 容量*加载因子)
    int threshold;
    //哈希表的加载因子
    final float loadFactor;

然后我们看下这个加载因子到底是什么鬼啊

static final float DEFAULT_LOAD_FACTOR = 0.75f;  

加载因子就是表示Hash表中元素的填满程度
加载因子越大,填充的元素越多,空间利用率就越高,但是冲突就越多
反之,加载因子越小,填满的元素越少,冲突的机会减小,但空间浪费多了。
所以,这个很矛盾emmmmm
《java源码分析(五)---HashMap源码》
HashMap有一个初始容量大小,默认是16

static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; // aka 16 

为了减少冲突的概率,当hashMap的数组长度到了一个临界值就会触发扩容,把所有元素rehash再放到扩容后的容器中,这是一个非常耗时的操作。
而这个临界值由【加载因子】和当前容器的容量大小来确定:DEFAULT_INITIAL_CAPACITY*DEFAULT_LOAD_FACTOR ,即默认情况下是16×0.75=12时,就会触发扩容操作。
所以,我们还是不知道为啥是0.75 emmmmmm
然后网上查下资料发现这块还和统计学有关233

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 varian/ce, the expected
occurrences of list size k are (exp(-0.5) * pow(0.5, k) /
factorial(k)). The first values are:
简单翻译一下就是在理想情况下,使用随机哈希码,节点出现的频率在hash桶中遵循泊松分布,同时给出了桶中元素个数和概率的对照表。

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
从上面的表中可以看到当桶中元素到达8个的时候,概率已经变得非常小,也就是说用0.75作为加载因子,每个碰撞位置的链表长度超过8个是几乎不可能的。
《java源码分析(五)---HashMap源码》
简单的说,Capacity就是bucket的大小,Load factor就是bucket填满程度的最大比例。如果对迭代性能要求很高的话,不要把capacity设置过大,也不要把load factor设置过小。当bucket中的entries的数目大于capacity*load factor时就需要调整bucket的大小为当前的2倍。

关键字是泊淞分布
科普地址d

构造方法

//创建一个空的哈希表,然后指定容量和加载因子

public HashMap(int var1, float var2) {
    //输入判断
    if (var1 < 0) {
        throw new IllegalArgumentException("Illegal initial capacity: " + var1);
    } else {
        if (var1 > 1073741824) {
            var1 = 1073741824;
        }
        //输入判断
        if (var2 > 0.0F && !Float.isNaN(var2)) {
            this.loadFactor = var2;
            this.threshold = tableSizeFor(var1);
        } else {
            throw new IllegalArgumentException("Illegal load factor: " + var2);
        }
    }
}
//指定的容量的哈希表,加载因子的0.75
public HashMap(int var1) {
    this(var1, 0.75F);
}
//空的哈希表 初始容量是16 加载因子是0.75
public HashMap() {
    this.loadFactor = 0.75F;
}
//创建一个内容为参数var1的哈希表
public HashMap(Map<? extends K, ? extends V> var1) {
    this.loadFactor = 0.75F;
    this.putMapEntries(var1, false);
}

在自己设置容量和加载因子的时候,它调用这个 tableSizeFor(var1);

static final int tableSizeFor(int var0) {
    int var1 = var0 - 1;
    var1 |= var1 >>> 1;
    var1 |= var1 >>> 2;
    var1 |= var1 >>> 4;
    var1 |= var1 >>> 8;
    var1 |= var1 >>> 16;
    return var1 < 0 ? 1 : (var1 >= 1073741824 ? 1073741824 : var1 + 1);
}

根据指定的容量设置阈值,这个方法经过若干次无符号右移、求异运算,得出最接近指定参数 cap 的 2 的 N 次方容量
反正因为使用了位运算,所以这个方法可能不能明确的知道结果,但是只要知道不管输入什么值,它的最后结果都会是0,1,2,4,8,16,32,68… 这些数字中的一个就对了(其实是有规律的),对于以下输入值有:
tableSizeFor(16) = 16
tableSizeFor(32) = 32
tableSizeFor(48) = 64
tableSizeFor(64) = 64
tableSizeFor(80) = 128
tableSizeFor(96) = 128
tableSizeFor(112) = 128
tableSizeFor(128) = 128
tableSizeFor(144) = 256
也即是说,对于容量的初始值16来说,其初始阈值便是16,与JDK 1.7中初始阈值相同,而其resize函数中,threshold的计算源码如下

final HashMap.Node<K, V>[] resize() {
    HashMap.Node[] var1 = this.table;
    int var2 = var1 == null ? 0 : var1.length;
    int var3 = this.threshold;
    int var5 = 0;
    int var4;
    //如果有容量,表示map已经有元素了
    if (var2 > 0) {
        if (var2 >= 1073741824) {
            this.threshold = 2147483647;
            return var1;
        }
     //容量翻倍
        if ((var4 = var2 << 1) < 1073741824 && var2 >= 16) {
            var5 = var3 << 1;
        }
    } else if (var3 > 0) {
        var4 = var3;
    } else {
        var4 = 16;
        var5 = 12;
    }

    if (var5 == 0) {
        float var6 = (float)var4 * this.loadFactor;
        var5 = var4 < 1073741824 && var6 < 1.07374182E9F ? (int)var6 : 2147483647;
    }

    this.threshold = var5;
    HashMap.Node[] var14 = (HashMap.Node[])(new HashMap.Node[var4]);
    this.table = var14;
    //最开始没往里面加东西的时候
    //上面的操作就是如果你初始化的时候带了参数的时候HashMap(int initialCapacity, float loadFactor))
   //newCap就是你的initialCapacithreshold就是 (int)(initialCapacity*loadFactor)
   //如果没带参数的话 按照默认的算initialCapacity = 16,threshold = 12

   // 如果已经有元素了,那么直接扩容2倍,如果oldCap >= DEFAULT_INITIAL_CAPACITY了,那么threshold也扩大两倍

    if (var1 != null) {
        for(int var7 = 0; var7 < var2; ++var7) {
            HashMap.Node var8;
            if ((var8 = var1[var7]) != null) {
                var1[var7] = null;
                if (var8.next == null) {
                    var14[var8.hash & var4 - 1] = var8;
                } else if (var8 instanceof HashMap.TreeNode) {
                //原来的结点转换成红黑树
                    ((HashMap.TreeNode)var8).split(this, var14, var7, var2);
                    
                } else {
                    HashMap.Node var9 = null;
                    HashMap.Node var10 = null;
                    HashMap.Node var11 = null;
                    HashMap.Node var12 = null;

                    HashMap.Node var13;
                    do {
                        var13 = var8.next;
                        if ((var8.hash & var2) == 0) {
                            if (var10 == null) {
                                var9 = var8;
                            } else {
                                var10.next = var8;
                            }

                            var10 = var8;
                        } else {
                            if (var12 == null) {
                                var11 = var8;
                            } else {
                                var12.next = var8;
                            }

                            var12 = var8;
                        }

                        var8 = var13;
                    } while(var13 != null);

                    if (var10 != null) {
                        var10.next = null;
                        var14[var7] = var9;
                    }

                    if (var12 != null) {
                        var12.next = null;
                        var14[var7 + var2] = var11;
                    }
                }
            }
        }
    }

    return var14;
}

emmmm看的这块的简直是脑壳痛,,,jdk的版本不一样计算阈值也不一样
JDK 1.6 当数量大于容量 * 负载因子即会扩充容量。
JDK 1.7 初次扩充为:当数量大于容量时扩充;第二次及以后为:当数量大于容量 * 负载因子时扩充。
JDK 1.8 初次扩充为:与负载因子无关;第二次及以后为:与负载因子有关。其详细计算过程需要具体详解。//涉及到红黑树

这个函数的解释看下大佬的解释,,红黑数搞不定啊啊啊
resize的解释

再看下整个集合添加到哈希表中

final void putMapEntries(Map<? extends K, ? extends V> var1, boolean var2) {
    int var3 = var1.size();
    if (var3 > 0) {
        //如果数组为空的情况,初始化参数
        if (this.table == null) {
            float var4 = (float)var3 / this.loadFactor + 1.0F;
            int var5 = var4 < 1.07374182E9F ? (int)var4 : 1073741824;
            if (var5 > this.threshold) {
                this.threshold = tableSizeFor(var5);
            }
            //数组不为空,超过阈值就扩容
        } else if (var3 > this.threshold) {
            this.resize();
        }

        Iterator var8 = var1.entrySet().iterator();

        while(var8.hasNext()) {
            Entry var9 = (Entry)var8.next();
            Object var6 = var9.getKey();
            Object var7 = var9.getValue();
            //通过hash()计算位置,然后复制
            this.putVal(hash(var6), var6, var7, false, var2);
        }
    }

}

HashMap的结点

static class Node<K, V> implements Entry<K, V> {
    //哈希值,就是位置
    final int hash;
    //键
    final K key;
    //值
    V value;
    //下个结点指针
    HashMap.Node<K, V> next;

    Node(int var1, K var2, V var3, HashMap.Node<K, V> var4) {
        this.hash = var1;
        this.key = var2;
        this.value = var3;
        this.next = var4;
    }

    public final K getKey() {
        return this.key;
    }

    public final V getValue() {
        return this.value;
    }

    public final String toString() {
        return this.key + "=" + this.value;
    }

    public final int hashCode() {
        return Objects.hashCode(this.key) ^ Objects.hashCode(this.value);
    }

    public final V setValue(V var1) {
        Object var2 = this.value;
        this.value = var1;
        return var2;
    }

    public final boolean equals(Object var1) {
        if (var1 == this) {
            return true;
        } else {
            if (var1 instanceof Entry) {
                Entry var2 = (Entry)var1;
                if (Objects.equals(this.key, var2.getKey()) && Objects.equals(this.value, var2.getValue())) {
                    return true;
                }
            }

            return false;
        }
    }
}

put函数

public V put(K var1, V var2) {
    //对key做hashCode()做hash
    return this.putVal(hash(var1), var1, var2, false, true);
}

final V putVal(int var1, K var2, V var3, boolean var4, boolean var5) {
    HashMap.Node[] var6 = this.table;
    int var8;
    //如果当前哈希表为空,新建一个,var8指桶的最后一个位置
    if (this.table == null || (var8 = var6.length) == 0) {
        var8 = (var6 = this.resize()).length;
    }

    Object var7;
    int var9;
    //如果要插入的位置没有元素,新建一个结点
    if ((var7 = var6[var9 = var8 - 1 & var1]) == null) {
        var6[var9] = this.newNode(var1, var2, var3, (HashMap.Node)null);
    } else {
       //如果要插入的桶已经有元素,替换
        Object var10;//被替换的元素
        label79: {
            Object var11;
            if (((HashMap.Node)var7).hash == var1) {
                var11 = ((HashMap.Node)var7).key;
                if (((HashMap.Node)var7).key == var2 || var2 != null && var2.equals(var11)) {
                    var10 = var7;
                    break label79;
                }
            }

            if (var7 instanceof HashMap.TreeNode) {
                var10 = ((HashMap.TreeNode)var7).putTreeVal(this, var6, var1, var2, var3);
            } else {
                int var12 = 0;

                while(true) {
                    var10 = ((HashMap.Node)var7).next;
                    if (((HashMap.Node)var7).next == null) {
                        ((HashMap.Node)var7).next = this.newNode(var1, var2, var3, (HashMap.Node)null);
                        //如果这个桶的链表的个数大于等于8,数化
                        if (var12 >= 7) {
                            this.treeifyBin(var6, var1);
                        }
                        break;
                    }
                      //如果找到要替换的结点就停止
                    if (((HashMap.Node)var10).hash == var1) {
                        var11 = ((HashMap.Node)var10).key;
                        if (((HashMap.Node)var10).key == var2 || var2 != null && var2.equals(var11)) {
                            break;
                        }
                    }

                    var7 = var10;
                    ++var12;
                }
            }
        }

        if (var10 != null) {
            Object var13 = ((HashMap.Node)var10).value;
            if (!var4 || var13 == null) {
                ((HashMap.Node)var10).value = var3;
            }

            this.afterNodeAccess((HashMap.Node)var10);
            return var13;
        }
    }
    //超出阈值,扩容
    ++this.modCount;
    if (++this.size > this.threshold) {
        this.resize();
    }

    this.afterNodeInsertion(var5);
    return null;
}

过程真的是很神奇唉

  • 1先对key的hashCode()做hash,然后再计算下标的值
  • 2 如果没有碰撞就弄到桶里面
  • 3 碰撞了就以链表的形式放在那个桶的链表里面
  • 4 如果这个桶的链表里面接了7个结点后,就把链表弄成红黑树
  • 5 如果结点已经有了,就替换旧的值,保证key的唯一性
  • 6 如果满了就扩容

get函数

public V get(Object key) {
    Node<K,V> e;
    //先计算hash值
    return (e = getNode(hash(key), key)) == null ? null : e.value;
}
final Node<K,V> getNode(int hash, Object key) {
//tab是hash表,n为hash的长度
    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 && 
            ((k = first.key) == key || (key != null && key.equals(k))))
            return first;
        // 未命中,挨个找
        if ((e = first.next) != null) {
            // 在树中get
            if (first instanceof TreeNode)
                return ((TreeNode<K,V>)first).getTreeNode(hash, key);
            // 在链表中get
            do {
                if (e.hash == hash &&
                    ((k = e.key) == key || (key != null && key.equals(k))))
                    return e;
            } while ((e = e.next) != null);
        }
    }
    return null;
}

这个过程就是拿着key找值的

  • 1还是先计算hash值
  • 2先在第一个桶里面找下,看下能否直接命中
  • 3如果用冲突的话,挨个遍历
  • 4计算出桶的位置
  • 5在桶的链表或者树里面挨个找

hash函数

这个函数更是神奇,很短,但是emmmmm头大
1 对key的值首先进行hashCode()运算//通用方法,根据对象的内存地址,返回一个特定的哈希码
2 二次处理哈希码 //进行无符号右移16位,求得键对应的真正的hash值
3 计算存储的数组的位置 二次处理的哈希码和数组长度减一 进行&运算

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

HashMap 中通过将传入键的 hashCode 进行无符号右移 16 位,然后进行按位异或,得到这个键的哈希值。

由于哈希表的容量都是 2 的 N 次方,在当前,元素的 hashCode() 在很多时候下低位是相同的,这将导致冲突(碰撞),因此 1.8 以后做了个移位操作:将元素的 hashCode() 和自己右移 16 位后的结果求异或。

由于 int 只有 32 位,无符号右移 16 位相当于把高位的一半移到低位:
《java源码分析(五)---HashMap源码》

直接看网上的图吧
《java源码分析(五)---HashMap源码》

这块真的是神乎奇迹令人窒息的操作,真的是可怕啊

  • 先是将key的值右移动16位,这个数的前16位都是0了,后16位是之前的前16位
  • 然后和之前的数字进行异或,相同为0,不同为1
  • 获取到值后再和桶的长度减一进行异或,获取下标

为撒要和桶的长度减一异或呢?
由于哈希表的容量都是 2 的 N 次方,在当前,元素的 hashCode() 在很多时候下低位是相同的,这将导致冲突(碰撞)//这个是别人的解释

我们还是按实际出发,如果哈希表的长度是8的话
如果和我们之前计算的值进行与运算的话,可能很多时候要碰撞
所以,为了不大于桶的长度,要减一
0000 0000 0000 0111
然后再进行计算,实际作用的只有低4位,很容易碰撞
所以,才将key进行高16位和低16位的异或操作,仅仅异或一下,既减少了系统的开销,也不会造成的因为高位没有参与下标的计算(table长度比较小时),从而引起的碰撞。

如果还是产生了频繁的碰撞,会发生什么问题呢?
作者注释说,他们使用树来处理频繁的碰撞233真的强

我们看get函数的时候
首先,先根据hashCode()做hash,然后确定bucket的index;
然后,再找到桶之后再接着匹配下key,然后找值

在Java 8之前的实现中是用链表解决冲突的,在产生碰撞的情况下,进行get时,两步的时间复杂度是O(1)+O(n)。因此,当碰撞很厉害的时候n很大,O(n)的速度显然是影响速度的。
因此在Java 8中,利用红黑树替换链表,这样复杂度就变成了O(1)+O(logn)了,这样在n很大的时候,能够比较理想的解决这个问题

话说这些骚操作到底是干了什么,为啥要这样干emmmmm
提高 存储key-value的数组下标位置 的随机性 & 分布均匀性,尽量避免出现hash值冲突emmmmmm
首先经过hashCode()处理的哈希码为什么不能直接当桶的下标
很简单,,,因为哈希码和数组的大小范围不匹配,就是直接计算出来的哈希码可能不在数组范围里面

为什么在计算数组下标前,需对哈希码进行二次处理:扰动处理?
加大哈希码低位的随机性,使得分布更均匀,从而提高对应数组存储下标位置的随机性 & 均匀性,最终减少Hash冲突

为什么采用 哈希码 与运算(&) (数组长度-1) 计算数组下标?
根据HashMap的容量大小(数组长度),按需取 哈希码一定数量的低位 作为存储的数组下标位置,从而 解决 “哈希码与数组大小范围不匹配” 的问题

扩容函数

在在HashMap中有两个很重要的参数,容量(Capacity)和负载因子(Load factor)
Capacity就是bucket的大小,Load factor就是bucket填满程度的最大比例。如果对迭代性能要求很高的话,不要把capacity设置过大,也不要把load factor设置过小。当bucket中的entries的数目大于capacity*load factor时就需要调整bucket的大小为当前的2倍。
我们来看下这块的扩容函数的过程

当put时,如果发现目前的bucket占用程度已经超过了Load Factor所希望的比例,那么就会发生resize。在resize的过程,简单的说就是把bucket扩充为2倍,之后重新计算index,把节点再放到新的bucket中

当超过限制的时候会resize,然而又因为我们使用的是2次幂的扩展(指长度扩为原来2倍),所以,元素的位置要么是在原位置,要么是在原位置再移动2次幂的位置。
怎么理解呢?例如我们从16扩展为32时,具体的变化如下所示:
《java源码分析(五)---HashMap源码》

因此元素在重新计算hash之后,因为n变为2倍,那么n-1的mask范围在高位多1bit(红色),因此新的index就会发生这样的变化:

《java源码分析(五)---HashMap源码》
因此,我们在扩充HashMap的时候,不需要重新计算hash,只需要看看原来的hash值新增的那个bit是1还是0就好了,是0的话索引没变,是1的话索引变成“原索引+oldCap”。可以看看下图为16扩充为32的resize示意

《java源码分析(五)---HashMap源码》
这个设计确实非常的巧妙,既省去了重新计算hash值的时间,而且同时,由于新增的1bit是0还是1可以认为是随机的,因此resize的过程,均匀的把之前的冲突的节点分散到新的bucket了。

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) {
            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
        newCap = oldThr;
    else {               // zero initial threshold signifies using defaults
        newCap = DEFAULT_INITIAL_CAPACITY;
        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);
    }
    threshold = newThr;
    @SuppressWarnings({"rawtypes","unchecked"})
        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) {
                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;
                        // 原索引
                        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;
}

总结问题

问: 什么时候会使用HashMap?他有什么特点?
答:是基于Map接口的实现,存储键值对时,它可以接收null的键值,是非同步的,HashMap存储着Entry(hash, key, value, next)对象,把这个当作一个结点,进行存储。

问:HashMap线程安全吗?为什么?
答:线程不安全,因为HashMap没有使用sychronized同步关键字,在添加数据put()时,无法做到线程同步,当多个线程在插入数据时,如果发生了哈希碰撞,可能会造成数据的丢失,然后在在扩容resize()时,也无法做到线程同步,当多个线程同时开启扩容,会各自生成新的数组进行拷贝扩容,最终结果只有一个新数组被赋值给table变量,其他的线程均会丢失。

问:如何保证HashMap线程安全
答:三种方式,Hashtable,ConcurrentHashMap,Synchronized Map
链接详解

问:HashMap的工作原理吗?
通过hash的方法进行存储结构构建hash表,通过put和get存储和获取对象。存储对象时,我们将K/V传给put方法时,它调用hashCode计算hash从而得到bucket位置,进一步存储,HashMap会根据当前bucket的占用情况自动调整容量(超过Load Facotr则resize为原来的2倍)。获取对象时,我们将K传给get,它调用hashCode计算hash从而得到bucket位置,并进一步调用equals()方法确定键值对。如果发生碰撞的时候,Hashmap通过链表将产生碰撞冲突的元素组织起来,在Java 8中,如果一个bucket中碰撞冲突的元素超过某个限制(默认是8),则使用红黑树来替换链表,从而提高速度。

问:你知道get和put的原理吗?equals()和hashCode()的都有什么作用?
答:对结点的key进行hash运算操作,计算下标,也就是要放到那个桶里面,如果碰撞的话,就在这个桶的链表或者树里面进行查询结点。

问:hash的实现方式
答:在jdk1.8后,是通过hashCode()的高16位异或低16位实现的:(h = k.hashCode()) ^ (h >>> 16),主要是从速度、功效、质量来考虑的,这么做可以在bucket的n比较小的时候,也能保证考虑到高低bit都参与到hash的计算中,同时不会有太大的开销。

问:扩容时候机制
答:这个分情况讨论这块,因为jdk的版本问题分情况讨论
JDK 1.6 当数量大于容量 * 负载因子即会扩充容量。
JDK 1.7 初次扩充为:当数量大于容量时扩充;第二次及以后为:当数量大于容量 * 负载因子时扩充
我们看的是jkd1.8这块,当发现现在的桶的占有量已经超过了容量 * 负载因子时候,进行扩容,直接扩容桶的数量为之前的2倍,并且重新调用hash方法,重新计算下标,然后再把之前的结点放到数组里面。

问:HashMap里面的红黑树
答:这块,,,自己还在看emmmmm目前回答不上来,当一个桶链表结点数量大于8的时候,就把链表结构转换成红黑树结构,这样的操作查找效率更高,这块就是红黑树和链表的区别emmmmm

    原文作者:sakurakider
    原文地址: https://blog.csdn.net/sakurakider/article/details/83794770
    本文转自网络文章,转载此文章仅为分享知识,如有侵权,请联系博主进行删除。
点赞