HashMap
允许键和值为null,存储无序,在非同步和键值非null情况下相当于HashTable。HashMap线程不安全,如果多个线程同时访问哈希映射,并且至少有一个线程在结构上修改了该映射,则必须在外部对其进行同步。(结构修改是添加或删除一个或多个映射的任何操作;仅更改与实例已有的键相关联的值不是结构修改)。同步例如:Map m = Collections.synchronizedMap(new HashMap(...));
常用方法
HashMap<String,String> map = new HashMap<>(); map.put("1","a"); // 放置指定键值到map中,已存在则覆盖
map.put("1","A"); // 若键已存在则覆盖并返回旧值(返回a) map.put("2","B"); map.put("3","C"); map.put(null,null); // 键值都可以为null,始终会被存到数组的0索引处 map.get("1"); // 获取指定键对应的值 map.keySet(); // 返回键的集合 map.values(); // 返回值的集合 map.isEmpty(); // 判断是否为空 map.size(); // 返回键值对个数 map.containsKey("1"); // 判断是否存在指定键 map.containsValue("A");// 判断是都存在指定值 HashMap<String,String> ano_map = new HashMap<>(); ano_map.put("4","D"); ano_map.put("5","E"); map.putAll(ano_map); map.remove("5"); // 移除指定键的键值对 map.replace("1","a"); // 替换指定键的值 map.replace("2","B","b"); // 迭代方式一 for (String s : map.keySet()) { // 遍历key System.out.println(s); } for (String value : map.values()) { // 遍历value System.out.println(value); } // 迭代方式二 Set<Map.Entry<String, String>> entries = map.entrySet(); Iterator<Map.Entry<String, String>> iterator = entries.iterator(); while(iterator.hasNext()){ Map.Entry<String, String> next = iterator.next(); System.out.println(next.getKey()+"---"+next.getValue()); } // 迭代方式三 Set<String> keys = map.keySet(); for (String key : keys) { Object value = map.get(key); System.out.println(key+"---"+value); } // 迭代方式四 map.forEach((key,value)->System.out.println(key+"---"+value));
JDK1.8
底层采用数组+链表+红黑树的方式存储。
红黑树
性质:
1.每个结点只能是红色或者黑色。
2.根结点一定是黑色。
3.红色结点不能连在一起。
4.每个红色结点的两个子结点必须都是黑色。
5.叶子结点都是黑色(NULL结点,一般不画)
红黑树本质上是满足以上性质的自平衡二叉查找树(不会出现退化成单支的情况)
红黑树的自平衡
1.左旋:
2.右旋:
结点的插入
插入的结点默认为红色
1.插入6
2.变色:
当前结点(6)的父亲结点(7)是红色,且叔叔结点(13)也是红色:
1.把父亲和叔叔结点都变为黑色(7、13)
2.把爷爷结点变为红色(12)
3.此时爷爷结点称为考察对象(12)
3.左旋:
当前结点(12)的父结点是红色(5),叔叔结点(30)是黑色,且当前结点是右子树,以父结点左旋。得到如下:
4.右旋:
当前结点(5)的父亲结点(12)是红色,叔叔(30)是黑色,且当前结点是左子树,
把父亲结点变成黑色,把爷爷结点变成红色,以爷爷结点右旋。
JDK1.8源码阅读
继承了AbstractMap<K,V>,实现类Map<K,V>, Cloneable, Serializable接口。
AbstractMap<K,V>已经实现了Map<K,V>接口,这里HashMap继承了AbstractMap<K,V>,却又实现了Map<K,V>,这里是版本迭代造成的问题,是作者的失误。
字段
static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; // 默认初始容量(必须是2的次幂) // 1左移4位,即16 static final int MAXIMUM_CAPACITY = 1 << 30; // 最大容量2的30次方 static final float DEFAULT_LOAD_FACTOR = 0.75f; // 默认负载因子 static final int TREEIFY_THRESHOLD = 8; // 树化阈值为8(超过8用红黑树存储) static final int UNTREEIFY_THRESHOLD = 6; // 解除树化阈值6(小于6恢复链表存储) static final int MIN_TREEIFY_CAPACITY = 64; // 最小树化容量64 // Node数组长度总是2的幂。哈希桶数组在首次使用(put方法)时初始化,并根据需要 // 调整大小。实际上在putVal方法中调用resize方法进行数组初始化 transient Node<K,V>[] table; // transient 表示序列化时该变量不参与 transient Set<Map.Entry<K,V>> entrySet; // 用于转化为Set存储 transient int size; // 键值映射数量 transient int modCount; // 结构性修改的次数 int threshold; // 阈值 final float loadFactor; // 负载因子
构造器
// 指定初始容量和负载因子 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)) // 负载因子小于0或是不个数 throw new IllegalArgumentException("Illegal load factor: " + loadFactor); // 否则,赋值操作 this.loadFactor = loadFactor; // threshold初始化为大于等于initialCapacity最小的2的n次幂 // 真正的值会在putVal中调用resize方法进行修改 this.threshold = tableSizeFor(initialCapacity); } // 建议指定容量(阿里手册建议:需要存储的元素个数/负载因子+1) public HashMap(int initialCapacity) { // 指定初始容量,负载因子用默认0.75 this(initialCapacity, DEFAULT_LOAD_FACTOR); } public HashMap() { // 默认初始容量为16,负载因子为0.75 this.loadFactor = DEFAULT_LOAD_FACTOR; } public HashMap(Map<? extends K, ? extends V> m) { // 指定Map实现类对象初始化 this.loadFactor = DEFAULT_LOAD_FACTOR; putMapEntries(m, false); }
结点类(内部类)
// 内部类Node实现了Map接口的内部接口Entry 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() { // 实现Map接口的内部接口Entry的hashCode()方法 return Objects.hashCode(key) ^ Objects.hashCode(value); } public final V setValue(V newValue) { // 设置结点新值,返回旧值 V oldValue = value; value = newValue; return oldValue; } 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; } }
重点方法
// 将指定Map实现类的对象存入 final void putMapEntries(Map<? extends K, ? extends V> m, boolean evict) { int s = m.size(); if (s > 0) { if (table == null) { // 加1后强转为int相当于向上取整,创建时就保证更大容量,防止刚创建完,还没put几个元素就要立马要扩容 // 若不加1,若size是6,则ft是8,创建的数组正好是满的,下次 // put就立马要进行扩容,降低效率 float ft = ((float)s / loadFactor) + 1.0F; int t = ((ft < (float)MAXIMUM_CAPACITY) ? (int)ft : MAXIMUM_CAPACITY); if (t > threshold) // threshold初始化为大于等于initialCapacity最小的2的n次幂 // 真正的值会在putVal中调用resize方法进行修改 threshold = tableSizeFor(t); } else if (s > threshold) resize(); // 依次存入键值对 for (Map.Entry<? extends K, ? extends V> e : m.entrySet()) { K key = e.getKey(); V value = e.getValue(); putVal(hash(key), key, value, false, evict); } } }
// 计算key的哈希值 static final int hash(Object key) { int h; return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16); // key为空则hash值为0 // h与h无符号右移16位进行异或运算(将高16位也利用上使hash值更加随机) }
hashCode()方法来自Object类,是native方法(本地方法)。
(h = key.hashCode()) ^ (h >>> 16)的作用
(1)进行无符号右移16位后再异或运算
注:这里的(n-1) & hash是putVal方法中代码,用于计算对应的数组索引。
实际上n在绝大多数情况都是小于2的16次方的,而n还必须是2的次方,则(n-1)低位连续都是1,所以(n-1) & hash也就是使hash的对应低位有效,其他高位清零。将key的hashCode方法计算的值的高16位也参与运算,可以使计算的索引更加随机,减少在hashCode方法计算的值低位不变,仅高位不同的时候造成的哈希冲突。
(2)不进行无符号右移16位后再异或运算
如果经hashCode方法计算的值低位不变,仅仅高位不同。
这里两次hashCode计算的值仅低位不变,两次(n-1) & hash的结算就是一样的,造成了哈希冲突。
// 返回一个大于等于cap且是2的n次幂的table长度 // cap为127返回128(2的7次方),cap为129返回256(2的8次方) static final int tableSizeFor(int cap) { int n = cap - 1; // 不减一,若cap已经是2的次幂,返回的是cap的2倍! n |= n >>> 1; n |= n >>> 2; n |= n >>> 4; n |= n >>> 8; n |= n >>> 16; return (n < 0) ? 1 : (n >= MAXIMUM_CAPACITY) ? MAXIMUM_CAPACITY : n + 1; }
public V get(Object key) { // 根据键获取对应值 Node<K,V> e; // 获取对应结点 // 根据key的hash值和key获取对应的结点 return (e = getNode(hash(key), key)) == null ? null : e.value; } // 键已存在则覆盖 public V put(K key, V value) { return putVal(hash(key), key, value, false, true); }
// onlyIfAbsent为true代表不更改现有值,evict为false代表table为创建状态 // 插入数据成功之后扩容 final V putVal(int hash, K key, V value, boolean onlyIfAbsent, boolean evict) { Node<K,V>[] tab; Node<K,V> p; int n, i; if ((tab = table) == null || (n = tab.length) == 0) // 哈希桶数组为空 n = (tab = resize()).length; // 计算元素对应的数组索引,并使p指向该位置 if ((p = tab[i = (n - 1) & hash]) == null) // 对应索引处为空 // 利用newNode方法创建新结点存入对应的索引处 tab[i] = newNode(hash, key, value, null); else { // 对应索引处为非空,即冲突 Node<K,V> e; K k; if (p.hash == hash && // 与链表首个元素的键相同 ((k = p.key) == key || (key != null && key.equals(k)))) e = p; // e指向该位置 else if (p instanceof TreeNode) // 冲突位置已经用红黑树进行存储 e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value); else { // 冲突位置仍用链表进行存储 for (int binCount = 0; ; ++binCount) { // 遍历链表 if ((e = p.next) == null) { // 当前链表只有一个元素,e指向null p.next = newNode(hash, key, value, null); // 插入尾部 if (binCount >= TREEIFY_THRESHOLD - 1) // 结点数大于8 treeifyBin(tab, hash); // 转化为红黑树 break; // 跳出链表遍历(因为已经转为红黑树了) } if (e.hash == hash && ((k = e.key) == key || (key != null && key.equals(k)))) // 遍历过程中发现相同键 break; p = e; // p指向下一个结点 } } if (e != null) { // e不为null V oldValue = e.value; // onlyIfAbsent 为true则不更改现有值 // 指定更改现有值或者旧值是null if (!onlyIfAbsent || oldValue == null) e.value = value; afterNodeAccess(e); return oldValue; // 返回旧值 } } ++modCount; if (++size > threshold) // 超出阈值 resize(); // 扩容 afterNodeInsertion(evict); return null; }
// 将链表转化为红黑树 final void treeifyBin(Node<K,V>[] tab, int hash) { int n, index; Node<K,V> e; // 数组为空或数组长度小于64 if (tab == null || (n = tab.length) < MIN_TREEIFY_CAPACITY) resize(); // 不转化为红黑树,而是扩容 // 根据hash值和数组长度计算对应的数组索引,e指向第一个元素 else if ((e = tab[index = (n - 1) & hash]) != null) { // 对应桶中元素不为null TreeNode<K,V> hd = null, tl = null; // 红黑树的头结点和尾结点 do { TreeNode<K,V> p = replacementTreeNode(e, null); // 转为树结点 if (tl == null) hd = p; else { p.prev = tl; tl.next = p; } tl = p; } while ((e = e.next) != null); if ((tab[index] = hd) != null) hd.treeify(tab); // 自平衡 } }
链表长度到了8,但数组长度没有到64,选择扩容解决。
JDK1.8之前扩容时会重新进行哈希值计算,会遍历所有元素,很耗时。
JDK1.8开始,不重新进行hash值计算,每次扩容都是翻倍,与原来计算的(n-1) & hash的结果相比,只是多了一个bit位,所以扩容后结点的位置:
(1)在原来位置;
(2)在“原来位置+旧容量”的位置。
翻倍的意义在于使对应的n-1的二进制看起来就像向左推进了一个“1”。
因此不必再重新进行hash值的计算,因为是&运算,只需看原来的hash值新增的那一位是1还是0,是0的话索引不变,是1的话变为原索引+旧容量。这样的话,扩容之后,一个桶中的元素会随机的分散到原索引或原索引+旧容量,扩容之后不会出现更严重的哈希冲突的情况。
// 扩容(2倍)或初始化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; // 返回旧数组 } // 新容量=旧容量的2倍,新容量小于最大值,并且旧容量大于等于16 else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY && oldCap >= DEFAULT_INITIAL_CAPACITY) newThr = oldThr << 1; // 新阈值=旧阈值2倍 } else if (oldThr > 0) // 创建时的阈值就是容量值 newCap = oldThr; else { // oldThr =0,即构造时没有指定容量,则容量、阈值使用默认值 newCap = DEFAULT_INITIAL_CAPACITY; newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY); } if (newThr == 0) { // 数组为空,阈值为0,这时真正计算阈值 float ft = (float)newCap * loadFactor; newThr = (newCap < MAXIMUM_CAPACITY && ft < (float)MAXIMUM_CAPACITY ? (int)ft : Integer.MAX_VALUE); } threshold = newThr; // 新数组阈值 Node<K,V>[] newTab = (Node<K,V>[])new Node[newCap]; // 创建新数组 table = newTab; // table数组初始化或扩容后的数组 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 { // 不为空不是红黑树,则是链表(保持顺序) Node<K,V> loHead = null, loTail = null; Node<K,V> hiHead = null, hiTail = null; Node<K,V> next; do { next = e.next; // oldCap不减1,相当于检测原hash的新增为是0还是1 if ((e.hash & oldCap) == 0) { // 新增位为0,即索引不变 if (loTail == null) loHead = e; else loTail.next = e; loTail = e; } else { // 新增位为1,新索引=原索引+旧容量 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; }
JDK1.7
字段
static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; static final int MAXIMUM_CAPACITY = 1 << 30; static final float DEFAULT_LOAD_FACTOR = 0.75f; static final Entry<?,?>[] EMPTY_TABLE = {}; // Entry数组 transient Entry<K,V>[] table = (Entry<K,V>[]) EMPTY_TABLE; // Entry桶数组 transient int size; transient int size; final float loadFactor; transient int modCount; static final int ALTERNATIVE_HASHING_THRESHOLD_DEFAULT = Integer.MAX_VALUE; transient int hashSeed = 0;
构造器
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; // 初始时,阈值都是容量值,在put方法调用inflateTable方法中重新计算 threshold = initialCapacity; init(); } public HashMap(int initialCapacity) { this(initialCapacity, DEFAULT_LOAD_FACTOR); } public HashMap() { this(DEFAULT_INITIAL_CAPACITY, DEFAULT_LOAD_FACTOR); } public HashMap(Map<? extends K, ? extends V> m) { this(Math.max((int) (m.size() / DEFAULT_LOAD_FACTOR) + 1, DEFAULT_INITIAL_CAPACITY), DEFAULT_LOAD_FACTOR); inflateTable(threshold); // 初始化table数组,并重新计算阈值 putAllForCreate(m); }
重要方法
// 添加元素,冲突时添加到链表采用的是头插法,易产生循环链表。 // 先扩容再插入(在addEntry方法中) public V put(K key, V value) { if (table == EMPTY_TABLE) { // 数组是空的 inflateTable(threshold); // 转化为大于等于threshold的最小2次方 } if (key == null) // 键是null return putForNullKey(value); int hash = hash(key); int i = indexFor(hash, table.length); // 根据hash值和表长计算出数组索引 for (Entry<K,V> e = table[i]; e != null; e = e.next) { // 遍历对应索引处的链表 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++; addEntry(hash, key, value, i); // 键不存在,添加新的Entry return null; }
private void inflateTable(int toSize) { // 转化为大于等于toSize的最小2次方 int capacity = roundUpToPowerOf2(toSize); threshold = (int) Math.min(capacity * loadFactor, MAXIMUM_CAPACITY + 1); table = new Entry[capacity]; // 创建table数组 initHashSeedAsNeeded(capacity); }
// 转化为大于等于toSize的最小2次方 private static int roundUpToPowerOf2(int number) { return number >= MAXIMUM_CAPACITY ? MAXIMUM_CAPACITY : (number > 1) ? Integer.highestOneBit((number - 1) << 1) : 1; }
public static int highestOneBit(int i) { i |= (i >> 1); i |= (i >> 2); i |= (i >> 4); i |= (i >> 8); i |= (i >> 16); return i - (i >>> 1); }
// 计算hash值 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); }
void addEntry(int hash, K key, V value, int bucketIndex) { // 装入指定桶 // 元素个数大于阈值(表长*0.75)并且对应桶不为空 if ((size >= threshold) && (null != table[bucketIndex])) { resize(2 * table.length); // 先扩容成原来的两倍 hash = (null != key) ? hash(key) : 0; bucketIndex = indexFor(hash, table.length); } createEntry(hash, key, value, bucketIndex); // 再插入 }
void resize(int newCapacity) { // 扩容(需传入指定容量) Entry[] oldTable = table; // 保存旧数组 int oldCapacity = oldTable.length; // 保存旧数组长度 if (oldCapacity == MAXIMUM_CAPACITY) { threshold = Integer.MAX_VALUE; return; } Entry[] newTable = new Entry[newCapacity]; // 创建新数组 transfer(newTable, initHashSeedAsNeeded(newCapacity)); // 从旧转移到新的 table = newTable; // table指向新的数组 // 修改阈值 threshold = (int)Math.min(newCapacity * loadFactor, MAXIMUM_CAPACITY + 1); }
// 从旧数组转移至新数组 void transfer(Entry[] newTable, boolean rehash) { int newCapacity = newTable.length; // 新容量 for (Entry<K,V> e : table) { // 从旧数组中取Entry while(null != e) { // 不为空 Entry<K,V> next = e.next; if (rehash) { e.hash = null == e.key ? 0 : hash(e.key); } int i = indexFor(e.hash, newCapacity); // 计算在新数组中的索引 e.next = newTable[i]; newTable[i] = e; e = next; } } }
图示:
可见,因为是头插法,扩容后链表中的元素顺序变成了原来的逆序。多线程环境下易产生循环链表。
常见疑问
为什么初始容量必须是2的次方?
构造方法中若指定初始容量,且不是2的次方,会在put方法中将其转化为大于等于它的最小2次方。
将key的hash值和length取余即可使元素对应的数组索引落在0~length-1范围内,可是JDK中并没有使用取余运算符,若使用取余运算符,在扩容时元素移动需要大量的取余计算效率较低,而与运算符效率高于取余运算符,因此jdk中采用了与运算符。
索引计算
JDK1.7中:
static int indexFor(int h, int length) { // 根据hash值和表长度计算对应的数组索引
return h & (length-1);
}
JDK1.8中:
tab[i = (n - 1) & hash] // putVal方法中代码
为了使与运算符达到取余运算符一样的效果,初始容量必须是2的次方。比如,当为初始容量为16时,length-1= 0000 0000 0000 0000 0000 0000 0000 1111,则任何一个值和(length-1)的与运算结果必然在0~15范围内。
初始容量是2的次方,hash%length等于hash&(length-1)
初始容量若不是2的次方,则hash%length 就不等于hash&(length-1)了
为什么默认负载因子是0.75?
默认负载因子(0.75)在时间和空间成本之间提供了一个很好的折衷方案。
As a general rule, the default load factor (.75) offers a good tradeoff between time and space costs.
为什么JDK1.8中转化为红黑树的阈值是8?
符合泊松分布,当一个桶中元素达到8个时,再有新元素添加进来的几率接近于0,也就是产生红黑树的几率并不大,红黑树的自平衡消耗比较大。而且TreeNode占用空间是Node的两倍。所以在小于等于8的时候,使用链表存储效率高(查询的时间复杂度是O(n)),大于8用红黑树效率高(查询的时间复杂度是O(logn))。
源码注释:
/* * Because TreeNodes are about twice the size of regular nodes, we * use them only when bins contain enough nodes to warrant use * (see TREEIFY_THRESHOLD). And when they become too small (due to * removal or resizing) they are converted back to plain bins. In * usages with well-distributed user hashCodes, tree bins are * rarely used. Ideally, under random hashCodes, the frequency of * nodes in bins follows a Poisson distribution * (http://en.wikipedia.org/wiki/Poisson_distribution) with a * parameter of about 0.5 on average for the default resizing * threshold of 0.75, although with a large variance because of * resizing granularity. Ignoring variance, the expected * occurrences of list size k are (exp(-0.5) * pow(0.5, k) / * factorial(k)). The first values are: * * 0: 0.60653066 * 1: 0.30326533 * 2: 0.07581633 * 3: 0.01263606 * 4: 0.00157952 * 5: 0.00015795 * 6: 0.00001316 * 7: 0.00000094 * 8: 0.00000006 * more: less than 1 in ten million */