面试题总结-Java集合类系列(2)-HashMap

1. HashMap 概述

  HashMap 是Java开发者最常用的集合类之一,由数组和链表组合构成的数据结构,数组存的的是一个Map内部定义的对象类 Node,Node里面是以key和value的形式保存数据的。

  ArrayList 是继承了AbstractMap 类,实现了 Map 接口。AbstractMap 是一个抽象类,也是实现了 Map 接口。

public class HashMap<K,V> extends AbstractMap<K,V> implements Map<K,V>, Cloneable, Serializable { }

 

1.1 HashMap的常量

复制代码
    /**
     * The default initial capacity - MUST be a power of two.
     *
     * 默认初始容量16,必须是2的幂,为什么写成1 << 4 ,因为位与运算比算数计算的效率高了很多
     */
    static final int DEFAULT_INITIAL_CAPACITY = 1 << 4;

    /**
     * The maximum capacity, used if a higher value is implicitly specified
     * by either of the constructors with arguments.
     * MUST be a power of two <= 1<<30.
     *
     * HashMap的最大容量,当使用有参数的构造函数创建HashMap时,如果参数超过最大容量,就只使用MAXIMUM_CAPACITY
     */
    static final int MAXIMUM_CAPACITY = 1 << 30;

    /**
     * The load factor used when none specified in constructor.
     *
     * 负载因子,当数据达到容量的0.75时,会使用扩容操作
     */
    static final float DEFAULT_LOAD_FACTOR = 0.75f;

    /**
     * 链表转红黑树的阈值
     */
    static final int TREEIFY_THRESHOLD = 8;

    /**
     * 红黑树转回链表的阈值
     */
    static final int UNTREEIFY_THRESHOLD = 6;

    /**
     * 可将容器树形化的最小表容量
     */
    static final int MIN_TREEIFY_CAPACITY = 64;
复制代码

 

1.2 数据结构

  Node是JDK1.8之后,HashMap的基础数据对象。HashMap的底层数据结构就是Node数组,记做:Node<K,V>[]

  final int hash  记录了当前节点数据的hash值

  final K key   记录此节点的的key值

  V value  记录此节点的value值

  Node<K, V> next  记录此Node的下一个Node,用于形成链表的数据形式。

复制代码
    /**
     * Basic hash bin node, used for most entries.  (See below for
     * TreeNode subclass, and in LinkedHashMap for its Entry subclass.)
     */
    static class Node<K,V> implements Map.Entry<K,V> {
        final int hash;
        final K key;
        V value;
        Node<K,V> next;

        Node(int hash, K key, V value, Node<K,V> next) {
            this.hash = hash;
            this.key = key;
            this.value = value;
            this.next = next;
        }

        public final K getKey()        { return key; }
        public final V getValue()      { return value; }
        public final String toString() { return key + "=" + value; }

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

      ......
    }
复制代码

  另外,当出现重复的hash值,出现链表,并且链表长度大于8时,就会转为红黑树。

  红黑树的数据对象是 TreeNode,TreeNode 继承自 LinkedHashMap.Entry<K,V>,而 LinkedHashMap.Entry<K,V> 有继承自 HashMap.Node<K, V> ,因为存在父子关系,所以TreeNode 可以转换为 Node

复制代码
    /**
     * Entry for Tree bins. Extends LinkedHashMap.Entry (which in turn
     * extends Node) so can be used as extension of either regular or
     * linked node.
     */
    static final class TreeNode<K,V> extends LinkedHashMap.Entry<K,V> {
        TreeNode<K,V> parent;  // red-black tree links
        TreeNode<K,V> left;
        TreeNode<K,V> right;
        TreeNode<K,V> prev;    // needed to unlink next upon deletion
        boolean red;
        TreeNode(int hash, K key, V val, Node<K,V> next) {
            super(hash, key, val, next);
        }

        .....
}
复制代码

 

1.3 hash方法

  HashMap内部的计算hash值的方法,使用 key.hashCode 再与 h >>> 16 做异或运算,得到hash值

复制代码
    /**
     * Computes key.hashCode() and spreads (XORs) higher bits of hash
     * to lower.  Because the table uses power-of-two masking, sets of
     * hashes that vary only in bits above the current mask will
     * always collide. (Among known examples are sets of Float keys
     * holding consecutive whole numbers in small tables.)  So we
     * apply a transform that spreads the impact of higher bits
     * downward. There is a tradeoff between speed, utility, and
     * quality of bit-spreading. Because many common sets of hashes
     * are already reasonably distributed (so don't benefit from
     * spreading), and because we use trees to handle large sets of
     * collisions in bins, we just XOR some shifted bits in the
     * cheapest possible way to reduce systematic lossage, as well as
     * to incorporate impact of the highest bits that would otherwise
     * never be used in index calculations because of table bounds.
     */
    static final int hash(Object key) {
        int h;
        return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
    }
复制代码

 

1.4 HashMap的属性

复制代码
    /**
     * Node数组,用于记录具体数据。在第一次使用时初始化,容量是2的幂
     */
    transient Node<K,V>[] table;

    /**
     * Holds cached entrySet(). Note that AbstractMap fields are used
     * for keySet() and values().
     */
    transient Set<Map.Entry<K,V>> entrySet;

    /**
     * 键值对的数量
     */
    transient int size;

    /**
     * HashMap 结构修改的次数
     */
    transient int modCount;

    /**
     * 要调整容量的下一个阈值( (capacity * load factor).
     */
    int threshold;

    /**
     * Hash表的负载因子
     */
    final float loadFactor;
复制代码

 

1.5 构造方法

  HashMap():无参构造方式,使用默认初始容量16,默认负载因子0.75

  HashMap(int initialCapacity):参数是初始容量,默认负载因子是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;
    }


    /**
     * 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);
    }
复制代码

  HashMap(int initialCapacity) 调用的就是 下面这个构造函数:

  HashMap(int initialCapacity, float loadFactor) 参数包含了初始容量和负载因子。如果 initialCapacity 小于0抛异常;如果 initialCapacity 大于最大容量,使用最大容量完成初始化; 如果负载因子小于等于0,或者是NaN,抛异常;

复制代码
    /**
     * 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);
    }
复制代码

 

2. Put方法

  HashMap是使用 Put 方法添加数据的。

复制代码
    /**
     * 保存传入的key和value到Map里,如果key在map中已经存在,则替换原有的value
     *
     * @param key 数据键
     * @param value 数据值
     */
    public V put(K key, V value) {
        return putVal(hash(key), key, value, false, true);
    }

    /**
     * HashMap 的 Put 实现方法
     *
     * @param hash  数据键的hash值
     * @param key 数据键
     * @param value 数据值
     * @param onlyIfAbsent 只处理不存在的值(默认false,如果是true,不会改变原有值)
     * @param evict (默认ture,如果是false,则table处于创建模式)
     * @return 前一个值,如果没有则为空
     */
    final V putVal(int hash, K key, V value, boolean onlyIfAbsent, boolean evict) {
        // 定义变量
        // Node<K,V>[] tab 整个Node数组的临时变量
        // Node<K,V> p 计算出数组坐标上的值
        // int n  数组的容量
        // int i   计算出当前插入数据的数组坐标
        Node<K,V>[] tab; Node<K,V> p; int n, i;
        // 这种写法是先对变量赋值,再判断(执行完这句之后,变量tab和 n就已经有值了)
        // n是hashMap的容量,当table等于空、或table的长度=0,执行resize方法,重新赋值n
        if ((tab = table) == null || (n = tab.length) == 0)
            n = (tab = resize()).length;

        // i = (n - 1) & hash 计算出当前插入数据的数组坐标
        // 执行完这句之后,p已经被赋值,判断p是否为空
        if ((p = tab[i = (n - 1) & hash]) == null)
            // 当数组坐标上没有数据时,创建新的Node,并赋值给tab[i]
            tab[i] = newNode(hash, key, value, null);
        else {
            // 如果数组坐标上有值,则执行下面的逻辑
            // Node<K,V> e 当前要处理的Node的临时变量
            // K k 当前要处理的Node的key
            Node<K,V> e; K k;
            // 如果新值和旧值的hash和key都相同
            if (p.hash == hash &&
                ((k = p.key) == key || (key != null && key.equals(k))))
               // 原值p赋给e
                e = p;
           
            // 如果p 是 TreeNode类型,下面红黑树逻辑
            else if (p instanceof TreeNode)
                e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);

            // 因为数组坐标相同,执行链表逻辑
            else {
                // 向Node单向链表里添加数据
                for (int binCount = 0; ; ++binCount) {
                    if ((e = p.next) == null) {
                        // 如果next为空,创建新的Node节点,复制给p.next
                        p.next = newNode(hash, key, value, null);
                        // 判断节点数量是否大于等于8
                        if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st     
// 转红黑树 treeifyBin(tab, hash); break; } // 如果链表中hash和key都相同,退出循环 if (e.hash == hash && ((k = e.key) == key || (key != null && key.equals(k)))) break; // p是记录下一个节点 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; }
复制代码

 

3. 扩容

  当新HashMap初始化时,或者 HashMap容量达到扩容阈值时,都需要调用扩容方法。

  扩容方法主要分为2步:1-计算扩容后的 数组新容量、扩容阈值。 2-实现扩容

  1 计算 新的容量、扩容阈值

    1.1 当 oldCap(原始容量) > 0 时

      1.1.1 判断 oldCap(原始容量)>= 最大容量(MAXIMUM_CAPACITY),不予扩容,返回原始数组

      1.1.2 oldCap * 2 赋值给 newCap(新容量)。  当 newCap 小于最大容量,并且 oldCap >= 16,oldThr * 2 = newThr (新扩容阈值)

    1.2 原始容量=0,判断扩容阈值 oldThr > 0

      1.2.1 用扩容阈值 作为 新的容量

    1.3 oldCap = 0 and oldThr = 0 时,表示是个新的HashMap,需要初始化

      1.3.1 使用默认值,newCap=16, newThr = 0.75 * 16 = 12

    1.4 这应该是个补救措施,此时 newThr 如果还是0,重新计算一个新值。

  2 开始实际扩容操作

    2.1 这块我有总结不好,自己看代码注释吧

    

复制代码
    /**
     * 初始化,或者table容量加倍
     * @return the table
     */
    final Node<K,V>[] resize() {
        // table赋值给oldTab,作为扩容前的原始数据
        Node<K,V>[] oldTab = table;
        // 原始table容量
        int oldCap = (oldTab == null) ? 0 : oldTab.length;
        // 原始扩容阈值
        int oldThr = threshold;
        int newCap, newThr = 0;
        if (oldCap > 0) {
            // 1.1.1 判断oldCap如果大于等于最大容量,不予扩容,返回原始表
            if (oldCap >= MAXIMUM_CAPACITY) {
                threshold = Integer.MAX_VALUE;
                return oldTab;
            }

            // 1.1.2 原始容量*2小于最大容量 并且  原始容量大于等于默认初始容量
            else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY && oldCap >= DEFAULT_INITIAL_CAPACITY)
                // 扩容阈值*2 作为扩容阈值
                newThr = oldThr << 1;
        }

        // 1.2 老table逻辑,oldThr大于0
        else if (oldThr > 0) // initial capacity was placed in threshold
            // 1.2.1 用扩容阈值 作为 新的容量
            newCap = oldThr;

        // 1.3 新的table
        else {               // zero initial threshold signifies using defaults
            // 1.3.1 新的Map,使用默认值
            newCap = DEFAULT_INITIAL_CAPACITY;
            newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY);
        }

        // 1.4 若新表的扩容阈值=0
        if (newThr == 0) {
            // 用新容量和负载因子 计算一个
            float ft = (float)newCap * loadFactor;
            newThr = (newCap < MAXIMUM_CAPACITY && ft < (float)MAXIMUM_CAPACITY ? (int)ft : Integer.MAX_VALUE);
        }

        // 新的扩容阈值,赋值到threshold
        threshold = newThr;

        // 2 扩容之后,把数据重新赋值到新的数组
        @SuppressWarnings({"rawtypes","unchecked"})
        Node<K,V>[] newTab = (Node<K,V>[])new Node[newCap];
        // 把新创建的newTab赋值给table
        table = newTab;
        if (oldTab != null) {
            // 若oldTable中有值,就需要通过循环将oldTab的值保存到新表里
            for (int j = 0; j < oldCap; ++j) {
                Node<K,V> e;
                // 获取oldTab中的第j个元素,赋值给e
                if ((e = oldTab[j]) != null) {
                    // 将odTab的第j的元素设置为null
                    oldTab[j] = null;
                    // 若判断成立,表示e下面就没有数据节点了
                    if (e.next == null)
                        // 将e保存到新表的指定位置
                        newTab[e.hash & (newCap - 1)] = e;
                    // 如果e是TreeNode类型
                    else if (e instanceof TreeNode)
                       // 分割树,将新表和旧表分割成2个数,并判断索引处节点的长度是否需要转换成红黑树放入新表存储
                        ((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循环,获取新旧索引的节点
                        do {
                            next = e.next;
                            if ((e.hash & oldCap) == 0) {
                                if (loTail == null)
                                    loHead = e;
                                else
                                    loTail.next = e;
                                loTail = e;
                            }
                            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;
                        }
                        if (hiTail != null) {
                            hiTail.next = null;
                            newTab[j + oldCap] = hiHead;
                        }
                    }
                }
            }
        }
        // 返回新表
        return newTab;
    }
复制代码

 

 4. 查询方法

  查询的核心方法是getNode

  1. 首先判断Node[] table 是否为空,table的长度是否为0,通过key计算的数组坐标上有没有值。  如果有一条不满足,就返回null

  2. 验证查询出来的 Node first 变量,hash和key是否相同,全部相同,返回变量 first;对比不同,往后执行。

  3. 判断 first.next 是否有值,如果不为空,说明存在hash冲突,此处保存的是链表或红黑树

  4. 如果是红黑树的数据结构,调用getTreeNode 方法

  5. 如果是链表,遍历对比 next 的值,如果有hash和key都相同的值,返回结果。

  6. 没有查到,返回null

复制代码
   /**
     * 根据Key,从Map中查询Value
     */
    public V get(Object key) {
        // 定义返回值
        Node<K,V> e;
        // 调用getNode方法
        return (e = getNode(hash(key), key)) == null ? null : e.value;
    }

    /**
     * Map.get 的相关方法
     *
     * @param hash 使用key计算的hash值
     * @param 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;
        // 1. 当table不为空、table长度大于0,key所在的坐标不为空时
        // (n - 1) & hash 计算出数组坐标
        if ((tab = table) != null && (n = tab.length) > 0 && (first = tab[(n - 1) & hash]) != null) {
            // 2. 如果hash相同,key相同,返回变量first
            if (first.hash == hash && ((k = first.key) == key || (key != null && key.equals(k))))
                return first;
            
            // 3. 如果first.next不等于空,说明存在hash冲突,此处保存的是链表或红黑树
            if ((e = first.next) != null) {
                // 4. 如果是红黑树,调用getTreeNode方法
                if (first instanceof TreeNode)
                    return ((TreeNode<K,V>)first).getTreeNode(hash, key);
                // 5. 链表,循环对比链表的所有的hash和key
                do {
                    if (e.hash == hash &&
                        ((k = e.key) == key || (key != null && key.equals(k))))
                        return e;
                } while ((e = e.next) != null);
            }
        }
        // 6. 没有找到数据,返回null
        return null;
    }
复制代码

 

posted @   闲人鹤  阅读(38)  评论(0编辑  收藏  举报
相关博文:
阅读排行:
· 无需6万激活码!GitHub神秘组织3小时极速复刻Manus,手把手教你使用OpenManus搭建本
· Manus爆火,是硬核还是营销?
· 终于写完轮子一部分:tcp代理 了,记录一下
· 别再用vector<bool>了!Google高级工程师:这可能是STL最大的设计失误
· 单元测试从入门到精通
历史上的今天:
2020-06-29 [转] 详解Spring boot启动原理
点击右上角即可分享
微信分享提示