【Java入门提高篇】Day24 Java容器类详解(七)HashMap源码分析(下)

  【Java入门提高篇】Day24 Java容器类详解(七)HashMap源码分析(下)前两篇对HashMap这家伙的主要方法,主要算法做了一个详细的介绍,本篇主要介绍HashMap中默默无闻地工作着的集合们,包括KeySet,values,EntrySet,以及对应的迭代器:HashIterator,KeyIterator,ValueIterator,EntryIterator和 fast-fail 机制。会介绍三个集合的作用以及它们中隐藏的惊人秘密。【Java入门提高篇】Day24 Java容器类详解(七)HashMap源码分析(下)

KeySet

  我们先来看看KeySet,HashMap中的成员变量keySet保存了所有的Key集合,事实上,这是继承自它的父类AbstractMap的成员变量:

transient Set<K> keySet;

  而keySet方法,也是覆盖了父类的方法:

//AbstractMap 中的keySet方法

    public Set<K> keySet() {
Set<K> ks = keySet;
if (ks == null) {
ks = new AbstractSet<K>() {
public Iterator<K> iterator() {
return new Iterator<K>() {
private Iterator<Entry<K,V>> i = entrySet().iterator(); public boolean hasNext() {
return i.hasNext();
} public K next() {
return i.next().getKey();
} public void remove() {
i.remove();
}
};
} public int size() {
return AbstractMap.this.size();
} public boolean isEmpty() {
return AbstractMap.this.isEmpty();
} public void clear() {
AbstractMap.this.clear();
} public boolean contains(Object k) {
return AbstractMap.this.containsKey(k);
}
};
keySet = ks;
}
return ks;
}
//HashMap 中的keySet方法

    /**
* 返回一个键值的集合视图,该集合由map支持,因此对map的更改会反映在集合中,反之亦然。
* 如果在对集合进行迭代的过程中修改了map中的映射(除了通过迭代器的删除操作),迭代的结果是未定义的。
* 该集合支持元素删除,通过Iterator.remove,Set.remove,removeAll,retainAll和clear操作
* 从映射中删除相应的映射。 它不支持add或addAll操作。
*/
public Set<K> keySet() {
Set<K> ks = keySet;
if (ks == null) {
ks = new KeySet();
keySet = ks;
}
return ks;
}

  可以看到,AbstractMap中keySet是一个AbstractSet类型,而覆盖后的keySet方法中,keySet被赋值为KeySet类型。翻翻构造器可以发现,在构造器中并没有初始化keySet,而是在KeySet方法中对keySet进行的初始化(HashMap中都是使用类似的懒加载机制),KeySet是HashMap中的一个内部类,让我们再来看看这个KeySet类型的全貌:

    final class KeySet extends AbstractSet<K> {
public final int size() { return size; }
public final void clear() { 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 (int i = 0; i < tab.length; ++i) {
for (Node<K,V> e = tab[i]; e != null; e = e.next)
action.accept(e.key);
}
if (modCount != mc)
throw new ConcurrentModificationException();
}
}
}

  其实KeySet就是继承自AbstractSet,并覆盖了其中的大部分方法,遍历KeySet时,会使用其中的KeyIterator,至于Spliterator,是为并行遍历设计的,一般是用于Stream的并行操作。forEach方法则是用于遍历操作,将函数式接口操作action应用于每一个元素,我们来看一个小栗子:

public class Test {

    public static void main(String[] args) {
Map<String, Integer> map = new HashMap();
map.put("小明", 66);
map.put("小李", 77);
map.put("小红", 88);
map.put("小刚", 89);
map.put("小力", 90);
map.put("小王", 91);
map.put("小黄", 92);
map.put("小青", 93);
map.put("小绿", 94);
map.put("小黑", 95);
map.put("小蓝", 96);
map.put("小紫", 97);
map.put("小橙", 98);
map.put("小赤", 99);
map.put("Frank", 100); Set<String> ks = map.keySet();
System.out.printf("keySet:%s,keySet的大小:%d,keySet中是否包含Frank:%s", ks, ks.size(), ks.contains("Frank"));
System.out.println();
ks.forEach((item) -> System.out.println(item));
}
}

  输出如下:

keySet:[小刚, 小橙, 小蓝, 小力, 小青, 小黑, 小明, 小李, 小王, 小紫, 小红, 小绿, Frank, 小黄, 小赤],keySet的大小:15,keySet中是否包含Frank:true
小刚
小橙
小蓝
小力
小青
小黑
小明
小李
小王
小紫
小红
小绿
Frank
小黄
小赤

  如果不记得这个AbstractMap和AbstractSet在容器框架中是什么地位,可以往前翻翻这系列文章的第一篇,看看容器家族的族谱。【Java入门提高篇】Day24 Java容器类详解(七)HashMap源码分析(下)

  但是说了这么多,这个keySet。里面的元素是什么时候放进去的呢?我们自然会想到,大概就是调用put方法往里添加元素的时候,顺便把key放进keySet中,完美!【Java入门提高篇】Day24 Java容器类详解(七)HashMap源码分析(下)让我们再回顾一下putVal方法,来看看是不是这样的:

    final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
boolean evict) {
Node<K,V>[] tab; Node<K,V> p; int n, i;
//如果当前table未初始化,则先重新调整大小至初始容量
if ((tab = table) == null || (n = tab.length) == 0)
n = (tab = resize()).length;
//(n-1)& hash 这个地方即根据hash求序号,想了解更多散列相关内容可以查看下一篇
if ((p = tab[i = (n - 1) & hash]) == null)
//不存在,则新建节点
tab[i] = newNode(hash, key, value, null);
else {
Node<K,V> e; K k;
//先找到对应的node
if (p.hash == hash &&
((k = p.key) == key || (key != null && key.equals(k))))
e = p;
else if (p instanceof TreeNode)
//如果是树节点,则调用相应的putVal方法,这部分放在第三篇内容里
//todo putTreeVal
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 1st
//如果链表长度达到树化的最大长度,则进行树化,该函数内容也放在第三篇
//todo treeifyBin
treeifyBin(tab, hash);
break;
}
if (e.hash == hash &&
((k = e.key) == key || (key != null && key.equals(k))))
break;
p = e;
}
}
//如果已存在该key的映射,则将值进行替换
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;
}

  emmmmm,好像没找到?【Java入门提高篇】Day24 Java容器类详解(七)HashMap源码分析(下)你也许会想,会不会是在TreeNode的putTreeVal方法或者在treeifyBin方法中对key进行插入?好了好了,不要再翻了,其实这个奥秘隐藏在KeySet的迭代器中,再回头看看,它的迭代器返回的是一个KeyIterator,而KeyIterator也是HashMap中的一个内部类,继承自HashMap中的另一个内部类HashIterator。

HashIterator

  让我们带着这个疑问,来看看这个HashIterator类里到底有什么玄机:

    abstract class HashIterator {
//指向下一个节点
Node<K,V> next;
//当前节点
Node<K,V> current;
//为实现 fast-fail 机制而设置的期望修改数
int expectedModCount;
//当前遍历到的序号
int index; HashIterator() {
expectedModCount = modCount;
Node<K,V>[] t = table;
current = next = null;
index = 0;
if (t != null && size > 0) {
// 移动到第一个非null节点
do {} while (index < t.length && (next = t[index++]) == null);
}
} public final boolean hasNext() {
return next != null;
} final Node<K,V> nextNode() {
Node<K,V>[] t;
Node<K,V> e = next;
// fast-fail 机制的实现 即在迭代器往后遍历时,每次都检测expectedModCount是否和modCount相等
// 不相等则抛出ConcurrentModificationException异常
if (modCount != expectedModCount)
throw new ConcurrentModificationException();
//如果遍历越界,则抛出NoSuchElementException异常
if (e == null)
throw new NoSuchElementException();
if ((next = (current = e).next) == null && (t = table) != null) {
//如果遍历到末尾,则跳到table中下一个不为null的节点处
do {} while (index < t.length && (next = t[index++]) == null);
}
return e;
} public final void remove() {
Node<K,V> p = current;
if (p == null)
throw new IllegalStateException();
if (modCount != expectedModCount)
throw new ConcurrentModificationException();
current = null;
K key = p.key;
//移除节点
removeNode(hash(key), key, null, false, false);
expectedModCount = modCount;
}
}

  可以发现,在迭代器中,使用nextNode进行遍历时,先把next引用赋值给current,然后把next.next赋值给next,再获取了外部类HashMap中的table引用(t = table),这样就直接通过遍历table的方式来实现对key,value和entry的读取。

 if ((next = (current = e).next) == null && (t = table) != null) {
//如果遍历到末尾,则跳到table中下一个不为null的节点处
do {} while (index < t.length && (next = t[index++]) == null);
}

  KeyIterator,ValueIterator,EntryIterator都是HashIterator的子类,实现也很简单,仅仅修改了泛型类型:

    final class KeyIterator extends HashIterator
implements Iterator<K> {
public final K next() { return nextNode().key; }
} final class ValueIterator extends HashIterator
implements Iterator<V> {
public final V next() { return nextNode().value; }
} final class EntryIterator extends HashIterator
implements Iterator<Map.Entry<K,V>> {
public final Map.Entry<K,V> next() { return nextNode(); }
}

  这样keySet在遍历的时候,就可以通过它的迭代器去遍历访问外部类HashMap中的table,类似的,values和entrySet也是使用相似的方式进行遍历。

    public Collection<V> values() {
Collection<V> vs = values;
if (vs == null) {
vs = new Values();
values = vs;
}
return vs;
} final class Values extends AbstractCollection<V> {
public final int size() { return size; }
public final void clear() { 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 (int i = 0; i < tab.length; ++i) {
for (Node<K,V> e = tab[i]; e != null; e = e.next)
action.accept(e.value);
}
if (modCount != mc)
throw new ConcurrentModificationException();
}
}
}
    public Set<Map.Entry<K,V>> entrySet() {
Set<Map.Entry<K,V>> es;
return (es = entrySet) == null ? (entrySet = new EntrySet()) : es;
} final class EntrySet extends AbstractSet<Map.Entry<K,V>> {
public final int size() { return size; }
public final void clear() { this.clear(); }
public final Iterator<Map.Entry<K,V>> iterator() {
return new EntryIterator();
}
public 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);
}
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<K,V>(HashMap.this, 0, -1, 0, 0);
}
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 (int i = 0; i < tab.length; ++i) {
for (Node<K,V> e = tab[i]; e != null; e = e.next)
action.accept(e);
}
if (modCount != mc)
throw new ConcurrentModificationException();
}
}
}

  至此,这个未解之谜算是告一段落了。【Java入门提高篇】Day24 Java容器类详解(七)HashMap源码分析(下)

transient

  但是,细心的同学可能会发现,HashMap中的table,entrySet,keySet,value等成员变量,都是用transient修饰的,为什么要这样做呢?

  首先,我们还是先说说这个transient是干嘛用的,这就要涉及Java中的序列化了,序列化是什么东西呢?

Java中对象的序列化指的是将对象转换成以字节序列的形式来表示,这些字节序列包含了对象的数据和信息。
一个序列化后的对象可以被写到数据库或文件中,也可用于网络传输,一般当我们使用缓存cache(内存空间不够有可能会本地存储到硬盘)或远程调用rpc(网络传输)的时候,
经常需要让我们的实体类实现Serializable接口,目的就是为了让其可序列化。

  当然,就像数据存储是为了读取那样,序列化后的最终目的是为了恢复成原先的Java对象,要不然序列化后干嘛呢,这个过程就叫做反序列化。

  当我们使用实现Serializable接口的方式来进行序列化时,所有字段都会被序列化,那如果不想让某个字段被序列化(比如出于安全考虑,不将敏感字段序列化传输),便可以使用transient关键字来标志,表示不想让这个字段被序列化。

  那么问题来了,存储节点信息的table用transient修饰了,那么序列化和反序列化的时候,数据还怎么传输???

  emmmm,这又涉及到一个蛋疼的操作,序列化并没有那么简单,实现了Serializable接口后,在序列化时,会先检测这个类是否存在writeObject和readObject方法,如果存在,则调用相应的方法:

    /**
* 将HashMap的实例状态保存到一个流中
*/
private void writeObject(java.io.ObjectOutputStream s)
throws IOException {
int buckets = capacity();
// 写出threshold,loadfactor和所有隐藏的成员
s.defaultWriteObject();
s.writeInt(buckets);
s.writeInt(size);
internalWriteEntries(s);
} /**
* 从流中重构HashMap实例
*/
private void readObject(java.io.ObjectInputStream s)
throws IOException, ClassNotFoundException {
// 读取threshold,loadfactor和所有隐藏的成员
s.defaultReadObject();
reinitialize();
if (loadFactor <= 0 || Float.isNaN(loadFactor))
throw new InvalidObjectException("Illegal load factor: " +
loadFactor);
// 读取并忽略桶的数量
s.readInt();
// 读取映射的数量
int mappings = s.readInt();
if (mappings < 0)
throw new InvalidObjectException("Illegal mappings count: " +
mappings);
else if (mappings > 0) {
// (如果是0,则使用默认值)
// Size the table using given load factor only if within
// range of 0.25...4.0
float lf = Math.min(Math.max(0.25f, loadFactor), 4.0f);
float fc = (float)mappings / lf + 1.0f;
int cap = ((fc < DEFAULT_INITIAL_CAPACITY) ?
DEFAULT_INITIAL_CAPACITY :
(fc >= MAXIMUM_CAPACITY) ?
MAXIMUM_CAPACITY :
tableSizeFor((int)fc));
float ft = (float)cap * lf;
threshold = ((cap < MAXIMUM_CAPACITY && ft < MAXIMUM_CAPACITY) ?
(int)ft : Integer.MAX_VALUE); SharedSecrets.getJavaOISAccess().checkArray(s, Map.Entry[].class, cap);
@SuppressWarnings({"rawtypes","unchecked"})
Node<K,V>[] tab = (Node<K,V>[])new Node[cap];
table = tab; // 读取键值对信息,然后把映射插入HashMap实例中
for (int i = 0; i < mappings; i++) {
@SuppressWarnings("unchecked")
K key = (K) s.readObject();
@SuppressWarnings("unchecked")
V value = (V) s.readObject();
putVal(hash(key), key, value, false, false);
}
}
}

  这确实是一个极其糟糕的设计。。。而且这里还是一个private方法。【Java入门提高篇】Day24 Java容器类详解(七)HashMap源码分析(下)

  那么直接使用默认的序列化不好吗?非要大费周章的骚操作一波?一部分原因是为了解决效率问题,因为HashMap中很多桶是空的,将其序列化没有任何意义,所以需要手动使用 writeObject() 方法,只序列化实际存储元素的数组。另一个很重要的原因便是,HashMap的存储是依赖于对象的hashCode的,而Object.hashCode()方法是依赖于具体虚拟机的,所以同一个对象,在不同虚拟机中的HashCode可能不同,那这样映射到的HashMap中的位置也不一样,这样序列化和反序列化的对象就不一样了。【Java入门提高篇】Day24 Java容器类详解(七)HashMap源码分析(下)引用大神的一段话:

For example, consider the case of a hash table. The physical
representation is a sequence of hash buckets containing key-value
entries. The bucket that an entry resides in is a function of the hash
code of its key, which is not, in general, guaranteed to be the same
from JVM implementation to JVM implementation. In fact, it isn't even
guaranteed to be the same from run to run. Therefore, accepting the
default serialized form for a hash table would constitute a serious
bug. Serializing and deserializing the hash table could yield an
object whose invariants were seriously corrupt.

  蹩脚翻译一下:

例如,考虑散列表的情况。 它的物理存储是一系列包含键值条目的散列桶。 条目驻留的存储区是其密钥的哈希码的函数,
通常,JVM的实现不保证相同。 事实上,它甚至不能保证每次运行都是一样的。 因此,接受哈希表的默认序列化形式将构成严重的错误。
对哈希表进行序列化和反序列化可能会产生不变性被严重损毁的对象。

  好了,到此为止,这部分内容算是over了,后面会继续介绍HashMap中最麻烦的一部分,TreeNode【Java入门提高篇】Day24 Java容器类详解(七)HashMap源码分析(下)让我们师母已呆【Java入门提高篇】Day24 Java容器类详解(七)HashMap源码分析(下)

  记得动动小手点个赞或者点个关注哦,如果觉得不错的话,也欢迎分享给你的朋友,让bug传播的更远一些,【Java入门提高篇】Day24 Java容器类详解(七)HashMap源码分析(下)呸,说错了,让知识传播的更远一些【Java入门提高篇】Day24 Java容器类详解(七)HashMap源码分析(下)如果写的有误的地方,欢迎大家及时指出,我会第一时间予以修正,也欢迎提出改进建议,之后还会继续更新,欢迎继续关注!

上一篇:浅谈《think in java》:一 对象导论总结


下一篇:SecureCRT光标颜色