Java源码解析HashMap的keySet()方法

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方法的断点会生效,可以正常断点调试。

总结

以上就是这篇文章的全部内容了,希望本文的内容对大家的学习或者工作具有一定的参考学习价值,谢谢大家对我们的支持。如果你想了解更多相关内容请查看下面相关链接

(0)

相关推荐

  • java在hashmap初始化时赋初值过程解析

    Java中的HashMap是一种常用的数据结构,一般用来做数据字典或者Hash查找的容器. 一般我们初始化并赋初值是这样做的: HashMap<String, Object> map = new HashMap<>(); map.put("name", "yanggb"); map.put("lover", "huangq"); 但是有时候我们会想在一个表达式中完成初始化并赋初值的操作: HashMap

  • Java并发系列之ConcurrentHashMap源码分析

    我们知道哈希表是一种非常高效的数据结构,设计优良的哈希函数可以使其上的增删改查操作达到O(1)级别.Java为我们提供了一个现成的哈希结构,那就是HashMap类,在前面的文章中我曾经介绍过HashMap类,知道它的所有方法都未进行同步,因此在多线程环境中是不安全的.为此,Java为我们提供了另外一个HashTable类,它对于多线程同步的处理非常简单粗暴,那就是在HashMap的基础上对其所有方法都使用synchronized关键字进行加锁.这种方法虽然简单,但导致了一个问题,那就是在同一时间

  • Java集合之HashMap用法详解

    本文实例讲述了Java集合之HashMap用法.分享给大家供大家参考,具体如下: HashMap是最常用的Map集合,它的键值对在存储时要根据键的哈希码来确定值放在哪里. HashMap 中作为键的对象必须重写Object的hashCode()方法和equals()方法 import java.util.Map; import java.util.HashMap; public class lzwCode { public static void main(String [] args) { M

  • Java源码解析HashMap的tableSizeFor函数

    aka,HashMap的容量大小必须为2的指数,即16,32,64,128这样的值.那么,在构造函数中,如果调用者指定了HashMap的初始大小不是2的指数,那么,HashMap的tableSizeFor函数,会计算一个大于或等于给定参数的2的指数的值.先来看一下tableSizeFor函数的源码,如下 /** * Returns a power of two size for the given target capacity. **/ static final int tableSizeFo

  • Java源码解析ConcurrentHashMap的初始化

    首先看一下代码 private final Node<K,V>[] initTable() { Node<K,V>[] tab; int sc; while ((tab = table) == null || tab.length == 0) { // 第一次检查 if ((sc = sizeCtl) < 0) Thread.yield(); // lost initialization race; just spin else if (U.compareAndSwapInt

  • java中ConcurrentHashMap的读操作为什么不需要加锁

    前言 ConcurrentHashMap是Java 5中支持高并发.高吞吐量的线程安全HashMap实现. 我们知道,ConcurrentHashmap(1.8)这个并发集合框架是线程安全的,当你看到源码的get操作时,会发现get操作全程是没有加任何锁的,这也是这篇博文讨论的问题--为什么它不需要加锁呢? 下面话不多说了,来一起看看详细的介绍吧 ConcurrentHashMap的简介 我想有基础的同学知道在jdk1.7中是采用Segment + HashEntry + ReentrantLo

  • AngularJS操作键值对象类似java的hashmap(填坑小结)

    前言: 我们知道java的hashmap中使用最多的是put(...),get(...)以及remove()方法,那么在angularJS中如何创造(使用)这样一个对象呢 思路分析: 我们知道在java中可以采用链式访问和"[]"访问hashmap的某一个值 具体实现: 链式访问: .factory('ParamsServices', function () { var params = {}; return { get: function (key) { return params.

  • Java编程WeakHashMap实例解析

    简述: <Thinking in Java>第4版 P519 页 WeakHashMap一章读书笔记 WeakHashMap 用来保存WeakReference,这一结构云逊垃圾回收器自动清理键和值 在添加键和值的操作时,映射会自动使用WeakReference包装它们, 见jdk源代码, public V put(K key, V value) { Object k = maskNull(key); int h = hash(k); Entry<K,V>[] tab = getT

  • Java源码解析HashMap的keySet()方法

    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

  • Java源码解析HashMap简介

    本文基于jdk1.8进行分析 HashMap是java开发中可以说必然会用到的一个集合.本文就HashMap的源码实现进行分析. 首先看一下源码中类的javadoc注释对HashMap的解释.如下图.HashMap是对Map接口的基于hash表的实现.这个实现提供了map的所有可选操作,并且允许null值(可以多个)和一个null的key(仅限一个).HashMap和HashTable十分相似,除了HashMap是非同步的且允许null元素.这个类不保证map里的顺序,更进一步,随着时间的推移,

  • Java源码解析HashMap成员变量

    本文基于jdk1.8进行分析 关于HashMap的简介,可以参考这篇文章https://www.jb51.net/article/154177.htm. 首先看一下HashMap的一些静态常量.第一个是DEFAULT_INITIAL_CAPACITY,默认初始大小,16.从注释中可以了解到,大小必须为2的指数.这里的16,采用的1左移4位实现.而"aka",是as known as的缩写. /** * The default initial capacity - MUST be a p

  • Java源码解析HashMap的resize函数

    HashMap的resize函数,用于对HashMap初始化或者扩容. 首先看一下该函数的注释,如下图.从注释中可以看到,该函数的作用是初始化或者使table的size翻倍.如果table是null,那么就申请空间进行初始化.否则,因为我们在使用2的指数的扩张,在原来table的每个位置的元素,在新的table中,他们要么待在原来的位置,要么移动2的指数的偏移.从这里可以看出,扩容前table每个位置上如果有多个元素,元素之间组成链表时,在扩容后,该链表中的元素,有一部分会待在原地,剩下的元素会

  • Java源码解析之HashMap的put、resize方法详解

    一.HashMap 简介 HashMap 底层采用哈希表结构 数组加链表加红黑树实现,允许储存null键和null值 数组优点:通过数组下标可以快速实现对数组元素的访问,效率高 链表优点:插入或删除数据不需要移动元素,只需要修改节点引用效率高 二.源码分析 2.1 继承和实现 public class HashMap<K,V> extends AbstractMap<K,V> implements Map<K,V>, Cloneable, Serializable {

  • Java源码解析之超级接口Map

    前言 我们在前面说到的无论是链表还是数组,都有自己的优缺点,数组查询速度很快而插入很慢,链表在插入时表现优秀但查询无力.哈希表则整合了数组与链表的优点,能在插入和查找等方面都有不错的速度.我们之后要分析的HashMap就是基于哈希表实现的,不过在JDK1.8中还引入了红黑树,其性能进一步提升了. 今天我们来说一说超级接口Map. 一.接口Map Map是基于Key-Value的数据格式,并且key值不能重复,每个key对应的value值唯一.Map的key也可以为null,但不可重复. 在看Ma

  • Java源码解析之LinkedHashMap

    一.成员变量 先来看看存储元素的结构吧: static class Entry<K,V> extends HashMap.Node<K,V> { Entry<K,V> before, after; Entry(int hash, K key, V value, Node<K,V> next) { super(hash, key, value, next); } } 这个Entry在HashMap中被引用过,主要是为了能让LinkedHashMap也支持树化.

  • Java源码解析之ConcurrentHashMap

    早期 ConcurrentHashMap,其实现是基于: 分离锁,也就是将内部进行分段(Segment),里面则是 HashEntry 的数组,和 HashMap 类似,哈希相同的条目也是以链表形式存放. HashEntry 内部使用 volatile 的 value 字段来保证可见性,也利用了不可变对象的机制以改进利用 Unsafe 提供的底层能力,比如 volatile access,去直接完成部分操作,以最优化性能,毕竟 Unsafe 中的很多操作都是 JVM intrinsic 优化过的

  • Java源码解析之详解ImmutableMap

    一.案例场景 遇到过这样的场景,在定义一个static修饰的Map时,使用了大量的put()方法赋值,就类似这样-- public static final Map<String,String> dayMap= new HashMap<>(); static { dayMap.put("Monday","今天上英语课"); dayMap.put("Tuesday","今天上语文课"); dayMap.p

随机推荐