HashMap
public class HashMap<K,V> extends AbstractMap<K,V> implements Map<K,V>, Cloneable, Serializable |
HashMap是Map接口的实现类,并且还是之前讲到的AbstractMap的子类,所以它是有AbstractMap的属性、方法。
HashMap是根据key的hashCode值存储数据的,大多数情况下可以直接定位到它的值,从而具有很快的访问速度,但遍历顺序是不确定的。
HashMap是非安全的,即任一时刻有多个线程同时写HashMap,会导致数据不一致,如果要实现线程安全可以用Collection的synchronizedMap方法使得HashMap具有线程安全能力,或使用ConcurentHashMap,而且最多值允许有一条记录key为null,和多条value为null。
HashMap和HashSet的实现接口规范不同,但底层的Hash存储机制是完全一样的,甚至HashSet本身就采用HashMap来实现的。
对于HashSet而言,系统采用Hash算法决定集合元素的存储位置,这样保证能快速存、取集合元素。而对于HashMap而言,K-V会被当成一个整体进行处理,系统根据Hash算法计算K-V存储位置,这样保证能快速存取Map的K-V对。
HashMap在解决哈希冲突上是使用链地址法,也就是数组+ 链表的方式。
哈希解释:https://blog.csdn.net/Su_Levi_Wei/article/details/104905121
JDK1.7-HashMap
在看网上文章时,会看到无数的文章在说,JDK1.8的HashMap改动很大,性能提升很高,那么JDK-1.8的上个版本1.7是怎么样的?
主干是数组,而且这是一个Entry数组,初始值为空数组,长度一定是2的幂。
/** * The table, resized as necessary. Length MUST Always be a power of two. */ transient Entry<K,V>[] table = (Entry<K,V>[]) EMPTY_TABLE;
/** * 实际存储的K-V键值对的个数 */ transient int size;
/** * 阈值,当table == {}时,初始化容量默认为16,当table被填充了,也就是为table分配内存空间后, * threshold一般为capacity * loadFactory,超过就会扩容 */ int threshold;
/** * 负载因子,代表了table的填充度有多少,默认是0.75,加载因子存在的原因在于减缓哈希冲突, * 如果初始桶为16,等到满16个元素才扩容,某些桶里可能就有不止一个元素了。 * 所以加载因子默认为0.75,也就是说大小为16的HashMap,到了13个元素,就会扩容成为32。 */ final float loadFactor;
/** * HashMap被改变的次数,由于HashMap非线程安全,在对HashMap进行迭代时, * 如果期间其他线程的参与导致HashMap结构出现变化了,就需要抛出ConcurrentModificationException(ArrayList的Fail-Fast机制) */ transient int modCount; |
Entry是HashMap中的一个静态内部类。
static class Entry<K,V> implements Map.Entry<K,V> { final K key; V value; Entry<K,V> next; //指向下一个Entry的引用,单链表的结构。 int hash; //对key的hashCode进行运算后得到的值,存储起来,避免重复计算 |
这个Entry应该放在数组的哪个位置上(这个位置通常称为为或Hash桶,即hash值相同的Entry会放在同一位置,用链表相连),是通过key的hashCode计算。
/** * Retrieve object hash code and applies a supplemental hash function to the * result hash, which defends against poor quality hash functions. This is * critical because HashMap uses power-of-two length hash tables, that * otherwise encounter collisions for hashCodes that do not differ * in lower bits. Note: Null keys always map to hash 0, thus index 0. */ final int hash(Object k) { int h = hashSeed; if (0 != h && k instanceof String) { return sun.misc.Hashing.stringHash32((String) k); }
h ^= k.hashCode();
// This function ensures that hashCodes that differ only by // constant multiples at each bit position have a bounded // number of collisions (approximately 8 at default load factor). h ^= (h >>> 20) ^ (h >>> 12); return h ^ (h >>> 7) ^ (h >>> 4); } |
总体结构:
总体结构:
简单来说,HashMap就是由数组+链表组成,数组是HashMap的主体,链表则是主要为了解决哈希冲突而存在的,如果定位到的数组位置不含列表(当前Entry的next指向为Null),那么查找、添加等操作很快,仅需要一次寻址。
如果定位到的数组包含列表,对于添加操作,其时间复杂度为O(n),首先遍历链表,存在即覆盖,否则新增;对于查找操作来说,仍然需要遍历链表,然后通过Key对象的equals方法逐一对比、查找,所以,性能考虑,HashMap中的链表出现的越少,性能才会越好。
通过hash计算出来的值,会使用indexFor方法找到应该所在的table下标。
/** * Returns index for hash code h. */ static int indexFor(int h, int length) { // assert Integer.bitCount(length) == 1 : "length must be a non-zero power of 2"; return h & (length-1); } |
这个方法相当于对table.length取模,这里的hash值和(length - 1),length=传入的容量16的话,16-1=15,二进制1111,即对h取低四位,从而对应0~15个桶。
即无论我们指定的容量为多少,构造方法都会将实际容量设为不小于指定容量的2的次方的一个数,且最大值不能超过2的30次方。
当两个key通过hashCode计算相同时,则发生了hash冲突(碰撞),HashMap解决hash冲突的方式是用链表。
当发生hash冲突时,则将存放在数组中的Entry设置为新值的next。
比如A和B进行hash后,都映射到下标x。之前已经有A了,当map.put(B)时,将B放在下标(x)中,A则为B的next,所以新值存放在数组中,旧值在新值的链表上。
所以,当hash冲突很多时,HashMap退化成链表。
注:牢记原则,hashCode相同,equals不一定相同,所以放在同一个数组位置,成为链表后,接下来到同一个位置查找时,需要进行对链表的key数据进行equals。
看一下put过程,继续理解。
HashMap在put一对键值时,会根据key的hashCode值计算出一个位置,该位置就是此对象准备往数组存放的位置,如果该位置没有对象存在,就将此对象之间放进数组中。
如果该位置已经有对象存在了,则顺着此存在的对象的链,开始寻找(为了判断是否相同的)。
public V put(K key, V value) { //当table数组为空数组,进行数组填充(为table分配实际内存空间),入参为threshold //此时thresholid为initialCapacity,默认是1<<4(24=16) if (table == EMPTY_TABLE) { inflateTable(threshold); } //如果key为null,存储位置为table[0]或table[0]的冲突链上 if (key == null) return putForNullKey(value); //对key的hashcode进一步计算,确保散列均匀 int hash = hash(key); //获取在table中的实际位置 int i = indexFor(hash, table.length); for (Entry<K,V> e = table[i]; e != null; e = e.next) { //如果该对应数据已UC你在,执行覆盖操作,用新vlaue替换旧value,并返回旧value。 Object k; if (e.hash == hash && ((k = e.key) == key || key.equals(k))) { V oldValue = e.value; e.value = value; e.recordAccess(this); return oldValue; } }
modCount++;//Fail-Fast机制,Array同理的线程不安全的快速失败机制 //新增一个entry addEntry(hash, key, value, i); return null; }
void addEntry(int hash, K key, V value, int bucketIndex) { if ((size >= threshold) && (null != table[bucketIndex])) { //当size超过临界阈值threshold,并且即将发生哈希冲突时进行扩容 resize(2 * table.length); hash = (null != key) ? hash(key) : 0; bucketIndex = indexFor(hash, table.length); }
createEntry(hash, key, value, bucketIndex); } |
在addEntry中可以看到,当发生哈希冲突并且size大于阈值时(size大于threshold),需要进行数组扩容,扩容时,需要新建一个长度为之前数组2倍的新的数组,然后将当前的Entry数组中的元素全部传输过去,扩容后的新数组长度为之前的2倍,所以扩容相对来说说是个耗资源的操作。时,发生扩容。thre
JDK1.7中的resize,只有当size>=threshold并且table中的那个槽中已经有Entry时,才回发生resize,即有可能虽然size>=threshold,但是必须等到每个槽都至少有一个Entry时,才会扩容,还有每次resize都会扩大一倍容量。
JDK1.8-HashMap
一直到JDK1.7为止,HashMap的结构是如上这样的,基于一个数组以及多个链表实现,hash值冲突的时候,就将对应节点以链表的形式存储。
这样子的HashMap在性能就有一定的疑问,如果说成千上百个节点在hash时发生碰撞,存储在一个链表中,那么如果要查找其中一个节点,那么久不可避免的花费O(n)的查找时间,这将是极大的性能损失。
在JDK1.8中,这个问题已经得到了解决,在最坏的情况下,链表的查找时间复杂度为O(n),而红黑树一直是O(logn),这样会提高HashMap的效率。
JDK1.7中的HashMap采用的是位桶+链表的方式,即散列链表的方式,而JDK1.8采用的是位桶+链表/红黑树的方式,也是非线程安全的。在当某个桶的链表长度达到某个阀值的时候,这个链表就会转换成红黑树。
即JDK1.8中,当同一个hash值的节点数不小于8时,将不在以单链表的形式存储了,会被调整成为一颗红黑树,这就是JDK1.7和JDK1.8中HashMap实现的最大区别。
注:以下内容以1.8为主。
JDK中的Entry的名字变成了Node,原因是要与红黑树的TreeNode相关联。
/** * The table, initialized on first use, and resized as * necessary. When allocated, length is always a power of two. * (We also tolerate length zero in some operations to allow * bootstrapping mechanics that are currently not needed.) */ transient Node<K,V>[] table; |
Node和Entry一样,也是HashMap的静态内部类。
//Node依旧还是单向链表 static class Node<K,V> implements Map.Entry<K,V> { final int hash; //该桶的hash值,即Key的hashCode值 final K key; //不可变的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); }
public final V setValue(V newValue) { V oldValue = value; value = newValue; return oldValue; } //判断两个node是否相等,如果K-V都相等,返回true,可以与自身比较为true public final boolean equals(Object o) { if (o == this) return true; if (o instanceof Map.Entry) { Map.Entry<?,?> e = (Map.Entry<?,?>)o; if (Objects.equals(key, e.getKey()) && Objects.equals(value, e.getValue())) return true; } return false; } } |
属性说明
/** * 默认初始化容量,即2的4次方,初始值为16 */ static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; // aka 16
/** * Node数组最大容量为2的30次方 */ static final int MAXIMUM_CAPACITY = 1 << 30;
/** * 负载因子,兼容以前的版本,构造方法没有指定时,则使用默认的 */ static final float DEFAULT_LOAD_FACTOR = 0.75f;
/** * add到某个位桶时,这个桶的链表节点达到8,就把链表转换红黑树 */ static final int TREEIFY_THRESHOLD = 8;
/** * remove某个位桶时,这个桶的红黑树节点达到6,就把红黑树转换链表 */ static final int UNTREEIFY_THRESHOLD = 6;
/** * 转为红黑树时,最小长度 */ static final int MIN_TREEIFY_CAPACITY = 64;
/** * 存储具体元素的集合 */ transient Set<Map.Entry<K,V>> entrySet;
/** * 实际存储的个数 */ transient int size;
/** * 操作次数,和ArrayList的Fail-Fast机制是一样的 */ transient int modCount;
/** * 临界值,当table == {}时,初始化容量默认为16,当table被填充了,也就是为table分配内存空间后, * 临界值就是当实际大小(容量*负载因子)超过临界值,会进行扩容 * threshold = capacity * loadFactory。 */ int threshold;
/** * 负载因子,代表了table的填充度有多少,默认是0.75,加载因子存在的原因在于减缓哈希冲突, * 如果初始桶为16,等到满16个元素才扩容,某些桶里可能就有不止一个元素了。 * 所以加载因子默认为0.75,也就是说大小为16的HashMap,到了13个元素,就会扩容成为32。 */ final float loadFactor; |
负载因子默认是0.75,如果填充比很大,说明利用的空间很多,如果一直不进行扩容的话,链表就会越来越长,这样查找效率很低,因为链表的长度很大(JDK1.8用了红黑色好很多),扩容之后,将原来链表数组的每一个链表分成奇偶两个子链表分别挂在新链表数组的散列位置,这样就减少了每个链表的长度,增加了查询效率。
如果负载因子越大,对空间利用更充分,但是查找效率会降低(链表长度会越来越长)。
如果加载因子太小,那么表中的数据将会过于稀疏(很多空间还没有用就开始扩容),对空间造成严重的浪费。
在使用时,没有在构造方法中指定,系统默认是0.75,这是一个比较理想的值,一般情况不需要修改。
构造器
/** * 构造器1 * @param initialCapacity 初始容量 * @param loadFactor 负载因子 * @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调用构造器1 * @param initialCapacity 初始化容量大小 * @throws IllegalArgumentException if the initial capacity is negative. */ public HashMap(int initialCapacity) { this(initialCapacity, DEFAULT_LOAD_FACTOR); }
/** * 构造器3 * 默认的容量大小(16)和负载因子(0.75) */ public HashMap() { this.loadFactor = DEFAULT_LOAD_FACTOR; // all other fields defaulted }
/** * 构造器4 * 用m的元素初始化散列映射 * @param m 映射集合 * @throws NullPointerException if the specified map is null */ public HashMap(Map<? extends K, ? extends V> m) { this.loadFactor = DEFAULT_LOAD_FACTOR; putMapEntries(m, false); } |
基础方法
/** * 根据key,获取hash值 */ static final int hash(Object key) { int h; //当key为null是会返回0的,HashMap允许key为null,进行存储,且存储在table[0]的位置 //如果key不为null,会对获取的hashCode进行高低16位按位与(右移动16位),减少hash冲突的概率 return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16); }
/** * 这个方法的作用就是看看x的Class是否实现了Comparable<x的class> * 如果对象x的类型实现了Comparable<C>接口,那么返回x的类型,否则返回null */ static Class<?> comparableClassFor(Object x) { //判断是否实现了Comparable接口 if (x instanceof Comparable) { Class<?> c; Type[] ts, as; Type t; ParameterizedType p;
//如果对象的x的实体类对应为String类型,则直接返回对象x的实体类 if ((c = x.getClass()) == String.class) // bypass checks return c;
//反射 //调用getGenericInterfaces获取实体类c的所有实现类接口里面的type或者类 if ((ts = c.getGenericInterfaces()) != null) { //遍历这些类和Type for (int i = 0; i < ts.length; ++i) {
//ParameterizedType结合getRawType获取这些实现接口的参数化类型,并且和Comparable相等 if (((t = ts[i]) instanceof ParameterizedType) && ((p = (ParameterizedType)t).getRawType() == Comparable.class) && //获取参数化类型里面的参数,并且参数类型只有一个, //当且类型为C,因为Comparable<T>只有一个参数泛型,这里是对Comaparable<T>的校验 (as = p.getActualTypeArguments()) != null && as.length == 1 && as[0] == c) // type arg is c return c; } } } return null; }
/** * 比较节点类型 * kc:新增的节点key类型 * k:新增的节点的key * x:当前节点的key */ @SuppressWarnings({"rawtypes","unchecked"}) static int compareComparables(Class<?> kc, Object k, Object x) {
//当前节点x为空 或 当前节点key的类型不同于新增的节点key的类型 //则返回0,否则返回k.compareTo(x)比较的节点 return (x == null || x.getClass() != kc ? 0 : ((Comparable)k).compareTo(x)); }
/** * 这个方法的作用就是,让容量变为2的倍数. * 当实例化HashMap时,如果给定了initialCapacity(初始容量),由于HashMap的capacity都是2的幂。 * 因此这个方法用于找到大于等于initialCapacity的最小的2的幂。 * 如果initialCapacity就是2的幂,则返回还是这个数。 */ static final int tableSizeFor(int cap) { //让cap-1,在赋值给n的目的是另找到的目标值大于或等于原值 //首先让cap-1再赋值给n的目的是另找到的目标值大于或等于原值,这是为了防止,cap已经是2的幂。 //如果cap已经是2的幂,又没有执行-1的操作,则执行完后面的几条无符号右移操作之后,返回的capacity将是这个capacity的2倍。 int n = cap - 1;
//由于n不等于0,则n的二进制表示中总会有一bit为1,这时考虑最高位的1。通过无符号右移1位,则将最高位的1右移了1位, //由于n不等于0,则n的二进制表中总会有bit为1,这时考虑最高位的1, //通过无符号右移1位,将最高位的1右移动1位,再做或操作,使得n的二进制表中与最高位的1紧邻的右边1位也为1,如000011xxxxxx n |= n >>> 1;
//这个n已经过了n |= n >>> 1操作,假设此时n为000011xxxxxx,则n无符号右移动两位。 //将最高位两个连续的1右移2为,然后在与原来的n做或操作,这样n的二进制标识的高位中会有4个连续的1,如如00001111xxxxxx n |= n >>> 2;
//这次把已经有的高位中的连续4个1,右移4位,再做或操作,这样的n的二进制标识的高位中会有8个连续的1,如00001111 1111xxxxxx //接下来以此类推,但是容量最大也就32bit的正数,因此最后n |= n >>> 16,最多也就32个1(但是这已经是负数了)。 //在执行tbleSizeFor之前,对initialCapacity做了判断。 //如果大于MAXIMUM_CAPACITY(2 ^ 30),则取MAXIMUM_CAPACITY。 //如果等于MAXIMUM_CAPACITY(2 ^ 30),会执行移位操作。 //所以这里面的移位操作之后,最大30个1,不会大于等于MAXIMUM_CAPACITY。 //30个1,加1之后的2 ^ 30 n |= n >>> 4; n |= n >>> 8; n |= n >>> 16; return (n < 0) ? 1 : (n >= MAXIMUM_CAPACITY) ? MAXIMUM_CAPACITY : n + 1; } |
在阅读源码到这里时,会感觉有点怪,从开始到现在一直在说,JDK1.8-HasMap最大的特点是红黑树,并且在一些基础参数中也看到,阀值达到多少时,就从链表转换为红黑树,那么究竟红黑树是什么?
红黑树是属于平衡二叉树的结构。
特点:
根节点一定是黑色。
每个节点不是黑色就是红色。
每个子节点(NIL)是黑色(即叶子节点,是指为NIL或NULL的叶子节点)。
如果有一个节点时红色,则它的子节点必然是黑色的。
从一个节点到该节点的子孙节点的所有路径上包含相同数目的黑节点(到叶子节点的路径)。
重点:左节点 < 根节点 < 右节点
HashMap中有一个静态内部类,这个内部类就是用来表示红黑树中的节点。
static final class TreeNode<K,V> extends LinkedHashMap.Entry<K,V> { TreeNode<K,V> parent; //父节点 TreeNode<K,V> left; //左节点 TreeNode<K,V> right; //右节点 TreeNode<K,V> prev; //上一个节点 boolean red; //不是红色就是黑色 TreeNode(int hash, K key, V val, Node<K,V> next) { super(hash, key, val, next); }
/** * 获取根节点 */ final TreeNode<K,V> root() { for (TreeNode<K,V> r = this, p;;) { if ((p = r.parent) == null) return r; r = p; } } |