Java 1.8 HashMap详解

本文详细解析了Java 8 HashMap的实现原理,包括构造函数、put方法、扩容机制和获取元素的过程。HashMap利用链表和红黑树处理冲突,当冲突超过7个时,链表转为红黑树,提高查找效率。此外,对比了HashMap在1.8与1.7版本中的区别,如1.8版中高位与低位异或的hash算法和优化后的扩容策略。

Java 8 HashMap 详解

HashMap 实现了Map接口,继承于 AbstractMap。利用散列表来实现 Key-Value 元素的存取。散列表是用链表数组实现的,每个列表被称为桶 (bucket)。利用 key 的 hashcode 来确保元素的唯一性。HashMap 不保证元素的顺序恒定不变,在扩充的过程中,键值对元素位置会被再分配。
这里主要基于 JDK1.8 版本的 HashMap 源码进行分析。Map 相关的类图如下所示:


图片来自于网络

HashMap构造

HashMap 有两个参数影响其性能:初始容量和加载因子。容量是哈希表中桶的数量,初始容量是哈希表在创建时的容量。加载因子是哈希表在其容量自动增加之前可以达到多满的一种尺度。
HashMap 提供了三种构造函数。第一种通过直接设置初始容量和加载因子。

/**
     * Constructs an empty HashMap 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;
        //将初始容量转为向上最近的2的次方。例如初始为9转为16
        this.threshold = tableSizeFor(initialCapacity);
    }

第二种设置初始容量,使用默认的加载因子。

/**
     * Constructs an empty HashMap 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);
    }

第三种两个参数都使用默认的设置。初始容量16,加载因子0.75。

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

构造函数主要完成两个参数的设置,真正初始化散列表是等到添加元素的时候才进行。

HashMap 主要方法

put 方法
 /**
     * Associates the specified value with the specified key in this map.
     * If the map previously contained a mapping for the key, the old
     * value is replaced.
     *
     * @param key key with which the specified value is to be associated
     * @param value value to be associated with the specified key
     * @return the previous value associated with key, or
     *         null if there was no mapping for key.
     *         (A null return can also indicate that the map
     *         previously associated null with key.)
     */
    public V put(K key, V value) {
        return putVal(hash(key), key, value, false, true);
    }

先总结下整个流程,大体如下。

  1. 通过 key 获取 hashcode 值,再利用高低位16异或。
  2. 若桶为空,则申请。计算出位置索引进行添加。
  3. 无冲突,直接添加。有冲突添加在桶的尾部,当冲突超过七个,则转为红黑树。
  4. 若已存在,进行 value 替换。
  5. 插入成功后,若超过阈值,进行扩充。

具体的添加代码如下:

/**
     * Implements Map.put and related methods
     *
     * @param hash hash for key
     * @param key the key
     * @param value the value to put
     * @param onlyIfAbsent if true, don't change existing value
     * @param evict if false, the table is in creation mode.
     * @return previous value, or null if none
     */
    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;
        //计算出位置索引,桶为空则直接添加
        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);
            //是链表,判断是否已存在。不存在则添加到尾部。若冲突超过7个则转为红黑树
            else {
                for (int binCount = 0; ; ++binCount) {
                    if ((e = p.next) == null) {
                        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;
                }
            }
            //已存在,则进行是否覆盖操作
            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)
            resize();
        afterNodeInsertion(evict);
        return null;
    }
扩容机制

resize 方法主要进行初始化或扩充的处理。当 HashMap 的键值对的元素数量超过容量*加载因子,则需要进行容量扩展,防止更多的冲突出现,从而影响性能。因为底层是用数组保存的,扩容时候,需要重新申请数组,再将原始的添加到新数组里,当元素从原始数组里转移到新的数组中,其位置索引要么保持在原 index 处,或者保持在与原 index 的固定大小偏移处
具体代码如下:

/**
     * Initializes or doubles table size.  If null, allocates in
     * accord with initial capacity target held in field threshold.
     * Otherwise, because we are using power-of-two expansion, the
     * elements from each bin must either stay at same index, or move
     * with a power of two offset in the new table.
     *
     * @return the table
     */
    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;
            }
            //容量和阈值都翻倍
            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);
        }
        //初始化时,计算新的上限
        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) {
                    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);
                        //将原位置的结点放入
                        if (loTail != null) {
                            loTail.next = null;
                            newTab[j] = loHead;
                        }
                        //将原位置+oldCap的结点放入
                        if (hiTail != null) {
                            hiTail.next = null;
                            newTab[j + oldCap] = hiHead;
                        }
                    }
                }
            }
        }
        return newTab;
    }

当冲突以红黑树形态情况下,进行扩充时,将树转成两棵树,若树的的结点数小于等于UNTREEIFY_THRESHOLD,则转为链表形式。

/**
         * Splits nodes in a tree bin into lower and upper tree bins,
         * or untreeifies if now too small. Called only from resize;
         * see above discussion about split bits and indices.
         *
         * @param map the map
         * @param tab the table for recording bin heads
         * @param index the index of the table being split
         * @param bit the bit of hash to split on
         */
        final void split(HashMap<K,V> map, Node<K,V>[] tab, int index, int bit) {
            TreeNode<K,V> b = this;
            // Relink into lo and hi lists, preserving order
            TreeNode<K,V> loHead = null, loTail = null;
            TreeNode<K,V> hiHead = null, hiTail = null;
            int lc = 0, hc = 0;
            //遍历整棵树。
            for (TreeNode<K,V> e = b, next; e != null; e = next) {
                next = (TreeNode<K,V>)e.next;
                e.next = null;
                //原索引
                if ((e.hash & bit) == 0) {
                    if ((e.prev = loTail) == null)
                        loHead = e;
                    else
                        loTail.next = e;
                    loTail = e;
                    ++lc;
                }
                //原索引+偏移量
                else {
                    if ((e.prev = hiTail) == null)
                        hiHead = e;
                    else
                        hiTail.next = e;
                    hiTail = e;
                    ++hc;
                }
            }

            if (loHead != null) {
                //判断是否转成链表形态
                if (lc <= UNTREEIFY_THRESHOLD)
                    tab[index] = loHead.untreeify(map);
                else {
                    tab[index] = loHead;
                    if (hiHead != null) // (else is already treeified)
                        //进行树的处理,确保根节点在桶中
                        loHead.treeify(tab);
                }
            }
            if (hiHead != null) {
                if (hc <= UNTREEIFY_THRESHOLD)
                    tab[index + bit] = hiHead.untreeify(map);
                else {
                    tab[index + bit] = hiHead;
                    if (loHead != null)
                        hiHead.treeify(tab);
                }
            }
        }
获取元素

当需要从 HashMap 里通过 Key 的 hash 来获取元素时,先定位数组中的首个结点,若不同则比对红黑树中或者链表中是否存在。
具体代码如下:

   /**
     * Implements Map.get and related methods
     *
     * @param hash hash for key
     * @param key the key
     * @return the node, or null if none
     */
    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;
    }

HashMap 1.8 与 1.7 对比

  1. 红黑树的出现,1.8 中当每个桶中的冲突超过 7 个时,链表则会转成红黑树,让 O(N) 访问效率转为O(logN)。
  2. 在 JDK 1.8 的实现中,优化了高位运算的算法,通过 hashCode() 的高 16 位异或低 16 位实现的,目的为了使得位置索引更离散些。
  3. 1.7 中 resize,只有当 size >= threshold 并且 table 中的那个槽中已经有 Entry 时,才会发生 resize。1.8 中只要大于 threshold 即扩容。
  4. 1.7 中添加元素时候,有冲突时,先遍历整个链表,确认是否已存在,不存在则进行头插法。而 1.8 中有冲突时候,链表形态下,是添加在尾部的。
  5. 1.7 中扩充时候,也是采用头插法,会导致之前元素相对位置倒置了。而 1.8 中扩充时,链表形态下,采用尾插法。之前元素相对位置未变化。
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值