JDK11版HashMap源码全部解析(详细)-一文覆盖各方面

1. 概述

本文很长,详细描述了HashMap源码级别的实现原理,并讨论了包括扩容,hash计算,新建HashMap的开销等问题,同时还提供了一些外部资料。由于内容太多,建议阅读时结合目录快速跳转查看。

Java源码阅读最好采用IDEA,Ctrl + N 输入HashMap即可看到HashMap的源码了,HashMap总共有2444行源码
本文查看的是JDK-11.0.1的源码

文章目录

咱们按照源码顺序来分析HashMap,除了HashMap本身的变量和方法,HashMap中还定义了定义如下内部类:

1.1 内部类

  1. Node
  2. KeySet
  3. Values
  4. EntrySet
  5. HashIterator
  6. KeyIterator
  7. ValueIterator
  8. EntryIterator
  9. HashMapSpliterator
  10. KeySpliterator
  11. ValueSpliterator
  12. EntrySpliterator
  13. TreeNode:这个类代表红黑树节点,HashMap中对红黑树的操作的方法都在此类中

1.2 基本实现

HashMap底层使用哈希表(数组 + 单链表),当链表过长会将链表转成 红黑树以实现 O(logn) 时间复杂度内查找。

HashMap的定义class HashMap<K,V> extends AbstractMap<K,V> implements Map<K,V>, Cloneable, Serializable

1.3 扩容原理

HashMap采用的扩容策略是,每次加倍,这样,原来位置的Entry在新扩展的数组中要么依然在原来的位置,要么在<原来位置+原来的容量>的位置。

1.4 hash计算

hash()函数计算hash值方法为(key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16),计算出的hash值会被缓存在Node.hash中。

为什么这样计算hash值

hash值计算相当于就是将高16位与低16位进行异或,结果是高16不变,低16位变成其异或的新结果。

为什么让低16位与高16为异或合成一个新的结果呢?是因为HashMap的容量通常比较小,在进行长度取模运算时采用的是只取二进制最右端几位,这样高位的二进制信息就没有用到,所带来的结果就是Hash结果分布不太均匀。而高16位与低16位异或后就可以让低位附带高位的信息,加大低位的随机性。具体请参考JDK 源码中 HashMap 的 hash 方法原理是什么? - 胖君的回答 - 知乎

不明白异或结果的朋友来看下这段验证代码,复制此代码运行即可明白高16位与低16位的异或的结果:

import java.util.Random;

class Scratch {
   
    public static void main(String[] args) {
   
        generateTestCase(41132564);

        Random random = new Random();
        for (int j = 0; j < 10; j++) {
   
            generateTestCase(random.nextInt(Integer.MAX_VALUE));
        }
    }

    /**
     * 显示根据key的hashCode算出最终元素的hash值
     *
     * @param hashCode 代表key的hashCode
     */
    public static void generateTestCase(int hashCode) {
   
        System.out.println("hashCode = " + hashCode + " 时");
        show(hashCode);

        int k = hashCode >>> 16;
        show(k);

        int x = hashCode ^ k;
        show(x);

        System.out.println();
    }

    /**
     * 显示一个数字的二进制,按照高16位在左,低16位在右的方式显示
     */
    public static void show(int n) {
   
        String s = Integer.toBinaryString(n);
        s = fillZero(s);

        System.out.print(s.substring(0, 16));
        System.out.print("  |  ");
        System.out.println(s.substring(16));
    }

    /**
     * 填充0到字符串前面使得总长32
     */
    public static String fillZero(String src) {
   
        StringBuilder sb = new StringBuilder(32);
        for (int i = 0; i < 32 - src.length(); i++) {
   
            sb.append('0');
        }
        return sb.append(src).toString();
    }
}

这就是为什么HashMap可以放入键值null,因为计算hash中为null的hash值为0,然后putVal插入

1.5 元素实际位置计算

根据hash()获取元素所在链表的位置的方法为:tab[(n - 1) & hash],由于n为容量是2的幂,n-1的二进制形式是111111这类二进制左边全1的形式,所以这个方法本质是截取hash二进制相应长度的0和1,如下例。

hash:   10111101
n - 1:  00111111
result: 00111101
// hash的最左端的1没有了,相当于只取二进制最右端几位

1.6 插入null原理

在hash计算中(上文),null的hash值为0,然后按照正常的putVal()插入

1.7 new HashMap()开销

从源码中(下文构造函数)我们可以看到:

new HashMap()开销非常少,仅仅确认装载因子。真正的创建table的操作尽可能的往后延迟,这使得HashMap有不少操作都需要检查table是否初始化。这种设计我猜想应该是为了让人们可以不用担心创建HashMap的开销,大量创建HashMap,比如ArrayList<HashMap> a = new ArrayList<>(1000)

2. HashMap的变量

2.1 DEFAULT_INITIAL_CAPACITY

HashMap的默认容量是16,被DEFAULT_INITIAL_CAPACITY定义。

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

2.2 MAXIMUM_CAPACITY

其最大容量为 1073741824(2的30次方)

static final int MAXIMUM_CAPACITY = 1 << 30;

2.3 DEFAULT_LOAD_FACTOR

默认装载因子0.75

static final float DEFAULT_LOAD_FACTOR = 0.75f;

2.4 TREEIFY_THRESHOLD

将链表转换成红黑树的阈值为8,即当链表长度>=8时,链表转换成红黑树

static final int TREEIFY_THRESHOLD = 8;

2.5 UNTREEIFY_THRESHOLD

将红黑树转换成链表的阈值为6(<6时转换),注意,这个是在resize()的过程中调用TreeNode.split()实现

static final int UNTREEIFY_THRESHOLD = 6;

2.6 MIN_TREEIFY_CAPACITY

要树化并不仅仅是超过TREEIFY_THRESHOLD ,同时容量要超过MIN_TREEIFY_CAPACITY,如果只是超过TREEIFY_THRESHOLD,则会进行扩容(调用resize(),因为扩容可以让链表变短),直到扩容>=MIN_TREEIFY_CAPACITY

static final int MIN_TREEIFY_CAPACITY = 64;

2.7 table

哈希表的数组主体定义,使用时初始化,在构造函数中并不会初始化,所以在各种操作中总是要检查其是否为null

transient Node<K,V>[] table;

2.8 entrySet

作为一个entrySet缓存,使用entrySet方法首先检查其是否为null,不为null则使用这个缓存,否则生成一个并缓存至此。

transient Set<Map.Entry<K,V>> entrySet;

2.9 size

HashMap中Entry的数量

transient int size;

2.10 modCount

记录修改内部结构化修改次数,用于实现fail-fast,ConcurrentModificationException就是通过检测这个抛出

transient int modCount;

2.11 threshold

其值=capacity * load factor,当size超过threshhold便进行一次扩容

int threshold;

2.12 loadFactor

装载因子

final float loadFactor;

2.13 serialVersionUID

用于序列化

private static final long serialVersionUID = 362498820763181265L;

3. HashMap的方法

3.1 构造函数

  1. 该构造函数并不初始化transient Node<K,V>[] table;,进行容量和装载因子的(范围)合法性验证,然而并没有对容量进行存储,只是用来确定扩容阈值threshold

    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. 显然

    public HashMap(int initialCapacity) {
         
            this(initialCapacity, DEFAULT_LOAD_FACTOR);
        }
    
  3. 无参构造函数仅仅确认装载因子

    public HashMap() {
         
            this.loadFactor = DEFAULT_LOAD_FACTOR; // all other fields defaulted
        }
    
  4. 通过Map构造HashMap时,使用默认装载因子,并调用putMapEntries将Map装入HashMap

    public HashMap(Map<? extends K, ? extends V> m) {
         
            this.loadFactor = DEFAULT_LOAD_FACTOR;
            putMapEntries(m, false);
        }
    

3.2 hash(Object key)

Hash函数负责产生HashCode,计算法则为若key为null则返回0,否则:对key的hashCode的高16位和低16位进行异或

static final int hash(Object key) {
   
        int h;
        return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);// >>>表示无符号右移
}

3.3 comparableClassFor(Object x)

对于一个Object,若其定义时是class X implement Comparable<X>,则返回X,否则返回null,注意一定Comparable<X>中的X一定得是X不能为其子类或父类,用于红黑树中的比较

  • 源码如下
static Class<?> comparableClassFor(Object x) {
   
        if (x instanceof Comparable) {
   
            Class<?> c; Type[] ts, as; ParameterizedType p;
            if ((c = x.getClass()) == String.class) // bypass checks
                return c;
            if ((ts = c.getGenericInterfaces()) != null) {
   
                for (Type t : ts) {
   
                    if ((t instanceof ParameterizedType) &&
                        ((p = (ParameterizedType) t).getRawType() ==
                         Comparable.class) &&
                        (as = p.getActualTypeArguments()) != null &&
                        as.length == 1 && as[0] == c) // type arg is c
                        return c;
                }
            }
        }
        return null;
    }
  • 验证代码如下,将如下代码拷贝即可明白其原理。
import java.lang.reflect.ParameterizedType;
import java.lang.reflect.Type;
class Scratch {
   
    public static void main(String[] args) {
   
        System.out.println(comparableClassFor(new C()));// class Scratch$C
        System.out.println(comparableClassFor(new CS()));// null
        System.out.println(comparableClassFor(new CSI()));// null
        System.out.println(comparableClassFor(new CSIC()));// class Scratch$CSIC
    }
    static Class<?> comparableClassFor(Object x) {
   
        if (x instanceof Comparable) {
   
            Class<?> c; Type[] ts, as; ParameterizedType p;
            if ((c = x.getClass()) == String.class) // bypass checks
                return c;
            if ((ts = c.getGenericInterfaces()) != null) {
   
                for (Type t : ts) {
   
                    if ((t instanceof ParameterizedType) &&
                            ((p = (ParameterizedType) t).getRawType() ==
                                    Comparable.class) &&
                            (as = p.getActualTypeArguments()) != null &&
                            as.length == 1 && as[0] == c) // type arg is c
                        return c;
                }
            }
        }
        return null;
    }
    static class C implements Comparable<C> {
   
        @Override
        public int compareTo(C o) {
   
            return 0;
        }
    }
    static class CS extends C {
   }
    static class CSI implements Comparable<C> {
   
        @Override
        public int compareTo(C o) {
   
            return 0;
        }
    }
    static class CSIC implements Comparable<CSIC> {
   
        @Override
        public int compareTo(CSIC o) {
   
            return 0;
        }
    }
}

3.4 compareComparables(Class<?> kc, Object k, Object x)

如果x=null,返回0;如果x的类型为kc,则返回k.compare(x);否则返回0

static int compareComparables(Class<?> kc, Object k, Object x) {
   
        return (x == null || x.getClass() != kc ? 0 :
                ((Comparable)k).compareTo(x));
    }

3.5 tableSizeFor(int cap)

对于给定cap,计算>=cap的2的幂。用于计算table数组大小

static final int tableSizeFor(int cap) {
   
        int n = -1 >>> Integer.numberOfLeadingZeros(cap - 1);
        return (n < 0) ? 1 : (n >= MAXIMUM_CAPACITY) ? MAXIMUM_CAPACITY : n + 1;
    }

3.6 putMapEntries(Map<? extends K, ? extends V> m, boolean evict)

先确定放入map时容量是否应该调整,调整好后,通过putVal一个个放入

final void putMapEntries(Map<? extends K, ? extends V> m, boolean evict) {
   
        int s = m.size();
        if (s > 0) {
   //放入的map的size要大于0才插入
            if (table == null) {
    // pre-size,如果本map的table未初始化(同时没有任何元素),就根据放入map大小以及loadfactor计算出threshold,依然不初始化table
                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)//放入的map超过threshold就扩容
                resize();
            //到这里容量问题解决了,就一个一个putVal插入
            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.7 size()

直接返回size变量的值

public int size(
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值