HashMap 实现原理

1.hash

哈希算法也叫散列,就是把任意长度的key值通过散列算法变成固定长度的key值的地址,我们通过这个地址进行访问数据结构。
它通过把关键码值映射到表中的一个位置来访问记录,以加快查找的速度。

映射的函数叫做散列函数,存放记录的数组叫做散列表。

    public static void main(String[] args) {
        //Map<String,String> map = new HashMap();
        Map<Integer, Integer> map = new HashMap<>(12);
        map.put(1,1);
        map.put(2,2);
        map.put(3,3);
        map.put(4,4);
        map.put(5,5);
        map.put(6,6);
        map.put(14,14);

        map.keySet().forEach(DemoArraysList::put);
    }


    public static void put(Integer key){
        // HashMap默认数组长度为16
        System.out.printf("key:%s,hash值:%s,存储位置:%s\r\n",key,key.hashCode(),Math.abs(key.hashCode() % 6));
    }

输出:

key:1,hash值:1,存储位置:1
key:2,hash值:2,存储位置:2
key:3,hash值:3,存储位置:3
key:4,hash值:4,存储位置:4
key:5,hash值:5,存储位置:5
key:6,hash值:6,存储位置:0
key:14,hash值:14,存储位置:2



1和14属于hash冲突,用链表存放

2.HashMap 技术实质

1.存储内容:key 、value
2.存储结构:数组、链表、红黑数(JDK8)
3.存储位置:数组下标
4.存储大小:数组长度

3.构造方法

3.1. 空参构造方法,0.75负载因子的赋值

    /**
     * Constructs an empty <tt>HashMap</tt> with the default initial capacity
     * (16) and the default load factor (0.75).
     */
    public HashMap() {
  
        this.loadFactor = DEFAULT_LOAD_FACTOR; // all other fields defaulted
    }

3.2.初始化map容量

       /**
     * Constructs an empty <tt>HashMap</tt> with the specified initial
     * capacity and the default load factor (0.75).
     *
     * @param  initialCapacity the initial capacity.
     * @throws IllegalArgumentException if the initial capacity is negative.
     */
    public HashMap(int initialCapacity) {
        this(initialCapacity, DEFAULT_LOAD_FACTOR);
    }
    /**
     * Constructs an empty <tt>HashMap</tt> with the specified initial
     * capacity and load factor.
     *
     * @param  initialCapacity the initial capacity
     * @param  loadFactor      the load factor
     * @throws IllegalArgumentException if the initial capacity is negative
     *         or the load factor is nonpositive
     */
    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);
    }

3.3.求2的n次方 >= initialCapacity

    /**
     * Returns a power of two size for the given target capacity.
     */
    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;
    }
    /**
     * Constructs a new <tt>HashMap</tt> with the same mappings as the
     * specified <tt>Map</tt>.  The <tt>HashMap</tt> is created with
     * default load factor (0.75) and an initial capacity sufficient to
     * hold the mappings in the specified <tt>Map</tt>.
     *
     * @param   m the map whose mappings are to be placed in this map
     * @throws  NullPointerException if the specified map is null
     */
    public HashMap(Map<? extends K, ? extends V> m) {
        this.loadFactor = DEFAULT_LOAD_FACTOR;
        putMapEntries(m, false);
    }
      /**
     * Implements Map.putAll and Map constructor
     *
     * @param m the map
     * @param evict false when initially constructing this map, else
     * true (relayed to method afterNodeInsertion).
     */
    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)
                //扩容
                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);
            }
        }
    }
 static final int hash(Object key) {
    /**
    获取key的hash值
    对象的hashcode值 ^(异或) 对象的hashcode值的高位(前16位)
    目的:尽可能降低hash冲突,提高hashcode的随机性
    */
        int h;
        return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
 }

获取key的hash值
对象的hashcode值 ^(异或) 对象的hashcode值的高位(前16位)
目的:尽可能降低hash冲突,提高hashcode的随机性

3.4 HashMap的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;
        //判断table数组是否为空或者长度为0
        if ((tab = table) == null || (n = tab.length) == 0)
            //为空 则初始化table
            n = (tab = resize()).length;
        //16 -1 = 15 & hash值 = i, 计算当前传入hash位置为i
        // p = tab[i],p 为当前hash在table位置的值 p是否为空,表示当前hash值在table中没有元素(没有hash冲突)
        //(n - 1) & hash 表示当前hash值在table中的位置
        if ((p = tab[i = (n - 1) & hash]) == null)
            //p 为空则表示当前位置没有值,newNode赋值赋值給tab[i]
            tab[i] = newNode(hash, key, value, null);
        else {//tab[i] 有值的情况, p为当前tab[i]的值,有hash冲突的情况
            Node<K, V> e;//新建节点
            K k;//k 为当前tab[i]的哇key值
            //判断当前hash 和 当前位置的节点p 的key值与传入的key值是否相等,相等则e = p等于当前hash位置的值
            if (p.hash == hash && ((k = p.key) == key || (key != null && key.equals(k))))
                e = p;
            //如果是树结构,红黑树如插入值, JDK1.8 = 红黑树优化方案
            else if (p instanceof HashMap.TreeNode)
                e = ((HashMap.TreeNode<K, V>) p).putTreeVal(this, tab, hash, key, value);
            else {
                //链表结构 插入元素
                for (int binCount = 0;; ++binCount) {
                    //判断p的下一个匀速是否为Null
                    if ((e = p.next) == null) {
                        //如果为空则插入到P对应链表的下一个元素
                        p.next = newNode(hash, key, value, null);
                        //判断阈值,查找的次数>=8次,则换换为红黑树(链表默认长度是8,大于8则需要转换为红黑树)
                        if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
                            //转换为 红黑树
                            treeifyBin(tab, hash);
                        break;
                    }
                    //掺入的hash和key与p链表的下一个元素相等 ,当前链表包含要插入的key值 ,循环结束
                    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;
                if (!onlyIfAbsent || oldValue == null)
                    e.value = value;
                afterNodeAccess(e);
                return oldValue;
            }
        }
        //修改次数
        ++modCount;
        //判断当前数组是否大于阈值(默认16)
        if (++size > threshold)
            //扩容
            resize();
        afterNodeInsertion(evict);
        return null;
    }

3.5 Node转换为红黑树

/**
     * Replaces all linked nodes in bin at index for given hash unless
     * table is too small, in which case resizes instead.
     */
    final void treeifyBin(HashMap.Node<K,V>[] tab, int hash) {
        int n, index; HashMap.Node<K,V> e;
        //判断如果tab为空 或者 tab的长度小于 MIN_TREEIFY_CAPACITY(最小树容量64),则进行扩容
        if (tab == null || (n = tab.length) < MIN_TREEIFY_CAPACITY)
            resize();
        //满足树的容量,则转换为树
        //如果当前hash对应的位置 有元素
        else if ((e = tab[index = (n - 1) & hash]) != null) {
            //重建树
            TreeNode<K,V> hd = null, tl = null;
            do {
                //新建一个树节点p
                HashMap.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);

            //hd为重建树的根节点
            if ((tab[index] = hd) != null)
                hd.treeify(tab);
        }
    }

4.HashMap中hash(Object key)的原理,为什么这样做?

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

4.1 h >>> 16 是什么,有什么用?

h是hashcode。h >>> 16是用来取出h的高16,(>>>是无符号右移) 如下展示:

0000 0100 1011 0011 1101 1111 1110 0001 >>> 16
0000 0000 0000 0000 0000 0100 1011 0011

4.2 为什么 h = key.hashCode()) 与 (h >>> 16) 异或

还要看一个方法indexFor,在jdk1.7中有indexFor(int h, int length)方法。jdk1.8里没有,但原理没变。下面看下1.7源码

1.8中用tab[(n - 1) & hash]代替但原理一样。

static int indexFor(int h, int length) {
    return h & (length-1);
}
这个方法返回值就是数组下标。

但由于绝大多数情况下length一般都小于2^16即小于65536。所以return h & (length-1);结果始终是h的低16位与(length-1)进行&运算

例如1:为了方便验证,假设length为8。HashMap的默认初始容量为16

length = 8; (length-1) = 7;转换二进制为111;

假设一个key的 hashcode = 78897121 转换二进制:0000 0100 1011 0011 1101 1111 1110 0001,与(length-1)& 运算如下

0000 0100 1011 0011 1101 1111 1110 0001
&运算
0000 0000 0000 0000 0000 0000 0000 0111
0000 0000 0000 0000 0000 0000 0000 0001 (就是十进制1,所以下标为1)

上述运算实质是:001 与 111 & 运算。也就是哈希值的低三位与length与运算。如果让哈希值的低三位更加随机,那么&结果就更加随机。

如何让哈希值的低三位更加随机,那么就是让其与高位异或。
补充知识:

当length=8(1000)时 下标运算结果取决于哈希值的低三位
当length=16(10000)时 下标运算结果取决于哈希值的低四位
当length=32时(100000) 下标运算结果取决于哈希值的低五位
当length=2的N次方, 下标运算结果取决于哈希值的低N位。

4.3 原因总结

由于和(length-1)运算,length 绝大多数情况小于2的16次方。所以始终是hashcode 的低16位(甚至更低)参与运算。要是高16位也参与运算,会让得到的下标更加散列。

所以这样高16位是用不到的,如何让高16也参与运算呢。所以才有hash(Object key)方法。让他的hashCode()和自己的高16位^运算。所以(h >>> 16)得到他的高16位与hashCode()进行^运算。

4.4 为什么用^而不用&和|

因为&和|都会使得结果偏向0或者1 ,并不是均匀的概念,所以用^。

这就是为什么有hash(Object key)的原因。

4.4.1 位异或运算(^)

运算规则是:两个数转为二进制,然后从高位开始比较,如果相同则为0,不相同则为1

4.4.2 位与运算符(&)

运算规则:两个数都转为二进制,然后从高位开始比较,如果两个数都为1则为1,否则为0

4.4.3 位或运算符(|)

运算规则:两个数都转为二进制,然后从高位开始比较,两个数只要有一个为1则为1,否则就为0

4.4.4.位非运算符(~)

运算规则:如果位为0,结果是1,如果位为1,结果是0.

比如:~37
在Java中,所有数据的表示方法都是以补码的形式表示,如果没有特殊说明,Java中的数据类型默认是int,int数据类型的长度是8位,一位是四个字节,就是32字节,32bit.

8转为二进制是100101.
补码后为: 00000000 00000000 00000000 00100101
取反为: 11111111 11111111 11111111 11011010
因为高位是1,所以原码为负数,负数的补码是其绝对值的原码取反,末尾再加1。
因此,我们可将这个二进制数的补码进行还原: 首先,末尾减1得反码:11111111 11111111 11111111 11011001 其次,将各位取反得原码:
00000000 00000000 00000000 00100110,此时二进制转原码为38
所以~37 = -38.

5.TREEIFY_THRESHOLD树化域名,默认为8,链表长度超过8则需要转换为经黑树

原文链接:https://blog.csdn.net/qq_42034205/article/details/90384772

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 212,029评论 6 492
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,395评论 3 385
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 157,570评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,535评论 1 284
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,650评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,850评论 1 290
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,006评论 3 408
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,747评论 0 268
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,207评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,536评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,683评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,342评论 4 330
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,964评论 3 315
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,772评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,004评论 1 266
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,401评论 2 360
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,566评论 2 349

推荐阅读更多精彩内容