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

本文涉及的产品
容器服务 Serverless 版 ACK Serverless,317元额度 多规格
容器镜像服务 ACR,镜像仓库100个 不限时长
容器服务 Serverless 版 ACK Serverless,952元额度 多规格
简介:   前两篇对HashMap这家伙的主要方法,主要算法做了一个详细的介绍,本篇主要介绍HashMap中默默无闻地工作着的集合们,包括KeySet,values,EntrySet,以及对应的迭代器:HashIterator,KeyIterator,ValueIterator,EntryIterator和 fast-fail 机制。

  前两篇对HashMap这家伙的主要方法,主要算法做了一个详细的介绍,本篇主要介绍HashMap中默默无闻地工作着的集合们,包括KeySet,values,EntrySet,以及对应的迭代器:HashIterator,KeyIterator,ValueIterator,EntryIterator和 fast-fail 机制。会介绍三个集合的作用以及它们中隐藏的惊人秘密。

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在容器框架中是什么地位,可以往前翻翻这系列文章的第一篇,看看容器家族的族谱。

  但是说了这么多,这个keySet。里面的元素是什么时候放进去的呢?我们自然会想到,大概就是调用put方法往里添加元素的时候,顺便把key放进keySet中,完美!让我们再回顾一下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,好像没找到?你也许会想,会不会是在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();
            }
        }
    }

  至此,这个未解之谜算是告一段落了。

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方法。

  那么直接使用默认的序列化不好吗?非要大费周章的骚操作一波?一部分原因是为了解决效率问题,因为HashMap中很多桶是空的,将其序列化没有任何意义,所以需要手动使用 writeObject() 方法,只序列化实际存储元素的数组。另一个很重要的原因便是,HashMap的存储是依赖于对象的hashCode的,而Object.hashCode()方法是依赖于具体虚拟机的,所以同一个对象,在不同虚拟机中的HashCode可能不同,那这样映射到的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让我们师母已呆

  记得动动小手点个赞或者点个关注哦,如果觉得不错的话,也欢迎分享给你的朋友,让bug传播的更远一些,呸,说错了,让知识传播的更远一些如果写的有误的地方,欢迎大家及时指出,我会第一时间予以修正,也欢迎提出改进建议,之后还会继续更新,欢迎继续关注!

 

真正重要的东西,用眼睛是看不见的。
相关文章
|
9天前
|
存储 Kubernetes Cloud Native
探索Python编程的奥秘云原生时代的容器编排:Kubernetes入门与实践
【8月更文挑战第30天】本文以浅显易懂的方式,探讨了Python编程的核心概念和技巧。从基础语法到高级特性,再到实际应用案例,逐步引导读者深入理解Python编程的精髓。通过本文的学习,读者将能够掌握Python编程的基本技能,并激发进一步探索的兴趣。
26 13
|
8天前
|
算法 Java 开发者
Java 编程入门:从零到一的旅程
本文将带领读者开启Java编程之旅,从最基础的语法入手,逐步深入到面向对象的核心概念。通过实例代码演示,我们将一起探索如何定义类和对象、实现继承与多态,并解决常见的编程挑战。无论你是编程新手还是希望巩固基础的开发者,这篇文章都将为你提供有价值的指导和灵感。
|
10天前
|
存储 Java 程序员
Java中的集合框架:从入门到精通
【8月更文挑战第30天】在Java的世界里,集合框架是一块基石,它不仅承载着数据的存储和操作,还体现了面向对象编程的精髓。本篇文章将带你遨游Java集合框架的海洋,从基础概念到高级应用,一步步揭示它的奥秘。你将学会如何选择合适的集合类型,掌握集合的遍历技巧,以及理解集合框架背后的设计哲学。让我们一起探索这个强大工具,解锁数据结构的新视角。
|
8天前
|
存储 开发者 C#
WPF与邮件发送:教你如何在Windows Presentation Foundation应用中无缝集成电子邮件功能——从界面设计到代码实现,全面解析邮件发送的每一个细节密武器!
【8月更文挑战第31天】本文探讨了如何在Windows Presentation Foundation(WPF)应用中集成电子邮件发送功能,详细介绍了从创建WPF项目到设计用户界面的全过程,并通过具体示例代码展示了如何使用`System.Net.Mail`命名空间中的`SmtpClient`和`MailMessage`类来实现邮件发送逻辑。文章还强调了安全性和错误处理的重要性,提供了实用的异常捕获代码片段,旨在帮助WPF开发者更好地掌握邮件发送技术,提升应用程序的功能性与用户体验。
17 0
|
8天前
|
Java 程序员 UED
Java 中的异常处理:从入门到精通
【8月更文挑战第31天】在Java编程的世界中,异常处理是保持应用稳定性的重要机制。本文将引导你理解异常的本质,学会如何使用try-catch语句来捕获和处理异常,并探索自定义异常类的魅力。我们将一起深入异常的世界,让你的代码更加健壮和用户友好。
|
8天前
|
Java 数据库连接 开发者
Java中的异常处理:从入门到精通
【8月更文挑战第31天】 在编程世界中,错误和异常就像是不请自来的客人,总是在不经意间打扰我们的程序运行。Java语言通过其异常处理机制,为开发者提供了一套优雅的“待客之道”。本文将带你走进Java异常处理的世界,从基础语法到高级技巧,再到最佳实践,让你的程序在面对意外时,也能从容不迫,优雅应对。
|
8天前
|
Java 开发者
Java 中的异常处理:从入门到精通
【8月更文挑战第31天】在Java的世界中,异常处理是保持程序健壮性的基石。本文将带你探索Java异常处理的奥秘,从基本的try-catch语句到深入理解自定义异常和最佳实践。你将学会如何优雅地处理错误,确保你的代码不仅能够面对意外情况,还能从中恢复。让我们一起开启这段旅程,掌握让程序更加稳定和可靠的技巧吧!
|
10天前
|
运维 Kubernetes Cloud Native
云原生技术入门:从容器到Kubernetes的探索之旅
【8月更文挑战第30天】在数字时代的浪潮中,云计算已成为推动创新的重要力量。本文旨在通过浅显易懂的语言,为初学者揭开云原生技术的神秘面纱,从容器化技术的基础出发,逐步深入到Kubernetes集群管理的实际应用。我们将一起见证代码如何转化为可在云端无缝运行的服务,体验技术变革带来的无限可能。
|
10天前
|
Kubernetes Cloud Native 网络安全
云原生入门指南:Kubernetes和容器化技术云计算与网络安全:技术融合的新篇章
【8月更文挑战第30天】在云计算的浪潮中,云原生技术如Kubernetes已成为现代软件部署的核心。本文将引导读者理解云原生的基本概念,探索Kubernetes如何管理容器化应用,并展示如何通过实践加深理解。
|
27天前
|
Java 开发者
奇迹时刻!探索 Java 多线程的奇幻之旅:Thread 类和 Runnable 接口的惊人对决
【8月更文挑战第13天】Java的多线程特性能显著提升程序性能与响应性。本文通过示例代码详细解析了两种核心实现方式:Thread类与Runnable接口。Thread类适用于简单场景,直接定义线程行为;Runnable接口则更适合复杂的项目结构,尤其在需要继承其他类时,能保持代码的清晰与模块化。理解两者差异有助于开发者在实际应用中做出合理选择,构建高效稳定的多线程程序。
42 7