Java源码解析HashMap的keySet()方法
作者:李灿辉 发布时间:2023-11-11 06:33:05
HashMap的keySet()
方法比较简单,作用是获取HashMap中的key的集合。虽然这个方法十分简单,似乎没有什么可供分析的,但真正看了源码,发现自己还是有很多不懂的地方。下面是keySet的代码。
public Set<K> keySet() {
Set<K> ks = keySet;
if (ks == null) {
ks = new KeySet();
keySet = ks;
}
return ks;
}
从代码中了解到,第一次调用keySet方法时,keySet属性是null,然后进行了初始化,再将keySet属性返回。也就是说,HashMap里并不会随着put和remove的进行也维护一个keySet集合,而是在第一次调用keySet方法时,才给keySet属性初始化。
按照自己以往的理解,以为keySet返回的是一个集合,集合里面保存了HashMap的所有的Key。因为有了中先入为主的印象,所以读源码时,才感觉源码很奇怪。从源码中可以看到,初始化时,只是创建了一个KeySet类的对象,并没有把HashMap的key都加入进来,方法就返回了。除了自己以往的理解外,还有一个现象,让我坚信这时HashMap的key已经加入到keySet了,那就是在调试代码过程中IDE给出的调试信息。如下图。从图中可以看出,创建完成KeySet()后,调试信息就已经可以显示出,ks中有2个元素了。这个信息更加坚定了自己之前的理解。
那么,HashMap的key是什么时候加入到keySet集合中的呢?顺着这个思路,我进行了一步一步的分析。自己看了KeySet类的构造函数,发现只有默认构造函数。那么我想,如果没有在KeySet构造函数里把HashMap的key加入进来,那么就有可能是在KeySet的父类的构造函数中加入进来的。然后,自己找遍了KeySet类的父类的构造函数,发现都是空实现,并没有任何加入HashMap的key的操作。这到底是怎么回事呢?
其实HashMap的key并没有加入到keySet集合中,而是在遍历的时候,使用迭代器对key进行的遍历。这是结论。下面我们看一下原因和过程。
首先看一下KeySet类的代码,如下图。可以看到,KeySet类中的迭代器函数,返回的是一个KeyIterator类的对象。它的next方法返回的是HashIterator的nextNode的key。也就是说,当使用迭代器遍历set内的元素时,KeySet类的迭代器,会保证能够依次获取到HashMap的节点的key值,这就是我们遍历keySet的过程的实质。
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 (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();
}
}
}
final class KeyIterator extends HashIterator
implements Iterator<K> {
public final K next() { return nextNode().key; }
}
abstract class HashIterator {
Node<K,V> next; // next entry to return
Node<K,V> current; // current entry
int expectedModCount; // for fast-fail
int index; // current slot
HashIterator() {
expectedModCount = modCount;
Node<K,V>[] t = table;
current = next = null;
index = 0;
if (t != null && size > 0) { // advance to first entry
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;
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;
}
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;
}
}
那么,这里我们可以思考这么一个问题。通过HashMap的keySet获取到keySet后,难道只能用迭代器遍历吗?keySet方法不把HashMap的key都加入到set中,那么调用者使用for(int i = 0; i < size; i ++)的方式遍历时,岂不是无法遍历set中的key了吗?是的,确实是的。keySet确实没有把key加入到set中,另外,它不用担心调用者用for(int i = 0; i < size; i ++)的方式遍历时获取不到key,因为set根本就没有set.get(i)这样类似的方法,要想遍历set,只能用迭代器,或者使用foreach方式(本质还是迭代器)。
这里还有个问题需要解释,就是在调试代码时,既然key没有加入到set中,那么IDE如何显示出set中有2个元素这样的信息的?原来,IDE显示对象信息时,会调用对象的toString方法。而集合的toString方法就是显示出集合中的元素个数。
这里再思考一步,如果我们在集合的toString方法加上断点,那么IDE显示对象信息时,会不先停下来?答案是看情况。记得早些年间使用eclipse调试代码时,在toString方法加上断点后,显示对象信息时确实会停下来。然而我现在使用的是IDE是idea,idea在这一点上做了优化。如果是IDE显示对象信息调用的toString方法,那么toString方法的断点会被跳过,即不生效,但会给出一条提示信息,如下图。如果程序员主动调用对象的toString方法,那么,toString方法的断点会生效,可以正常断点调试。
来源:https://blog.csdn.net/li_canhui/article/details/85051250


猜你喜欢
- 简介:接上文实现对FTP的传送文件,此文和上文可以说是如出一辙,不过此文是通过cmd进行建立连接的,建立连接后也是通过以下几个步骤实现操作。
- 谨记:Url表只储存受保护的资源,不在表里的资源说明不受保护,任何人都可以访问1、MyFilterInvocationSecurityMet
- 一. 为什么需要比较对象上一节介绍了优先级队列,在优先级队列中插入的元素必须能比较大小,如果不能比较大小,如插入两个学生类型的元素,会报Cl
- HttpClient使用post方法提交数据 源代码:package post;import Java.io.IOException;imp
- postman测试传入List<String>参数第一步:设置headersContent-type 的值为applicatio
- 前言最近公司要把百度地图集成的项目中,于是我就研究了一天百度地图的SDK,当前的版本:Android SDK v3.0.0 。 虽然百度地图
- 继承什么是继承呢?继承(Inheritance)是一种联结类与类的层次模型。指的是一个类(称为子类、子接口)继承另外的一个类(称为父类、父接
- 前言在工作中经常遇到C#数组、ArrayList、List、Dictionary存取数据,但是该选择哪种类型进行存储数据,对于初学者的我一直
- 1 概述Java虚拟机把描述类的数据从Class文件加载到内存, 并对数据进行校验、转化解析和初始化,最终形成可以被虚拟机直接使用的Java
- 泛型泛型的语法定义class 类名 <泛型标识,泛型标识,…>{ private 泛型标识1,变量名;常用
- 本文实例讲述了C#判断一个矩阵是否为对称矩阵及反称矩阵的方法。分享给大家供大家参考。具体如下:1.判断对称矩阵对任意i和j,有a[i,j]=
- 最近在做一个项目,需要用到非对称加密,但是出现一个很诡异的情况,本地开发环境是Windows环境,测试环境是Linux环境,出现一个问题,
- (一)什么是微服务网关后端写完所有的微服务之后,最终是要交给前端去调用。我们都知道每个微服务都有各自的端口号,如果前端直接通过IP加端口的方
- 在Java中,泛型的引入是为了在编译时提供强类型检查和支持泛型编程。为了实现泛型,Java编译器应用类型擦除实现: &
- eclipse汉化包我们会经常用到,因为它可以帮助英语基础差的用户更快的掌握这个软件,非常方便,但是我看了下整个互联网上并没有对这个汉化包进
- 系统启动过程图: Framework层所有的Service都是运行在SystemServer进程中;SystemServer进程
- 目录卡顿原理卡顿监控ANR原理卡顿原理主线程有耗时操作会导致卡顿,卡顿超过阀值,触发ANR。 应用进程启动时候,Zygote会反射调用Act
- 归并排序是利用递归和分而治之的技术将数据序列划分成为越来越小的半子表,再对半子表排序,最后再用递归步骤将排好序的半子表合并成为越来越大的有序
- 摘要:其实两种方法归结起来看还是一种,都是利用Thread的构造器进行创建,区别就是一种是无参的,一种是有参的。一、继承Thread线程类:
- 前言我们之前介绍了不少有关动画的篇章。前面介绍的动画都是只有一个动画效果,那如果我们想对某个组件实现一组动效,比如下面的效果,该怎么办?st