JDK源码分析之HashMap(一)

176 阅读5分钟

本文是自己阅读源码时产生的一些想法以及结合日常使用做一些相关讲解,欢迎各位大牛指点。

一、前言

由于这会涉及到一些源码,所以默认读此文章的人对JDK已经有了一些认知,希望看完这个文章对于JDK1.8版本的HashMap有一定理解。

二、HashMap常用家族

首先会先列出整个家族(当然全列出来就太多了,而且还有好多都不常用,就只列出了自己平常用的较多那几个),如下图:

下面会对HashMap中常用方法的讲解以及对应源码的分析,图中的TreeMap因为用到了红黑树的知识,会放到另一个额外讲解。
配合HashMap源码可以清晰看到它是继承自AbstractMap同时实现了Map接口

三、HashMap常用方法

在讲解HashMap之前,先说说Map中有哪些方法吧:

  1. int size();
  2. boolean isEmpty();
  3. boolean containsKey(Object key);
  4. boolean containsValue(Object value);
  5. V get(Object key);
  6. V put(K key, V value);
  7. V remove(Object key);
  8. void putAll(Map<? extends K, ? extends V> m);
  9. void clear();
  10. Set keySet();
  11. Collection values();
  12. Set<Map.Entry<K, V>> entrySet();

这12个就是Map自己定义的方法,同时也是HashMap中常用的方法,HashMap中也加了很多自己的方法 在说方法之前必须要了解一些定义的属性

初始化默认容器大小,这里必须是2的幂次,具体说明放在下面的构造方法中
默认加载因子(也可以叫做扩容因子)0.75,具体说明放在下面的构造方法中

  1. 四种构造方法说明
    • HashMap()
    • HashMap(int initialCapacity)
    • HashMap(int initialCapacity, float loadFactor)
    • HashMap(Map<? extends K, ? extends V> m)
    • **比较:**前三种构造方法的使用,区别就在于是否传入了初始化容器的参数和加载因子,在构造HashMap的时候如果没有传入初始化容器,这时候是不会去给容器初始化赋值,只有在往容器中put元素的时候才会给它赋值为16,后两个方法加载因子的区别就在于什么时候会使容器会扩容,举个例子:默认容器大小为16,加载因子为0.75的情况下,在你添加在第12个元素的时候判断大于(16*0.75)的边界值了,就会进行扩容,扩容为原来的两倍
    • 如图所示,我只截取了一部分扩容的源码,框内的代码就是判断原先的容器不为空,接着判断即使原容器扩容2倍仍小于配置的最大值(2^30,配置的这个最大数据基本够用了),就将原容器扩大两倍。
    • 而最后一种的构造方法则是创建一个新的HashMap并将参数Map中的值也放置到新的容器中,使用的都是默认的容器初始化大小和加载因子
  2. final V putVal(int hash, K key, V value, boolean onlyIfAbsent,boolean evict) 说明
    • 其中一共提供了三种put方式putIfAbsent(K key, V value);put(K key, V value);putAll(Map<? extends K, ? extends V> m)但是不管是哪一种,最后用的方法都是putVal这个方法。
    • 下面是我将putVal源码进行了注释,方便解读。
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是否为空,为空则去赋值
        if ((tab = table) == null || (n = tab.length) == 0)
            n = (tab = resize()).length;
        //判断当前hash值得判断tab中对应位置是否为空,为空就创建
        if ((p = tab[i = (n - 1) & hash]) == null)
            tab[i] = newNode(hash, key, value, null);
        else {
            Node<K,V> e; K k;
            //对应位置不为空,并且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值不相等
                for (int binCount = 0; ; ++binCount) {
                    if ((e = p.next) == null) {
                        //JDK1.8 默认结点长度大于8就会转成红黑树
                        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;
                }
            }
            //onlyIfAbsent 配置为true,则不会替换当前值
            if (e != null) { // existing mapping for key
                V oldValue = e.value;
                if (!onlyIfAbsent || oldValue == null)
                    e.value = value;
                //留着给LinkedHashMap回调使用
                afterNodeAccess(e);
                return oldValue;
            }
        }
        ++modCount;
        //判断当前size+1之后是否大于扩容边界
        if (++size > threshold)
            //扩容
            resize();
        //留着给LinkedHashMap回调使用
        afterNodeInsertion(evict);
        return null;
    }
  1. 上述代码中的hash值,都是调用了该方法来实现的,其中就是因为此处的扰动算法要求了容量必须为2 的幂次。
  2. 由于Map中还有一个Entry<K,V> 的接口,该接口中有getKey();getValue();setValue(V value);等等所以HashMap中可以使用entrySet()来遍历。

四、总结

其中提供的方法还有很多,其中还提供了不少的方法留给LinkedHashMap,具体相关的方法研究还是需要自己去查看源码,这在提供一个源码的查看方法,建议学会使用Ide的快捷键以及里面的标签来阅读源码

例如,图中的锁可以看出哪些是公有的,哪些是私有的,哪些是预留给子类回调,还有后面的箭头可以清晰的告诉你继承于哪个类或者是接口。 在阅读源码的过程中也不需要通篇全读,毕竟太累了,借助各种工具,去看看你用到的或者是感兴趣的就好。