类声明html
public class HashMap<K,V> extends AbstractMap<K,V> implements Map<K,V>, Cloneable, Serializable
AbstractMap
抽象类。Map的一些操做这里面已经提供了默认实现,后面具体的子类若是没有特殊行为,可直接使用AbstractMap
提供的实现。Map
,Clone
,Serializable
接口。支持拷贝和序列化。支持Map常见的增删查改。HashMap
是数组和链表的折中,既保证了几乎$O(1)$的时间复杂度,也保证了插入和删除的时间复杂度为$O(1)$。在HashMap
内部,采用了数组+链表的形式来组织键值对Entry <Key,Value>
。java
HashMap
内部维护了一个Entry[] table
数组,当咱们使用 new HashMap()建立一个HashMap时,Entry[] table
的默认长度为16。Entry[] table
的长度又被称为这个HashMap
的容量(capacity
);node
对于Entry[] table
的每个元素而言,或为null
,或为由若干个Entry<Key,Value>
组成的链表。HashMap中Entry<Key,Value>
的数目被称为HashMap的大小(size
);算法
Entry[] table
中的某一个元素及其对应的Entry<Key,Value>
又被称为桶(bucket
);数组
HashMap的容量(即Entry[] table
的大小)*加载因子(经验值0.75)就是threshhold
,当hashmap的size大于threshhold时,容量翻倍。安全
static final int hash(Object key) { int h; return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16); }
为何须要将key的hashcode的高16为与第16为异或? 充分利用key的高位和低位(否则在利用hash求index的时候可能永远也利用不上key的高位,主要是table的长度n的二进制高位都是0,在求 (n-1)&hash
是利用不上key的hash的高位的),以最小的代价来下降冲突的可能性。 原话: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.数据结构
Key
的hashCode
,能够直接定位到存储这个Entry<Key,Value>
的桶所在的位置,这个时间的复杂度为O(1);Entry<Key,Value>
对象节点,须要遍历这个桶的Entry<Key,Value>
链表,时间复杂度为O(n);或者遍历红黑树,时间复杂度为O(logn); 那么,如今,咱们应该尽量地将第2个问题的时间复杂度O(n)降到最低,咱们应该要求**桶中的链表的长度越短越好!**桶中链表的长度越短,所消耗的查找时间就越低,最好就是一个桶中就一个Entry<Key,Value>
对象节点就行了!这样一来,桶中的Entry<Key,Value>
对象节点要求尽量第少,这就要求,HashMap中的桶的数量要多了。多线程
HashMap的桶数目,即Entry[]table
数组的长度,因为数组是内存中连续的存储单元,它的空间代价是很大的,可是它的随机存取的速度是Java集合中最快的。咱们增大桶的数量,而减小Entry<Key,Value>
链表的长度,来提升从HashMap
中读取数据的速度。这是典型的拿空间换时间的策略。app
可是咱们不能刚开始就给HashMap分配过多的桶(即Entry[] table
数组起始不能太大),这是由于数组是连续的内存空间,它的建立代价很大,何况咱们不能肯定给HashMap分配这么大的空间,它实际到底可以用多少,为了解决这一个问题,HashMap采用了根据实际的状况,动态地分配桶的数量。函数
动态分配桶的数量,HashMap动态分配桶的数量的策略: 若是 HashMap的大小 > HashMap的容量(即Entry[] table
的大小)*加载因子(经验值0.75) 则 HashMap中的Entry[]table
的容量扩充为当前的一倍;而后从新将之前桶中的Entry<Key,Value>
链表从新分配到各个桶中。
容量翻倍,怎么从新分配解决hash冲突?:容量翻倍后,从新计算每一个Entry<Key,Value>
的index,将有限的元素映射到更大的数组中,减小hash冲突的几率。
你了解从新调整HashMap大小存在什么问题吗?:多线程的状况下,可能产生条件竞争(race condition)(虽然通常咱们不使用HashMap在多线程环境中)。若是在多线程环境中使用HashMap,若是两个线程都发现HashMap须要从新调整大小了,它们会同时试着调整大小。在调整大小的过程当中,存储在链表中的元素的次序会反过来,由于移动到新的bucket位置的时候,HashMap并不会将元素放在链表的尾部,而是放在头部,这是为了不尾部遍历(tail traversing)。若是条件竞争发生了,那么就死循环了。
//默认的初始容量,必须是2的幂。 static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; // aka 16 //最大容量(必须是2的幂且小于2的30次方,传入容量过大将被这个值替换) static final int MAXIMUM_CAPACITY = 1 << 30; //默认装载因子,这个后面会作解释 static final float DEFAULT_LOAD_FACTOR = 0.75f; //JDK1.8特有 //当hash值相同的记录超过TREEIFY_THRESHOLD,会动态的使用一个专门的treemap实现来代替链表结构,使得查找时间复杂度从O(n)变为O(logn) static final int TREEIFY_THRESHOLD = 8; //JDK1.8特有 //也是阈值同上一个相反,当桶(bucket)上的链表数小于UNTREEIFY_THRESHOLD 时树转链表 static final int UNTREEIFY_THRESHOLD = 6; //JDK1.8特有 //树的最小的容量,至少是 4 x TREEIFY_THRESHOLD = 32 而后为了不(resizing 和 treeification thresholds) 设置成64 static final int MIN_TREEIFY_CAPACITY = 64; //存储数据的Entry数组,长度是2的幂。看到数组的内容了,接着看数组中存的内容就明白为何博文开头先复习数据结构了 transient Node<K,V>[] table; transient Set<Map.Entry<K,V>> entrySet; //map中保存的键值对的数量 transient int size; //Map结构被改变的次数 transient int modCount; //须要调整大小的极限值(容量*装载因子)。保存的是下次entrySet大小的极限值。 int threshold; //装载因子,当Map结构中的bucket数等于capacity*loadFactor时,bucket数量翻倍。 final float loadFactor;
public HashMap() { this.loadFactor = DEFAULT_LOAD_FACTOR; // all other fields defaulted } public HashMap(int initialCapacity) { this(initialCapacity, DEFAULT_LOAD_FACTOR); } 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); } public HashMap(Map<? extends K, ? extends V> m) { this.loadFactor = DEFAULT_LOAD_FACTOR; putMapEntries(m, false); }
有四个构造器,除HashMap(int initialCapacity, float loadFactor)
都是使用默认的加载因子构造。 HashMap(int initialCapacity, float loadFactor)
中,加载因子是用户设置的,而且根据用户设置的加载因子和容量肯定threshold。 肯定threshold的方法是tableSizeFor
,保证threshhold
是2的幂次方(大于或等于initialCapacity的最小的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; }
先将cap-1保证最后的结果是大雨或等于cap的最小的2的幂次方,例如输入的原本就是一个2的幂次方的数,好比4,若是不先-1,则会输出8,-1就会输出4。 为何每次移动位数的分别是1,2,4,8,16位?先移动一位,并作或运算,将最高位上的二进制1
移动到次高位;再右移两位,将最高位和次高位上的二进制11
移动到与次高位相邻的两位上,以此类推,最后保证最改成和比最高位的全部二进制位所有是1,在返回时,+1,就保证这个书是2的幂次方。 为何没有移动32位?正整数的最大2的幂次方是$2^16$
次方。
tableSizeFor
是一个求大于或等于给定数的最小2的幂次方的最快方法。实用的算法!
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; } }
继承自Map.Entry
,主要功能:节点的初始化,set方法,重写hashCode和equals方法。是全部操做的基础
put
public V put(K key, V value) { //传入key的hash值 return putVal(hash(key), key, value, false, true); } /** * hash key的hash值 * key 键 * value 值 * 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; // tab为空则建立table if ((tab = table) == null || (n = tab.length) == 0) n = (tab = resize()).length; // 计算index,当index所在bucket没有数据null,则直接将index位置设置为传入的key-value。 if ((p = tab[i = (n - 1) & hash]) == null) tab[i] = newNode(hash, key, value, null); else { Node<K,V> e; K k; // 节点存在,而且key值相等,直接覆盖 if (p.hash == hash && ((k = p.key) == key || (key != null && key.equals(k)))) e = p; //节点中的数据为TreeNode的实例,则是使用红黑树优化的结构 else if (p instanceof TreeNode) e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value); //节点中的数据不是TreeNode的实例,是普通的单链表结构 else { for (int binCount = 0; ; ++binCount) { //不断遍历,没有找到相同的key,则直接加到链表或的后一个节点 if ((e = p.next) == null) { p.next = newNode(hash, key, value, null); if (binCount >= TREEIFY_THRESHOLD - 1) //-1 for 1st 超过TREEIFY_THRESHOLD,则将链表变为树结构,提升冲突链效率 treeifyBin(tab, hash); break; } //若是找到key,后面直接覆盖 if (e.hash == hash && ((k = e.key) == key || (key != null && key.equals(k)))) break; p = e; } } if (e != null) { // 找到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; }
put函数大体的思路为:
resize
。resize
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; } //探测:容量翻倍后仍是小于MAXIMUM_CAPACITY,而且原来的容量大于等于默认容量。则threshold翻倍,容量翻倍 else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY && oldCap >= DEFAULT_INITIAL_CAPACITY) newThr = oldThr << 1; // double threshold } else if (oldThr > 0) // 初始化的容量被加入到threshold中,则新的容量等于就得threshold newCap = oldThr; else { // threshold=0,即threshold未被使用过。 newCap = DEFAULT_INITIAL_CAPACITY; newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY); } if (newThr == 0) {//代表 float ft = (float)newCap * loadFactor; newThr = (newCap < MAXIMUM_CAPACITY && ft < (float)MAXIMUM_CAPACITY ? (int)ft : Integer.MAX_VALUE); } threshold = newThr; @SuppressWarnings({"rawtypes","unchecked"}) Node<K,V>[] newTab = (Node<K,V>[])new Node[newCap]; table = newTab; 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 { // preserve order Node<K,V> loHead = null, loTail = null; Node<K,V> hiHead = null, hiTail = null; Node<K,V> next; 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; }
(e.hash & oldCap) == 0
是扩容的关键点,由于容量扩展为原来的两倍,至关于oldCap<<1
,因此计算hash时,须要考虑的二进制位数向高位多增长了一位(至关于求hash的掩码由之前的前x位为0,后32-x位1变为前x-1位0,32-x+1位1),为了不重复计算hash(key)和(n-1)&hash
,直接判断key的hash在增长位上的值是否为1(经过e.hash & oldCap
,获得增长位上,key的hash值。),若是为1,索引的二进制位的增长位也为1,若是为0,则索引的增长位也是0。既省去了从新计算hash值的时间,并且同时,因为新增的1bit是0仍是1能够认为是随机的,所以resize的过程,均匀的把以前的冲突的节点分散到新的bucket。 例如:
其中增长位为红色。 通过扩容从新分配 ,原来在一个bucket的index 5,分配到不一样的index=21的bucket,避免与index=5的key冲突,提升了查询的效率。
resize的策略:
putMapEntries
final void putMapEntries(Map<? extends K, ? extends V> m, boolean evict) { int s = m.size(); if (s > 0) { if (table == null) { // pre-size float ft = ((float)s / loadFactor) + 1.0F; int t = ((ft < (float)MAXIMUM_CAPACITY) ? (int)ft : MAXIMUM_CAPACITY); if (t > threshold) 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); } } }
putMapEntries是一个默认访问权限的final类型函数,表示该函数只能在它所在的包内访问,而且该方法不能被重载。
java访问权限复习: java的访问权限有:public,protected,private,默认。
- public是公开访问,全部的包中的类都可访问;
- protected是继承访问,对于同一个包的类,这个类的方法或变量是能够被访问的;对于不一样包的类,只有继承于该类的类才能够访问到该类的方法或者变量;
- private只能在该类自己中被访问,在类外以及其余类中都不能显示地进行访问;
- 默认访问权限是包访问权限,只有本包内的类能够访问。
get
public V get(Object key) { Node<K,V> e; return (e = getNode(hash(key), key)) == null ? null : e.value; } 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; }
getNode的核心流程:
final关键字:
- 修饰变量:变量的引用不能变,可是能够改变引用值;成员变量必须在构造器中初始化;
- 修饰函数:把方法锁定,以防任何继承类修改它的含义;提升效率效率。在早期的Java实现版本中,会将final方法转为内嵌调用。可是若是方法过于庞大,可能看不到内嵌调用带来的任何性能提高。在最近的Java版本中,不须要使用final方法进行这些优化了。
- 修饰类:类不能被继承。
null
的形式存储<null,Value>
键值对;hashCode()
进行hashing,并计算下标( n-1 )& hash
,从而得到buckets的位置。若是产生碰撞,则利用key.equals()
方法去链表或树中去查找对应的节点。TREEIFY_THRESHOLD
閥值,当一个桶里的Entry超过閥值,就不以单向链表而以红黑树来存放以加快Key的查找速度。Thanks for reading! want more