场景:1.打断点会看到数据,map调用entrySet的时候什么时候把数据存入Set中的
2.看源码new EntrySet,里面有迭代器,但是这个迭代器什么时候调用的?根据迭代器的next调用了nextNode()里面有对应的节点处理
3.根据 第2条,猜测是调的迭代器依次遍历的,那第1条疑问就更重了
4.以上看到的是否是视图,那视图是什么map数据的引用?
5.foreach的时候调用的迭代器还是entrySet已经调用拿到了???
以下转载自:https://blog.csdn.net/NumberOneStudent/article/details/101668704,并实际验证过确实那样
原理解析
首先看keySet()的使用:
Map<String, Integer> map = new HashMap<String, Integer>(); map.put("a", 1); map.put("b", 2); map.put("c", 3); Set<String> ks = map.keySet(); for (String s: ks){ System.out.println(s); }
再看HashMap中的keySet()方法:
public Set<K> keySet() { Set<K> ks = keySet; return (ks != null ? ks : (keySet = new KeySet())); }
而且keySet成员初始为null,且并没有在构造函数中初始化过
transient volatile Set<K> keySet = null;
所以初次调用keySet()方法时会new KeySet(),而KeySet()是一个内部类
private final class KeySet extends AbstractSet<K> { public Iterator<K> iterator() { return newKeyIterator(); } public int size() { return size; } public boolean contains(Object o) { return containsKey(o); } public boolean remove(Object o) { return HashMap.this.removeEntryForKey(o) != null; } public void clear() { HashMap.this.clear(); } }
这个时候我们只是新建了一个KeySet 内部类对象,并没有调用其他方法,而且内部类KeySet 的父类无参构造函数也并没有做啥,那么问题来了,我们是怎么获取的HashMap中的key值的,之前对这个问题一直没明白。
其实,Set ks = map.keySet(); 这里的 ks 就仅仅只是一个Set引用,指向HashMap内部类KeySet的一个实例,重点在于该实例拥有自己的迭代器,当我们在使用增强for循环时才会调用该迭代器,也才会输出我们想要的东西。
现在看看迭代器是如何工作的:
private final class KeySet extends AbstractSet<K> { public Iterator<K> iterator() { return newKeyIterator(); } }
newKeyIterator()是指向外部类的一个函数
Iterator<K> newKeyIterator() { return new KeyIterator(); }
现在又指向创建的另一个内部类对象KeyIterator(),该类继承HashIterator也是HashMap的内部类
private final class KeyIterator extends HashIterator<K> { public K next() { return nextEntry().getKey(); } }
当我们在增强for循环时会调用该next()方法,它指向的是nextEntry().getKey(),Entry中不仅存放了key,value,也存放了next,指向下一个Entry对象,我们知道,HashMap的数据层实现是数组+链表,nextEntry会先遍历链表,然后再继续遍历下一个数组位置的链表,直至全部遍历完成,其部分源码如下:
if ((next = e.next) == null) { Entry[] t = table; while (index < t.length && (next = t[index++]) == null) ; }
ohhhhhhh, 我们知道了其实是在遍历 table
总结:keySet()方法返回一个内部引用,并指向一个内部类对象,该内部类重写了迭代器方法,当在增强for循环时才调用,并从外部类的table中取值。
同理: entrySet() 的使用
断点调试的思考
public class Main { public static void main(String[] args) { HashMap hashMap = new HashMap(); hashMap.put(1,2); hashMap.put(3,4); Set set = hashMap.entrySet(); } }
就以上面的思路我进行了断点调试,发现一初始化就会将entrySet() 中的 entrySet 初始化成功
但是明明没有对entrySet 赋值的语句啊?
原因: 在对某个对象进行 add to watch 实际上调用该对象的 toString 方法。
class T { private String name; public T() { } //测试断点的情况下是否会调用 toString() 方法 @Override public String toString() { this.name = "abc"; System.out.println("输出了:" + name); return super.toString(); } } main()方法 System.out.println("start"); T t = new T(); System.out.println("end");
没有断点的情况的输出结果:
System.out: start
System.out: end
断点的情况的输出结果:
System.out: start
System.out: 输出了:abc
System.out: end
在这里可以得出结论就是对一个对象打断点会执行该对象的 toString() 方法。同理在我们给 entries 打断点时可以看的返回的 EntrySet 集合是有数据的,可以知道这个数据肯定就是在 EntrySet 中的 toString() 方法获取的,查阅源码可以知道, EntrySet 并没有实现 toString 方法,而是在祖父类 AbstractCollection 实现了,代码如下:
public String toString() { //实际上就是获取 Iterator 对象,然后不断地调用 next() 方法而得到的值。 Iterator<E> it = iterator(); if (! it.hasNext()) return "[]"; StringBuilder sb = new StringBuilder(); sb.append('['); for (;;) { E e = it.next(); sb.append(e == this ? "(this Collection)" : e); if (! it.hasNext()) return sb.append(']').toString(); sb.append(',').append(' '); } }
结论:在获取 EntrySet 之后,我们一般调用 iterator() 获取 Iterator 对象,然后不断的调用 next() 方法,实际上这个才是真正获取数据的地方。它内部遍历 table 数组来获取指定的数据。