首先经过源码,类中的field以下,java
transient Node<K,V>[] table; transient Set<Map.Entry<K,V>> entrySet; transient int size; transient int modCount; int threshold; final float loadFactor;
其中 Node
, Map.Entry
是两个比较核心的数据结构,先看下Node的定义数组
Map.Entry
Map接口中内部定义的接口, 提供了操做Map中键值对的基本方法数据结构
一个Entry
对象,表明了Map中的一个键值对,能够经过它获取key,value也能够从新设置valueapp
interface Entry<K,V> { K getKey(); V getValue(); V setValue(V value); boolean equals(Object o); int hashCode(); }
依次说明下上面的每一个方法的做用工具
K getKey()
V getValue()
V setValue(V value)
返回entry 的 hash code
, 定义以下:学习
(e.getKey()==null ? 0 : e.getKey().hashCode()) ^ (e.getValue()==null ? 0 : e.getValue().hashCode())
确保两个 Entry对象 equals返回true,则hashcode的值必然相同this
当两个entry对象表示的是同一个映射关系时,返回true.net
规则以下设计
(e1.getKey()==null ? e2.getKey()==null : e1.getKey().equals(e2.getKey())) && (e1.getValue()==null ? e2.getValue() ==null : e1.getValue().equals(e2.getValue()))
Node<K, V>
做为HashMap中对 Map.Entry的实现,具体逻辑以下code
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; } }
说明
hash
这个字段是干吗的next
元素Node<K,V>[] table;
说明按咱们的理解,map是一个kv结构,每一个Node对象表示的就是一个kv对,那么这个
table
应该就是保存全部的kv对的数据结构了为何会是一个数组? 怎么根据key来定位kv对在数组中的位置?
table数组大小,必须为2的n次方,首次使用是初始化,必要时(如添加新的kv对时)能够扩充容量
要了解这个数组的使用过程,最佳的思路就是经过三个方法来定位了
new HashMap<>()
建立对象时,数组的初始化put(k,v)
添加kv时,数组的扩容以及塞值get(k)
经过key获取value时,在数组中的定位构造方法以下,主要是设置了阀值,loadFactory
(后面说其用处)
public HashMap(int initialCapacity, float loadFactor) { // 参数合法性校验省略 this.loadFactor = loadFactor; this.threshold = tableSizeFor(initialCapacity); } /** * 找到大于等于cap的最小的2的幂. */ static final int tableSizeFor(int cap) { int n = cap - 1; 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; }
上面的构造,并无如咱们预期的初始化 table
数组,接下来看put方法,是否有设置 table
数组呢
put(k,v)
实现以下,逻辑比较复杂,会直接在代码中给出一些注释
public V put(K key, V value) { return putVal(hash(key), key, value, false, true); } /** * Implements Map.put and related methods * * @param hash (key的hash值,经过hash方法计算) * @param key the key * @param value the value to put * @param onlyIfAbsent true表示在不存在kv时,才塞入数据 * @param evict if false, the table is in creation mode. * @return 返回原来的value(若是以前不存在,返回null) */ final V putVal(int hash, K key, V value, boolean onlyIfAbsent, boolean evict) { Node<K,V>[] tab; Node<K,V> p; int n, i; // 首先是将tab局部变量指向 table数组 if ((tab = table) == null || (n = tab.length) == 0) { // 当table数组没有初始化时,进行初始化,并返回数组长度 n = (tab = resize()).length; } if ((p = tab[i = (n - 1) & hash]) == null) { // 根据数组长度和key的hash值,计算出key放入数组的位置,若该位置没有值,则直接建立一个新的Entry(即Node),放在该位置便可 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)))) { // 若根据key的hash值,从数组中获取的Entry对象,其key正好是咱们指定的key,则直接修改这个Entry的value值便可 e = p; } // 下面则表示出现hash碰撞,虽然key的hash值相同,可是这个Entry的key并非咱们指定的key else if (p instanceof TreeNode) { e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value); } else { // 迭代Entry的next节点,知道找到Entry.Key 正好是咱们指定的Key为止 for (int binCount = 0; ; ++binCount) { if ((e = p.next) == null) { // 若一直都不存在,则建立一个新的Entry对象,并塞入table p.next = newNode(hash, key, value, null); if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st treeifyBin(tab, hash); break; } if (e.hash == hash && ((k = e.key) == key || (key != null && key.equals(k)))) break; 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; }
逻辑拆解:
table
数组是否初始化,不然进行初始化hash()
方法获取)table
数组中查询Node
节点
equals()
方法)
到这里就能够解决在介绍Node类结构的两个问题
hash()
方法计算的值Node
是否为咱们查找的节点hash
值的kv键值对,由此能够看出HashMap
的存储结构
push(k,v)
包含较多的内容,上面只给出了设计逻辑,具体实现有必要扣一扣,研究下其中一些有意思的点
从上面的的代码能够看出,调用 resize()
方法进行的初始化(此外这个方法也负责数组的扩容)
源码实现比较长,这里主要关注初始化过程,以如下面这段逻辑进行实例分析
Map map = new HashMap<>(); map.put(xx, xx);
对resize方法中一些逻辑配合上面的使用方式进行简化处理, 抽出代码以下
final Node<K,V>[] resize() { Node<K,V>[] oldTab = table; // null int oldCap = 0; int oldThr = 0; int newCap, newThr = 0; // zero initial threshold signifies using defaults newCap = 16; // DEFAULT_INITIAL_CAPACITY; newThr = 12; // (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY); threshold = newThr; @SuppressWarnings({"rawtypes","unchecked"}) Node<K,V>[] newTab = (Node<K,V>[])new Node[newCap]; table = newTab; return newTab; }
上面是简化resize的内部逻辑,单独剥离出初始化 table
数组的代码块;
说明
0.75 * 数组长度
计算key的hash值,这个直接决定hash碰撞的几率
实现以下
static final int hash(Object key) { int h; return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16); }
到这里天然就会有一个疑问
如何根据hash
值与table
数组进行关联,又如何保证碰撞较小?
这个问题单独成篇,再将这个,这里先记下
HashMap 的底层数据结构是一个Node数组,配合Node链表的方式进行kv存储
数组的初始化延迟在首次向Map中添加元素时进行
默认数组长度为16,阀值为12
阀值定义为: The next size value at which to resize (capacity * load factor).
数组长度要求为2的n次方
tableSizeFor
方法实现获取正大于数字n的2的整数次幂 (这个实现比较有意思)
如何经过key获取对应的Entry对象呢?
hash()
方法计算key的hash值table
数组中的下标Node
节点
Node
的 next
节点,直到为null或者找到为止关注小灰灰blog,一块儿学习