Java源码阅读——HashMap
Java源码阅读——HashMap
定义
public class HashMap<K,V> extends AbstractMap<K,V>
implements Map<K,V>, Cloneable, Serializable
继承了AbstractMap抽象类,实现Map,Cloneable,Serializable接口。
HashMap 是一个散列表,它存储的内容是键值对(key-value)映射。
HashMap 继承于AbstractMap,实现了Map、Cloneable、java.io.Serializable接口。
HashMap 的实现不是同步的,这意味着它不是线程安全的。它的key、value都可以为null。此外,HashMap中的映射不是有序的。
HashMap 的实例有两个参数影响其性能:“初始容量”和 “加载因子”。
容量是哈希表中桶的数量,初始容量只是哈希表在创建时的容量。
加载因子是哈希表在其容量自动增加之前可以达到多满的一种尺度。当哈希表中的条目数超出了加载因子与当前容量的乘积时,则要对该哈希表进行 rehash 操作(即重建内部数据结构),从而哈希表将具有大约两倍的桶数。
通常,默认加载因子是0.75, 这是在时间和空间成本上寻求一种折衷。加载因子过高虽然减少了空间开销,但同时也增加了查询成本(在大多数 HashMap 类的操作中,包括 get 和 put 操作,都反映了这一点)。在设置初始容量时应该考虑到映射中所需的条目数及其加载因子,以便最大限度地减少 rehash 操作次数。如果初始容量大于最大条目数除以加载因子,则不会发生 rehash 操作。
HashMap与Hashtable区别
1. HashTable产生于JDK 1.1,而HashMap产生于JDK 1.2。
2. 两个类的继承体系有些不同。虽然都实现了Map、Cloneable、Serializable三个接口,但是HashMap继承自抽象类AbstractMap,而HashTable继承自抽象类Dictionary。其中Dictionary类是一个已经被废弃的类
3. HashMap是支持null键和null值的,而HashTable在遇到null时,会抛出NullPointerException异常。因为HashMap在实现时对null做了特殊处理,,将null的hashCode值定为了0,从而将其存放在哈希表的第0个bucket中。
4. HashMap/Hashtable内部用Entry数组实现哈希表,而对于映射到同一个哈希桶(数组的同一个位置)的键值对,使用Entry链表来存储(解决hash冲突)。在这一点上实现是一样的。但是在遍历方式上,两者都可以用Iterator迭代,但Hashtable仍然有Enumeration(废弃)的方式。
5. 在Hashtable中同步是通过synchronized关键字实现的,而HashMap需要使用ConcurrentHashMap包装一下。
静态常量
主要是一些默认参数
1. static final intDEFAULT_INITIAL_CAPACITY = 1 << 4; // aka 16
默认初始容量为16
2. static final intMAXIMUM_CAPACITY = 1 << 30;
最大容量为1左移30位,如果隐式指定较高的值,则使用该容量,必须是2的指数倍。
3. static final floatDEFAULT_LOAD_FACTOR = 0.75f;
默认加载因子0.75
4. static final int TREEIFY_THRESHOLD= 8;
一个桶中bin(箱子)的存储方式由链表转换成树的阈值。即当桶中bin的数量超过TREEIFY_THRESHOLD时使用树来代替链表。默认值是8
5. static final intUNTREEIFY_THRESHOLD = 6;
当执行resize操作时,当桶中bin的数量少于UNTREEIFY_THRESHOLD时使用链表来代替树。默认值是6
6. static final intMIN_TREEIFY_CAPACITY = 64;
当桶中的bin被树化时最小的hash表容量。(如果没有达到这个阈值,即hash表容量小于MIN_TREEIFY_CAPACITY,当桶中bin的数量太多时会执行resize扩容操作)这个MIN_TREEIFY_CAPACITY的值至少是TREEIFY_THRESHOLD的4倍。
成员变量
transient Node<K,V>[] table;//
transient Set<Map.Entry<K,V>>entrySet;// 保存缓存的entrySet()。请注意,AbstractMap字段用于keySet()和values()。
transient int size;//HashMap元素数量
transient int modCount;//HashMap结构改变的次数
int threshold;// 下一个调整大小的值(容量*加载因子)
final float loadFactor;//加载因子
数据结构Node
HashMap节点的数据结构,实现了Map.Entry接口,包括一个哈希值,key,value,和下一个节点next。
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); } 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; } }
TreeNode
JDK 1.8 以前 HashMap 的实现是 数组+链表,即使哈希函数取得再好,也很难达到元素百分百均匀分布。
当 HashMap 中有大量的元素都存放到同一个桶中时,这个桶下有一条长长的链表,这个时候HashMap 就相当于一个单链表,假如单链表有 n 个元素,遍历的时间复杂度就是O(n),完全失去了它的优势。
针对这种情况,JDK 1.8中引入了 红黑树(查找时间复杂度为 O(logn))来优化这个问题。
当链表长度大于8时,将链表转换为红黑树。
/**
* Entry for Tree bins. ExtendsLinkedHashMap.Entry (which in turn
* extends Node) so can be used asextension of either regular or
* linked node.
*/
static final class TreeNode<K,V> extends LinkedHashMap.Entry<K,V> {
TreeNode<K,V> parent; // red-blacktree links
TreeNode<K,V> left;
TreeNode<K,V> right;
TreeNode<K,V> prev; // needed tounlink next upon deletion
boolean red;
HashMap中关于红黑树的三个关键参数:
/** * The bin count threshold for using a tree rather than list for a * bin. Bins are converted to trees when adding an element to a * bin with at least this many nodes. The value must be greater * than 2 and should be at least 8 to mesh with assumptions in * tree removal about conversion back to plain bins upon * shrinkage. * 一个桶的树化阈值 * 当桶中元素个数超过这个值时,需要使用红黑树节点替换链表节点 * 这个值必须为 8,要不然频繁转换降低效率 */ static final int TREEIFY_THRESHOLD = 8; /** * The bin count threshold for untreeifying a (split) bin during a * resize operation. Should be less than TREEIFY_THRESHOLD, and at * most 6 to mesh with shrinkage detection under removal. * 一个树的链表还原阈值 * 当扩容时,桶中元素个数小于这个值,就会把树形的桶元素 还原(切分)为链表结构 * 这个值应该比上面那个小,至少为 6,避免频繁转换 */ static final int UNTREEIFY_THRESHOLD = 6; /** * The smallest table capacity for which bins may be treeified. * (Otherwise the table is resized if too many nodes in a bin.) * Should be at least 4 * TREEIFY_THRESHOLD to avoid conflicts * between resizing and treeification thresholds. * 哈希表的最小树形化容量 * 当哈希表中的容量大于这个值时,表中的桶才能进行树形化 * 否则桶内元素太多时会扩容,而不是树形化 * 为了避免进行扩容、树形化选择的冲突,这个值不能小于 4 * TREEIFY_THRESHOLD */ static final int MIN_TREEIFY_CAPACITY = 64;
JDK 1.8 以后哈希表的添加、删除、查找、扩容方法都增加了一种 节点为 TreeNode 的情况:
添加时,当桶中链表个数超过8 时会转换成红黑树;
删除、扩容时,如果桶中结构为红黑树,并且树中元素个数太少的话,会进行修剪或者直接还原成链表结构;
查找时即使哈希函数不优,大量元素集中在一个桶中,由于有红黑树结构,性能也不会差。
参考https://tech.meituan.com/java-hashmap.html
构造方法
构造方法有四个。
HashMap()
构造一个具有默认初始容量(16) 和默认加载因子 (0.75)的空 HashMap。
public HashMap() { this.loadFactor = DEFAULT_LOAD_FACTOR; // all other fields defaulted }
HashMap(int initialCapacity)
构造一个带指定初始容量和默认加载因子 (0.75) 的空 HashMap。
public HashMap(int initialCapacity) { this(initialCapacity, DEFAULT_LOAD_FACTOR); }
调用另一个构造函数HashMap(intinitialCapacity, float loadFactor),使用默认加载因子。
HashMap(int initialCapacity, float loadFactor)
构造一个带指定初始容量和加载因子的空 HashMap。
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); }
判断初始容量合法性,小于0抛出IllegalArgumentException异常,太大直接使用MAXIMUM_CAPACITY。判断加载因子合法性,小于0或者为NaN(Not a Number)抛出IllegalArgumentException异常。threshold(下一次调整大小的值,为2的幂次方倍数),tableSizeFor(initialCapacity)该方法则是计算当前initialCapacity下次的大小,比如initialCapacity=17,则返回32。
HashMap(Map<? extends K,? extends V> m)
构造一个映射关系与指定Map 相同的新 HashMap。
public HashMap(Map<? extends K, ? extends V> m) { this.loadFactor = DEFAULT_LOAD_FACTOR; putMapEntries(m, false); }
使用putMapEntries方法填充。
public方法
void clear()
此映射中移除所有映射关系。
public void clear() { Node<K,V>[] tab; modCount++; if ((tab = table) != null && size > 0) { size = 0; for (int i = 0; i < tab.length; ++i) tab[i] = null; } }
增加修改结构次数,modCount++。将表中数据table循环置为null,size为0;
Object clone()
返回此 HashMap 实例的浅表副本:并不复制键和值本身。
public Object clone() { HashMap<K,V> result; try { result = (HashMap<K,V>)super.clone(); } catch (CloneNotSupportedException e) { // this shouldn't happen, since we are Cloneable throw new InternalError(e); } result.reinitialize(); result.putMapEntries(this, false); return result; }
浅拷贝,先置空,然后进行赋值操作。putMapEntries方法将会把当前(this)map对象的k-v元素赋值到被clone的对象中,遇到hash值相同时,链接到相同hash值队列的后面。
boolean containsKey(Object key)
如果此映射包含对于指定键的映射关系,则返回 true。
public boolean containsKey(Object key) { return getNode(hash(key), key) != null; }
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; }
查找是否存在该key,返回getNode(key)是否为null。在getNode(int hash, Object key)中,找key的hash值相同的链表下,是否存在key。
boolean containsValue(Object value)
如果此映射将一个或多个键映射到指定值,则返回 true。
public boolean containsValue(Object value) { Node<K,V>[] tab; V v; if ((tab = table) != null && size > 0) { for (int i = 0; i < tab.length; ++i) { for (Node<K,V> e = tab[i]; e != null; e = e.next) { if ((v = e.value) == value || (value != null && value.equals(v))) return true; } } } return false; }
这个方法很好的说明了HashMap中存储的结构。遍历整个表,表中每个元素(具有相同hash值的一条链)再遍历链中元素(e=e.next)。
Set<Map.Entry<K,V>> entrySet()
返回此映射所包含的映射关系的Set 视图。
public Set<Map.Entry<K,V>> entrySet() { Set<Map.Entry<K,V>> es; return (es = entrySet) == null ? (entrySet = new EntrySet()) : es; }
V get(Object key)
返回指定键所映射的值;如果对于该键来说,此映射不包含任何映射关系,则返回 null。
public V get(Object key) { Node<K,V> e; return (e = getNode(hash(key), key)) == null ? null : e.value; }
和上面提到的类似,通过key找value,(遍历相同hash值的情况)。
boolean isEmpty()
如果此映射不包含键-值映射关系,则返回 true。
public boolean isEmpty() { return size == 0; }
Set<K> keySet()
返回此映射中所包含的键的Set 视图。
public Set<K> keySet() { Set<K> ks = keySet; if (ks == null) { ks = new KeySet(); keySet = ks; } return ks; }
KeySet为AbstractMap中定义,通过iterator遍历获得KeySet。
V put(K key, V value)
在此映射中关联指定值与指定键。
public V put(K key, V value) { return putVal(hash(key), key, value, false, true); }
调用putVal方法,参数分别为key的hash值,key,value,(是否)改变存在的值,(是否)不是新建的map。该方法实现比较复杂,但是只要记住HashMap实现的方式,就很好理解。
void putAll(Map<? extends K,? extends V> m)
将指定映射的所有映射关系复制到此映射中,这些映射关系将替换此映射目前针对指定映射中所有键的所有映射关系。
public void putAll(Map<? extends K, ? extends V> m) { putMapEntries(m, true); }
在putMapEntries()中,遍历m,调用putVal()方法。
V remove(Object key)
此映射中移除指定键的映射关系(如果存在)。
public V remove(Object key) { Node<K,V> e; return (e = removeNode(hash(key), key, null, false, true)) == null ? null : e.value; }
调用removeNode()方法,参数和putVal()方法类似,后面两个参数分别为(如果为true,只移除和value相等的元素)和(是否移动其他元素),当然这些参数我们外部调用时并不需要关心。
int size()
返回此映射中的键-值映射关系数。
public int size() { return size; }
Collection<V> values()
返回此映射所包含的值的Collection 视图。
public Collection<V> values() { Collection<V> vs = values; if (vs == null) { vs = new Values(); values = vs; } return vs; }
values和keyset一样是在AbstractMap中定义,通过iterator遍历获得所有value的值。
总结
HashMap实现比list稍微复杂一些,但是理解上并不难,只要理解HashMap是如何存储的就能知道操作是怎么实现的。HashMap中方法没有synchronized关键字,所以不是线程安全的(在Hashtable中,方法都有synchronized修饰),如果需要进行线程安全控制,可以使用java.util.concurrent包里的ConcurrentHashMap,后续会继续阅读该包下的一些源码。