在总集篇中我大概梳理了一下整个集合类的关系,这篇文章是对总集篇的扩展,本文将详细讨论HashMap的实现原理。所有涉及到的源码都是基于JDK11。顺便插一句,大家要学就学新的嘛,毕竟11可是长期支持版本,可以用好几年的那种。

HashMap简介

HashMap是一种不需要维持键值对有序的基于Hash提供高效存取功能的集合类。不需要维持键值对有序是相对于SortedMap来说的,并不是指HashMap中的键值对不可以排序。基于Hash是指这种Map通过Hash的方式实现相关功能。防止有人将Hash和Map绑在一起看。

HashMap的类描述信息

public class HashMap<K,V> extends AbstractMap<K,V>implements Map<K,V>, Cloneable, Serializable

实现了克隆和序列化接口,支持相关功能。

HashMap的成员变量

// 默认容量16
static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; // aka 16
// 最大容量,2的30次方,没啥太大的意义
static final int MAXIMUM_CAPACITY = 1 << 30;
// 默认负载因子0.75,详细讨论见下文
static final float DEFAULT_LOAD_FACTOR = 0.75f;
// 转变成树的阈值,大于8开始变成树,详细讨论见下文
static final int TREEIFY_THRESHOLD = 8;
// 转变成链表的阈值,小于6开始退化为链表,详细讨论见下文
static final int TREEIFY_THRESHOLD = 8;
// 变树的另一个条件
static final int MIN_TREEIFY_CAPACITY = 64;
// HashMap存储结构中的数组
transient Node<K,V>[] table;
// HashMap中三大幻神之EntrySet,keySet,values其中keySet和values来自于父类
transient Set<Map.Entry<K,V>> entrySet;
transient Set<K>        keySet;
transient Collection<V> values;
// 大小
transient int size;
// 更改次数
transient int modCount;
// threshold=capacity*loadFactor,当size大于这个值时,需要扩容
int threshold;
// 负载因子
final float loadFactor;

支撑HashMap的内部类

先给张图,体会下。

我们知道HashMap中有三个集合,分别为entry集合,key集合,value集合,简称三幻神。它们有点像数据库中的view,我们可以通过它们遍历并操作数据,但是它们并没有保存数据,HashMap中的数据只在数据结构table和其延申链表或者树中保存。在上面的内部类中,可以分为四类,第一类entry相关的,Node,EntrySet,EntryIterator,EntrySpliterator。key集合与value集合与之类似。第四类时Node,TreeNode,其中Node被划分了两次,因为Node不仅是EntrySet中的元素,同时我们知道HashMap底层依赖于数组和红黑树实现,Node还是这个数组中的元素。同理,当Hash的冲突到一定程度的时候,冲突元素将被组织成一棵红黑树,而TreeNode就是这棵树中的节点。这些类之间的关系如下,由于三幻神都是Collection的实现类,而Collection继承了Iterable接口,该接口规定需要实现iterator(),spliterator(),forEach(Consumer<? super T> action),三个函数,前二者规定需要提供并行和串行迭代器,后面一个函数时函数式编程思想的体现。而XXXIterator和XXXSpliterator具体实现的串行和并行迭代器。

EntrySet,KeySet,Values的实现

Entry中放的元素,Node的定义如下:

static class Node<K,V> implements Map.Entry<K,V> {// 注意这个hashfinal int hash;final K key;V value;// Hash冲突后支持链表结构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; }// 重点,重写了hashCode// 生成新hash的方式为按位异或public final int hashCode() {return Objects.hashCode(key) ^ Objects.hashCode(value);}public final V setValue(V newValue) {V oldValue = value;value = newValue;return oldValue;}// 重点,重写了equals方法public final boolean equals(Object o) {// 这是Object类中equals方法,表示含义是,当o与this的内存地址相等的时候,表示是同一个对象。if (o == this)return true;/* 下面对相等在HashMap中的语义进行扩展,具体的是,如果两个对象的key和value相等,则它们也被认为为相等。
其中key和value的比较使用的是equals的原始语义*/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;}}

重要的东西已经写了注释,还需要提出的是,这个重写后的equals就是HashMap与IdentityHashMap之间的区别,即IdentityHashMap没有进行语义扩展而仅仅取决于equals的原始语义,也就是说IdentityHashMap判断相等的条件更为苛刻。
EntrySet,KeySet,Values的实现,建议只看EntrySet的实现,其他的都差不多:

// 注意EntrySet本质上是一个Set,原因是EntrySet不允许重复元素
final class EntrySet extends AbstractSet<Map.Entry<K,V>> {// 与HashMap的size同步public final int size()                 { return size; }// 对EntrySet的清空操作实际上操作的是HashMappublic final void clear()               { HashMap.this.clear(); }// Iterable规定需要实现的方法public final Iterator<Map.Entry<K,V>> iterator() {return new EntryIterator();}// 本质上是Node的equalspublic final boolean contains(Object o) {if (!(o instanceof Map.Entry))return false;Map.Entry<?,?> e = (Map.Entry<?,?>) o;Object key = e.getKey();Node<K,V> candidate = getNode(hash(key), key);return candidate != null && candidate.equals(e);}// 同样调用的是HashMap的remove方法public final boolean remove(Object o) {if (o instanceof Map.Entry) {Map.Entry<?,?> e = (Map.Entry<?,?>) o;Object key = e.getKey();Object value = e.getValue();return removeNode(hash(key), key, value, true, true) != null;}return false;}public final Spliterator<Map.Entry<K,V>> spliterator() {return new EntrySpliterator<>(HashMap.this, 0, -1, 0, 0);}// 使用modCount感知多线程操作public final void forEach(Consumer<? super Map.Entry<K,V>> action) {Node<K,V>[] tab;if (action == null)throw new NullPointerException();if (size > 0 && (tab = table) != null) {int mc = modCount;for (Node<K,V> e : tab) {for (; e != null; e = e.next)action.accept(e);}if (modCount != mc)throw new ConcurrentModificationException();}}}// KeySet也是一个Set
final class KeySet extends AbstractSet<K> {public final int size()                 { return size; }public final void clear()               { HashMap.this.clear(); }public final Iterator<K> iterator()     { return new KeyIterator(); }public final boolean contains(Object o) { return containsKey(o); }public final boolean remove(Object key) {return removeNode(hash(key), key, null, false, true) != null;}public final Spliterator<K> spliterator() {return new KeySpliterator<>(HashMap.this, 0, -1, 0, 0);}public final void forEach(Consumer<? super K> action) {Node<K,V>[] tab;if (action == null)throw new NullPointerException();if (size > 0 && (tab = table) != null) {int mc = modCount;for (Node<K,V> e : tab) {for (; e != null; e = e.next)action.accept(e.key);}if (modCount != mc)throw new ConcurrentModificationException();}}}// values是一个Collection,对元素重复没有限制
final class Values extends AbstractCollection<V> {public final int size()                 { return size; }public final void clear()               { HashMap.this.clear(); }public final Iterator<V> iterator()     { return new ValueIterator(); }public final boolean contains(Object o) { return containsValue(o); }public final Spliterator<V> spliterator() {return new ValueSpliterator<>(HashMap.this, 0, -1, 0, 0);}public final void forEach(Consumer<? super V> action) {Node<K,V>[] tab;if (action == null)throw new NullPointerException();if (size > 0 && (tab = table) != null) {int mc = modCount;for (Node<K,V> e : tab) {for (; e != null; e = e.next)action.accept(e.value);}if (modCount != mc)throw new ConcurrentModificationException();}}}

可以看到三幻神的大量功能是依赖于HashMap本身的,也就是说其功能是委托给HashMap来实现的,其实不难理解,因为它们都不持有数据,所以对数据的操作都要委托给HashMap,这在详细方法分析的时候会做详细说明。

串行迭代器

final class KeyIterator extends HashIteratorimplements Iterator<K> {public final K next() { return nextNode().key; }}final class ValueIterator extends HashIteratorimplements Iterator<V> {public final V next() { return nextNode().value; }}final class EntryIterator extends HashIteratorimplements Iterator<Map.Entry<K,V>> {public final Map.Entry<K,V> next() { return nextNode(); }
}
abstract class HashIterator {Node<K,V> next;        // next entry to returnNode<K,V> current;     // current entryint expectedModCount;  // for fast-fail,重点int index;             // current slot,这个是为了在table中找到下一个元素// 我们知道,HashMap底层是数组+链表+红黑树的组合,元素经过Hash运算散列之后,// 分布在数组当中,当产生hash冲突的时候,通过链表或者红黑树的方式对数组当前节点进行扩展以解决。// 所以在初始化的时候需要找到数组中第一个不为空的元素HashIterator() {expectedModCount = modCount;Node<K,V>[] t = table;current = next = null;index = 0;if (t != null && size > 0) { // advance to first entrydo {} while (index < t.length && (next = t[index++]) == null);}}// 直接判断next是否为空public final boolean hasNext() {return next != null;}/* 获取下一个节点的逻辑为:1.看当前节点的next域有没有后继节点,也就是说hash冲突后形成的链式结构上还有没有元素。2.如果链式结构上没有元素或者未冲突,则在HashMap的数组结构中寻找下一个不为空的元素*/final Node<K,V> nextNode() {Node<K,V>[] t;Node<K,V> e = next;if (modCount != expectedModCount)throw new ConcurrentModificationException();if (e == null)throw new NoSuchElementException();if ((next = (current = e).next) == null && (t = table) != null) {do {} while (index < t.length && (next = t[index++]) == null);}return e;}// 实际的remove功能还是执行的HashMap的removeNode,这个函数在后面分析。public final void remove() {Node<K,V> p = current;if (p == null)throw new IllegalStateException();if (modCount != expectedModCount)throw new ConcurrentModificationException();current = null;removeNode(p.hash, p.key, null, false, false);expectedModCount = modCount;}}

串行迭代器(iterator)指的是线程不安全的迭代器,也就是我们通常说的,这个迭代器会抛出快速失败的异常。这也就是为什么我们在使用的迭代器的时候,如果需要修改集合元素,需要使用迭代器提供的功能,而不是调用集合类的方法,通常如果遵循这个原则,使用串行迭代器是比较安全的。但是有的时候,看起来违反了这个准则的代码也可以很好的工作,比如:

    // 代码Map<String, String> map = new HashMap<>();map.put("k1", "k");map.put("k2", "k");map.put("k3", "k");map.put("k4", "k");map.put("k5", "k");Iterator<Map.Entry<String, String>> iterator = map.entrySet().iterator();while (iterator.hasNext()){if(iterator.next().getKey().equals("k5"))map.remove("k5");}map.forEach((k, v) -> System.out.println(k + " " + v));// 执行结果k1 kk2 kk3 kk4 k

在while中不仅使用了迭代器,而且调用了原集合的remove方法,这似乎违背了我们的准则,但是代码居然很好的工作了,现在我对代码做一点修改:

    Map<String, String> map = new HashMap<>();map.put("k1", "k");map.put("k2", "k");map.put("k3", "k");map.put("k4", "k");map.put("k5", "k");Iterator<Map.Entry<String, String>> iterator = map.entrySet().iterator();while (iterator.hasNext()){// 注意,这里是修改部分if(iterator.next().getKey().equals("k4"))map.remove("k4");}map.forEach((k, v) -> System.out.println(k + " " + v));// 执行结果Exception in thread "main" java.util.ConcurrentModificationException

这个改动会导致程序抛出快速失败异常,这会让人感到很困惑。后来我去查询了一些关于iterator快速失败的文章,主要有两个讨论方向,第一个思考角度时这样的,他们认为Iterator工作在另一个线程,并持有排他锁,通过这种方式实现了Iterator并发修改时抛出异常的功能。另一个思考角度是通过分析源码得出Iterator的这个功能是依赖于modCount这个成员变量来实现的。我认为第二个思考角度是合理的(当然你也可以有其他看法),主要原因在于,从源码(jdk11)的角度来讲,Iterator的其它实现有没有启动了额外的线程,并且上了锁我不知道,但是至少在集合类中是没有这么干的。另外集合类的所有涉及到元素变动都维护了modCount的一致性。所以从这个角度来说,明显第二种观点是正确的。所以有必要讲一下是怎么通过modCount来感知并发操作并且及时抛出异常的。简单来说,就是CAS思想,CAS保证了线程安全的修改。在生成一个Iterator的时候,会持有HashMap当前的modCount值,当通过Iterator的API对集合元素进行变更,它会自己维持其持有的modCount和HashMap的modCount一致,以Iterator的remove为例:

// 这调用的是HashMap的函数,会修改HashMap的modCount值
removeNode(p.hash, p.key, null, false, false);
// expectedModCount是Iterator持有的,马上将该值与modCount同步
expectedModCount = modCount;// 所以在下一次执行该函数的时候,这个判断可以通过
if (modCount != expectedModCount)

有了这个解释之后我们来看之前给出的例子,我们在迭代器中对最后一个元素调用了map的remove方法,这个时候已经造成了Iterator其内部持有的expectedModCount和HashMap持有的modCount不一致,此时我们如果再调用一次Iterator中依赖于expectedModCount的方法,比如remove,那么毫无疑问,会抛出异常,但恰恰就在于第一种情况我们在Iterator其内部持有的expectedModCount和HashMap持有的modCount不一致时完成了所有的迭代,而第二种则还没有完成,所以第一种情况下代码表现正常,而第二种则抛出了错误。

并行迭代器

    static class HashMapSpliterator<K,V> {final HashMap<K,V> map;Node<K,V> current;          // current nodeint index;                  // current index, modified on advance/splitint fence;                  // one past last indexint est;                    // size estimateint expectedModCount;       // for comodification checksHashMapSpliterator(HashMap<K,V> m, int origin,int fence, int est,int expectedModCount) {this.map = m;this.index = origin;this.fence = fence;this.est = est;this.expectedModCount = expectedModCount;}final int getFence() { // initialize fence and size on first useint hi;if ((hi = fence) < 0) {HashMap<K,V> m = map;est = m.size;expectedModCount = m.modCount;Node<K,V>[] tab = m.table;hi = fence = (tab == null) ? 0 : tab.length;}return hi;}public final long estimateSize() {getFence(); // force initreturn (long) est;}}static final class KeySpliterator<K,V>extends HashMapSpliterator<K,V>implements Spliterator<K> {KeySpliterator(HashMap<K,V> m, int origin, int fence, int est,int expectedModCount) {super(m, origin, fence, est, expectedModCount);}// 如何进行分割,分割后返回多个Spliterator,利于并发处理。// 划分规则,二分,由于HashMap虽然数组容量确定,但是每个数组位置上实际有的元素// 数量并不确定,所以使用二分的方式并不是一个平衡划分的方式,在元素相对较多且冲突较少的情况下划分后的并发性能通常较好。public KeySpliterator<K,V> trySplit() {int hi = getFence(), lo = index, mid = (lo + hi) >>> 1;return (lo >= mid || current != null) ? null :new KeySpliterator<>(map, lo, index = mid, est >>>= 1,expectedModCount);}// 对剩下的所有元素执行actionpublic void forEachRemaining(Consumer<? super K> action) {int i, hi, mc;if (action == null)throw new NullPointerException();HashMap<K,V> m = map;Node<K,V>[] tab = m.table;if ((hi = fence) < 0) {mc = expectedModCount = m.modCount;hi = fence = (tab == null) ? 0 : tab.length;}elsemc = expectedModCount;if (tab != null && tab.length >= hi &&(i = index) >= 0 && (i < (index = hi) || current != null)) {Node<K,V> p = current;current = null;do {if (p == null)p = tab[i++];else {action.accept(p.key);p = p.next;}} while (p != null || i < hi);if (m.modCount != mc)throw new ConcurrentModificationException();}}// 对当前元素执行action,若还有剩余元素,则返回true,否则返回falsepublic boolean tryAdvance(Consumer<? super K> action) {int hi;if (action == null)throw new NullPointerException();Node<K,V>[] tab = map.table;if (tab != null && tab.length >= (hi = getFence()) && index >= 0) {while (current != null || index < hi) {if (current == null)current = tab[index++];else {K k = current.key;current = current.next;action.accept(k);if (map.modCount != expectedModCount)throw new ConcurrentModificationException();return true;}}}return false;}public int characteristics() {return (fence < 0 || est == map.size ? Spliterator.SIZED : 0) |Spliterator.DISTINCT;}}static final class ValueSpliterator<K,V>extends HashMapSpliterator<K,V>implements Spliterator<V> {ValueSpliterator(HashMap<K,V> m, int origin, int fence, int est,int expectedModCount) {super(m, origin, fence, est, expectedModCount);}public ValueSpliterator<K,V> trySplit() {int hi = getFence(), lo = index, mid = (lo + hi) >>> 1;return (lo >= mid || current != null) ? null :new ValueSpliterator<>(map, lo, index = mid, est >>>= 1,expectedModCount);}public void forEachRemaining(Consumer<? super V> action) {int i, hi, mc;if (action == null)throw new NullPointerException();HashMap<K,V> m = map;Node<K,V>[] tab = m.table;if ((hi = fence) < 0) {mc = expectedModCount = m.modCount;hi = fence = (tab == null) ? 0 : tab.length;}elsemc = expectedModCount;if (tab != null && tab.length >= hi &&(i = index) >= 0 && (i < (index = hi) || current != null)) {Node<K,V> p = current;current = null;do {if (p == null)p = tab[i++];else {action.accept(p.value);p = p.next;}} while (p != null || i < hi);if (m.modCount != mc)throw new ConcurrentModificationException();}}public boolean tryAdvance(Consumer<? super V> action) {int hi;if (action == null)throw new NullPointerException();Node<K,V>[] tab = map.table;if (tab != null && tab.length >= (hi = getFence()) && index >= 0) {while (current != null || index < hi) {if (current == null)current = tab[index++];else {V v = current.value;current = current.next;action.accept(v);if (map.modCount != expectedModCount)throw new ConcurrentModificationException();return true;}}}return false;}public int characteristics() {return (fence < 0 || est == map.size ? Spliterator.SIZED : 0);}}static final class EntrySpliterator<K,V>extends HashMapSpliterator<K,V>implements Spliterator<Map.Entry<K,V>> {EntrySpliterator(HashMap<K,V> m, int origin, int fence, int est,int expectedModCount) {super(m, origin, fence, est, expectedModCount);}public EntrySpliterator<K,V> trySplit() {int hi = getFence(), lo = index, mid = (lo + hi) >>> 1;return (lo >= mid || current != null) ? null :new EntrySpliterator<>(map, lo, index = mid, est >>>= 1,expectedModCount);}public void forEachRemaining(Consumer<? super Map.Entry<K,V>> action) {int i, hi, mc;if (action == null)throw new NullPointerException();HashMap<K,V> m = map;Node<K,V>[] tab = m.table;if ((hi = fence) < 0) {mc = expectedModCount = m.modCount;hi = fence = (tab == null) ? 0 : tab.length;}elsemc = expectedModCount;if (tab != null && tab.length >= hi &&(i = index) >= 0 && (i < (index = hi) || current != null)) {Node<K,V> p = current;current = null;do {if (p == null)p = tab[i++];else {action.accept(p);p = p.next;}} while (p != null || i < hi);if (m.modCount != mc)throw new ConcurrentModificationException();}}public boolean tryAdvance(Consumer<? super Map.Entry<K,V>> action) {int hi;if (action == null)throw new NullPointerException();Node<K,V>[] tab = map.table;if (tab != null && tab.length >= (hi = getFence()) && index >= 0) {while (current != null || index < hi) {if (current == null)current = tab[index++];else {Node<K,V> e = current;current = current.next;action.accept(e);if (map.modCount != expectedModCount)throw new ConcurrentModificationException();return true;}}}return false;}public int characteristics() {return (fence < 0 || est == map.size ? Spliterator.SIZED : 0) |Spliterator.DISTINCT;}}

关于并行迭代器是在java8后支持函数式编程后引入的。关于这部分的详细内容就不在这里详细介绍了,后面可能会总结1.8的新特性。简单来说就是便利的并发性能,java在底层实现了自适应的并发策略,而用户只需要提供相应的切分规则和并发需要执行的功能即可。想了解原理的可以参考下google大佬们那三篇改变世界的分布式文章。里面介绍的Map/Reduce就是底层并发策略的基础。

TreeNode

        // 类声明static final class TreeNode<K,V> extends LinkedHashMap.Entry<K,V> // 支持树功能的成员变量TreeNode<K,V> parent;  // red-black tree linksTreeNode<K,V> left;TreeNode<K,V> right;TreeNode<K,V> prev;    // needed to unlink next upon deletionboolean red;// 继承自父类,依旧维持链表关系Entry<K,V> before, after;final int hash;final K key;V value;Node<K,V> next;

在HashMap中我不会涉及过多的红黑树的细节,这些内容我会放在TreeMap中进行介绍。在这里想要说明一点的是虽然在hash冲突比较严重的时候,会将冲突的节点由链表转变为树,但是在转变为树之后并不说链表结构就不在了,也就说此时树和链表结构同时共存。树是为了提供高效查找功能,而链表是为了提供高效遍历功能。此时涉及到一个问题,就是链表的头节点和树的根节点问题,这是两个节点,可能相同也可能不同,同时,在数组中存放的可能是树的根节点,也可能不是。

以上,就是HashMap中用于支持其功能的内部数据结构,其中除了用来存放数据的Node和TreeNode之外,最重要的就是三幻神的迭代器实现。总结一下Map的迭代方式,第一种就是基于串行迭代器的增强for循环,和使用Iterator进行的显示的迭代循环,其中增强的for循环只是Iterator的一个包装,所以还是需要遵循迭代器的使用规则,第二种就是基于并行迭代器的循环方式,这种方式有些不同,形式为forEach(function action),其中我们只需要提供迭代时需要执行的功能,Java会自己根据相关的规则采取优化的并行策略。通常来说,提供索引访问的集合类还有第三种迭代方式,即我们传统的方式。

HashMap中重要的方法实现

get方法

public V get(Object key) {Node<K,V> e;return (e = getNode(hash(key), key)) == null ? null : e.value;}

通过调用getNode方法来获取,需要传递两个参数,其中一个是hash(key),我们来看看hash()这个函数。

static final int hash(Object key) {int h;return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);}

这个函数的作用在于混合key的hash值的高16位和低16位,这样做的原因在于HashMap的容量一般不会太大,这样在做散列的时候会忽视高位的特征,所以将高位特征加入低位让其影响散列函数的行为,这个操作对降低散列后冲突的概论有一定帮助,但是并不是质的提升。
下面来看看getNode函数:

final Node<K,V> getNode(int hash, Object key) {Node<K,V>[] tab; Node<K,V> first, e; int n; K k;// table存在且table不为空,且指定元素存在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;}

这段代码需要说明的是(n - 1) & hash是什么意思,简单来说这就是一个高效的取模运算。它能实现取模运算的原因在于,HashMap中规定容量必须是2的整数幂。当一个数n是2的整数幂的时候,n-1的二进制表示有一个别称,叫掩码,而与掩码进行按位与运算等同于取模运算,其本质是屏蔽超出位数的影响使其落在指定范围。比如HashMap的容量为16,则15的二进制表示为01111,则在按位与的过程当中,只有后四位的特征会被保留,而后四位的取值为0-15,等同于取模运算。如果理解了这个东西,那么还有一条规则就比较好理解了,即HashMap扩容之后,元素需要被重新散列,重新散列的结果只有两种,第一种为新索引与旧索引相同,第二种为新索引等于就索引加上旧的容量。举个例子来说,比如原来16容量不足,进行扩容,则被扩容为32,则31的二进制表示为11111,在容量为16的时候,hash值为10001和00001的两个元素按位与后结果都为1,此时产生了冲突,原因是它们的后四位都相同。在容量为32的时候,10001按位与后为17,而00001按位与后为1,可以清楚的看到,第5位二进制位生效了,而第5位就表示16为原HashMap容量。同理进行推广,结论就是上面描述的规则。
经过(n - 1) & hash运算之后我们直接获取到了数组的索引,此时我们直接看数组中该索引位置的元素是否为我们需要寻找的元素,如果是,返回,不是,则按节点类型进行查找。链表的查找方式我们不谈,说一下树的查找,即getTreeNode函数,下面是函数的源码:

final TreeNode<K,V> getTreeNode(int h, Object k) {// 1.首先找到红黑树的根节点;2.使用根节点调用find方法return ((parent != null) ? root() : this).find(h, k, null);}
// 典型的红黑树查找方式,类似于二叉排序树的查找方式
final TreeNode<K,V> find(int h, Object k, Class<?> kc) {TreeNode<K,V> p = this;do {int ph, dir; K pk;TreeNode<K,V> pl = p.left, pr = p.right, q;// h小于当前节点,则左子树if ((ph = p.hash) > h)p = pl;// h大于当前节点,则右子树else if (ph < h)p = pr;// 以下都是hash相等的情况// 若key相等或者广义相等,则找到啦else if ((pk = p.key) == k || (k != null && k.equals(pk)))return p;// hash相等但是key不等的情况,先处理两种特殊情况,左子树为空则找右子树,右子树为空则找左子树else if (pl == null)p = pr;else if (pr == null)p = pl;// 此时hash不能决定查找顺序,则如果元素实现了comparable接口,则按该接口顺序查找else if ((kc != null ||(kc = comparableClassFor(k)) != null) &&(dir = compareComparables(kc, k, pk)) != 0)p = (dir < 0) ? pl : pr;// 如果你没有实现comparable接口,则先遍历右后便利左else if ((q = pr.find(h, k, kc)) != null)return q;elsep = pl;} while (p != null);return null;}

这里需要说明的是,如果你对前面Node的源码有印象,那么需要指明的是find中的hash指的是Node中的hash,其值为key和value按位与。当我们确定元素在数组中的位置时,使用的时key的hash值,当获取到在数组中的索引后,便不在使用key的hash值构造树或者链表了,因为同一个索引的元素其key的hash值经过散列后都相同,此时会使用Node的hash值进行处理,所以在find函数中出现了hash值相等的情况,但是这里hash值相等并不能说明就是需要查找的元素,因为Node的hash值由key和value共同决定,所以后面又对key进行单独判定,如果判定结果失败,此时表示Node的hash值也相同,那么如果对象实现了comparable接口,则用该接口来辅助构造之后的树,如果没有,则优先放右子树,虽然放右子树,但是红黑树有自平衡功能,放在右子树最后不一定在右子树,所以现在右子树查询,后再左子树查询。

put方法

public V put(K key, V value) {return putVal(hash(key), key, value, false, true);}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;// 如果数组当前索引位置为空,则可以直接放入if ((p = tab[i = (n - 1) & hash]) == null)tab[i] = newNode(hash, key, value, null);else {Node<K,V> e; K k;// 如果当前entry节点存在,则更新当前节点if (p.hash == hash &&((k = p.key) == key || (key != null && key.equals(k))))e = p;// 如果是树节点,则按树节点的插入要求插入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) {p.next = newNode(hash, key, value, null);// 到达阈值,变树if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1sttreeifyBin(tab, hash);break;}// 如果e节点存在hash值和key值都与传入的相同,则e节点即为目标节点,跳出循环if (e.hash == hash &&((k = e.key) == key || (key != null && key.equals(k))))break;// // 将p指向下一个节点p = e;}}if (e != null) { // existing mapping for keyV oldValue = e.value;if (!onlyIfAbsent || oldValue == null)e.value = value;// HashMap中空操作,共LinkedHashMap用的afterNodeAccess(e);return oldValue;}}++modCount;// 如果插入节点后节点数超过阈值,则调用resize方法进行扩容if (++size > threshold)resize();// 支持LinkedHashMap,在HashMap中是一个空操作afterNodeInsertion(evict);return null;}

首先来看看扩容的规则:

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;}// 扩容为两倍,阈值也扩容为两倍else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY &&oldCap >= DEFAULT_INITIAL_CAPACITY)newThr = oldThr << 1; // double threshold}// 比较特殊的情况,如果老表的容量为0, 老表的阈值大于0, 是因为初始容量被放入阈值,则将新表的容量设置为老表的阈值else if (oldThr > 0) // initial capacity was placed in thresholdnewCap = oldThr;else {               // zero initial threshold signifies using defaults// 否则默认值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"})// 至此,扩容完成,下面要完成扩容后原来table中元素的处理,重散列使元素放置到正确的位置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) {// 置空,GC可回收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 orderNode<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;// 否则,成链elseloTail.next = e;loTail = e;}// 索引为原索引加原容量的情况else {if (hiTail == null)hiHead = e;elsehiTail.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;}
// 树的重散列规则,index是table的索引,bit是原来的容量
final void split(HashMap<K,V> map, Node<K,V>[] tab, int index, int bit) {TreeNode<K,V> b = this;// Relink into lo and hi lists, preserving orderTreeNode<K,V> loHead = null, loTail = null;TreeNode<K,V> hiHead = null, hiTail = null;int lc = 0, hc = 0;// 从当前结点开始遍历红黑树for (TreeNode<K,V> e = b, next; e != null; e = next) {next = (TreeNode<K,V>)e.next;e.next = null;// 下面是调整链表的过程,在putVal中已经进行过说明if ((e.hash & bit) == 0) {if ((e.prev = loTail) == null)loHead = e;elseloTail.next = e;loTail = e;++lc;}else {if ((e.prev = hiTail) == null)hiHead = e;elsehiTail.next = e;hiTail = e;++hc;}}// 经过上述过程之后,原来的链表分别变味了两个链表,但此时两个链表的元素都还在一棵树当中,也就是说树还没有被分开if (loHead != null) {// 如果元素个数小于6则解树,untreeify的功能就是将链表中的节点由TreeNode替换为Nodeif (lc <= UNTREEIFY_THRESHOLD)tab[index] = loHead.untreeify(map);else {tab[index] = loHead;// 否则重新构建树,treeify函数的功能就是构建一棵红黑树,详细实现在Treemap中讨论if (hiHead != null) // (else is already treeified)loHead.treeify(tab);}}// 同理如上if (hiHead != null) {if (hc <= UNTREEIFY_THRESHOLD)tab[index + bit] = hiHead.untreeify(map);else {tab[index + bit] = hiHead;if (loHead != null)hiHead.treeify(tab);}}}

关于扩容规则,在java8之前的扩容方案中,如果并发操作,会造成死循环的问题,其原因在于扩容后元素顺序发生了变动,在1.8之后的扩容方案中解决了这个问题,所以死循环的问题不会再发生,其次,不要在并发的情况下使用HashMap,别人本来就不保证并发安全。再来看看putTreeVal函数

final TreeNode<K,V> putTreeVal(HashMap<K,V> map, Node<K,V>[] tab,int h, K k, V v) {Class<?> kc = null;boolean searched = false;TreeNode<K,V> root = (parent != null) ? root() : this;// 从根节点进行遍历for (TreeNode<K,V> p = root;;) {int dir, ph; K pk;// 小于则走左子树if ((ph = p.hash) > h)dir = -1;// 大于则走右子树else if (ph < h)dir = 1;// 如果是该元素,则返回该元素else if ((pk = p.key) == k || (k != null && k.equals(pk)))return p;// 如果没有实现comparable接口else if ((kc == null &&(kc = comparableClassFor(k)) == null) ||(dir = compareComparables(kc, k, pk)) == 0) {// 第一次遇到这种情况,则在左右子树中寻找是否存在该节点,存在则返回用于更新if (!searched) {TreeNode<K,V> q, ch;searched = true;if (((ch = p.left) != null &&(q = ch.find(h, k, kc)) != null) ||((ch = p.right) != null &&(q = ch.find(h, k, kc)) != null))return q;}// 没找到,定义一套规则来处理这种极端情况,规则为:// 1.处理元素为空的情况// 2.比较元素类名// 3.比较元素的hashCode值dir = tieBreakOrder(k, pk);}// 在确定的位置插入节点,并维持树的结构TreeNode<K,V> xp = p;if ((p = (dir <= 0) ? p.left : p.right) == null) {Node<K,V> xpn = xp.next;TreeNode<K,V> x = map.newTreeNode(h, k, v, xpn);if (dir <= 0)xp.left = x;elsexp.right = x;xp.next = x;x.parent = x.prev = xp;if (xpn != null)((TreeNode<K,V>)xpn).prev = x;// 将树的根节点放到链表的头节点中,实现原理是通过调整链表中元素的位置实现的,// 最后还会检查整个树是否符合红黑树的定义moveRootToFront(tab, balanceInsertion(root, x));return null;}}}

之后再来看看变树的函数treeifyBin:

// 这个函数很简单,本质是在调用treeify函数
final void treeifyBin(Node<K,V>[] tab, int hash) {int n, index; Node<K,V> e;if (tab == null || (n = tab.length) < MIN_TREEIFY_CAPACITY)resize();else if ((e = tab[index = (n - 1) & hash]) != 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);}}

remove方法

public V remove(Object key) {Node<K,V> e;return (e = removeNode(hash(key), key, null, false, true)) == null ?null : e.value;}
// 删除节点基本思路分两步,第一步先找到,第二部删除,第一步的源码已经分析过了
final Node<K,V> removeNode(int hash, Object key, Object value,boolean matchValue, boolean movable) {Node<K,V>[] tab; Node<K,V> p; int n, index;// 开始找if ((tab = table) != null && (n = tab.length) > 0 &&(p = tab[index = (n - 1) & hash]) != null) {Node<K,V> node = null, e; K k; V v;if (p.hash == hash &&((k = p.key) == key || (key != null && key.equals(k))))node = p;else if ((e = p.next) != null) {if (p instanceof TreeNode)node = ((TreeNode<K,V>)p).getTreeNode(hash, key);else {do {if (e.hash == hash &&((k = e.key) == key ||(key != null && key.equals(k)))) {node = e;break;}p = e;} while ((e = e.next) != null);}}// 找到了,开始执行删除if (node != null && (!matchValue || (v = node.value) == value ||(value != null && value.equals(v)))) {// 如果是树节点执行树节点的删除,本质上是红黑树节点的删除,在TreeMap中讨论,只是需要额外维护链表顺序if (node instanceof TreeNode)((TreeNode<K,V>)node).removeTreeNode(this, tab, movable);// 否则链表删除else if (node == p)tab[index] = node.next;elsep.next = node.next;++modCount;--size;// LinkedHashMap用的,HashMap中是空操作afterNodeRemoval(node);return node;}}return null;}

这里提一下LinkedHashMap,LinkedHashMap是HashMap的子类,LinkedHashMap保留了HashMap的底层结构,同时加入了双向链表的结构,双向链表的结构主要就是为了保存插入顺序,LinkedHashMap只要实现在Map中遗留的一些函数即可实现这些功能。

Java集合之HashMap相关推荐

  1. java集合之HashMap相关原理 方法

    java集合之HashMap Map接口的基于哈希表的实现. 此实现提供所有可选的映射操作,并允许空null值和空null键.(除了非同步和允许使用 null 之外,HashMap 类与 Hashta ...

  2. Java集合:HashMap源码剖析

    一.HashMap概述 二.HashMap的数据结构 三.HashMap源码分析      1.关键属性      2.构造方法      3.存储数据      4.调整大小 5.数据读取     ...

  3. Java集合之一—HashMap

    深入浅出学Java--HashMap 哈希表(hash table) 也叫散列表,是一种非常重要的数据结构,应用场景及其丰富,许多缓存技术(比如memcached)的核心其实就是在内存中维护一张大的哈 ...

  4. hashmap修改对应key的值_死磕 java集合之HashMap源码分析

    简介 HashMap采用key/value存储结构,每个key对应唯一的value,查询和修改的速度都很快,能达到O(1)的平均时间复杂度.它是非线程安全的,且不保证元素存储的顺序: 继承体系 Has ...

  5. Java——集合(HashMap与Hashtable的区别)

    * HashMap和Hashtable的区别* 共同点:* 底层都是哈希算法,都是双列集合* 区别:* 1,HashMap是线程不安全的,效率高* Hashtable是线程安全的,效率低 * 2,Ha ...

  6. Java集合:HashMap线程不安全?有哪些表现?

    HashMap是线程不安全的!主要表现在多线程情况下: 1)hash冲突时,put方法不是同步的,先存的值会被后存的值覆盖.(1.7和1.8都有的表现) 2)在resize的时候,可能会导致死循环(环 ...

  7. Java集合中HashMap日常问题及解决办法

    2019独角兽企业重金招聘Python工程师标准>>> 前言 今天在学习Session的时候,利用了Session可持久化保存服务器端的特性尝试做了一下用HashMap嵌套的购物车( ...

  8. java集合:HashMap的底层实现原理

    HashMap的底层实现原理是面试中出现频率非常高的一道面试题,本文将对HashMap的底层实现原理做一个简要的概况和总结,便于复习. 一.对于Map集合存储结构的理解 首先介绍以HashMap为典型 ...

  9. Java集合:HashMap

    各种Map总结 就比如问你 HashMap 是不是有序的?你回答不是有序的. 那面试官就会可能继续问你,有没有有序的Map实现类呢?你如果这个时候说不知道的话,那这块问题就到此结束了.如果你说有 Tr ...

最新文章

  1. Linux下tomcat的安装与卸载以及配置(超简单)
  2. python executemany
  3. How do I sort groups of data items?(WPF)
  4. CrashFinder,找到崩溃代码行
  5. QueryString加密
  6. java加载类时静态代码块、构造代码块、构造方法执行顺序
  7. 自适应宽_移动端实现自适应缩放界面的方法汇总
  8. 内核中的UDP socket流程(11)——ip_append_data
  9. QT读取csv文件并且绘制折线图
  10. mysql数据库 怎么替换_mysql数据库替换
  11. vue启动项目报错 `webpack-dev-server --inline --progress --config build/webpack.dev.conf
  12. H3C防火墙-安全域
  13. 【NOIP2016普及组】复赛——魔法阵
  14. iPhone 屏幕尺寸
  15. CTF show 萌新区解题报告 (一)
  16. 喜欢花,喜欢海,喜欢日出和日落
  17. 常见的分布式解决方案
  18. 安卓手机屏幕共享给电脑操作的几款软件
  19. 多线程 4——线程通信、线程池、定时器
  20. python3 16进制字符串转ASCii码值

热门文章

  1. C/C++数字转字符串
  2. Java Web总结
  3. DeFi仍是鲸鱼的专属?前5名地址竟能占总供应量的40%以上
  4. SSM整合Jackson
  5. SkyIDC - 国际云服务厂商
  6. 18-Openwrt sysupgrade系统升级
  7. 蓝牙耳机什么牌子好?国产蓝牙耳机最好的牌子推荐
  8. JPress企业模板-Athena
  9. 女孩需要富养出来的优雅
  10. 10、门禁系统安装与调试知识大全