面试题总结-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; }
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 无需6万激活码!GitHub神秘组织3小时极速复刻Manus,手把手教你使用OpenManus搭建本
· Manus爆火,是硬核还是营销?
· 终于写完轮子一部分:tcp代理 了,记录一下
· 别再用vector<bool>了!Google高级工程师:这可能是STL最大的设计失误
· 单元测试从入门到精通
2020-06-29 [转] 详解Spring boot启动原理