前言
这个问题在我分析HashMap源码时,困扰了我一段时间,在网上搜了很多文章,反复揣摩n次之后,终于直到为什么了.
如果你也困惑,不清楚entrySet和keySet数据的来源,为什么entrySet比keySet快,就请往下看.
entrySet
源码分析
1 | public Set<Map.Entry<K,V>> entrySet() { |
进入HashMap的这个方法,我们可以看到,首先去找成员变量entrySet
如果为空就创建一个EntrySet
对象,不为空直接进行返回
这个方法返回的是Set集合,里面放的是Map.Entry<K,V>
类型,
1 | static class Node<K,V> implements Map.Entry<K,V> { |
因为内部类Nodestatic class Node<K,V> implements Map.Entry<K,V>
实现了Map.Entry<K,V>
接口,
所以所Set集合中放的都是Node节点,Node里面封装了我们需要的数据,
我们知道了Set集合中放入的是什么了,我们再进入EntrySet
对象一探究竟,
我们entrySet()
方法返回的entrySet变量是通过new EntrySet()
得来的.
1 | final class EntrySet extends AbstractSet<Map.Entry<K,V>> { |
这里我对一些方法体进行了省略,我们可以看到EntrySet
对象是继承AbstractSet<Map.Entry<K,V>>
对象的,public abstract class AbstractSet<E> extends AbstractCollection<E> implements Set<E> {
根据继承的特点,父类AbstractSet<E>
实现了Set<E>
接口,我们可知子类EntrySet
也相当于实现了Set
接口,
这里的Set集合的size()
方法,返回的就是HashMap成员变量size,作用是记录数据量,里面有多少个Node节点.
遍历数据(for,while)
1 | HashMap<String,String> hashMap=new HashMap<>(); |
根据迭代器的原理,我们想遍历Set集合,需要先通过方法iterator()
获取迭代器,通过迭代器才能循环获取里面的数据,
而这里的iterator()
方法返回的是一个EntryIterator
对象,我们再继续往下走,一探究竟 ->
1 | final class EntryIterator extends HashIterator |
可以看出,它确实实现了迭代器Iterator
接口,而且接口封装了Map.Entry<K,V>
数据,也就是说迭代器所迭代的就是Node节点数据.EntryIterator
对象继承了HashIterator
对象,
根据继承性,创建EntryIterator
无参对象会先调用HashIterator
无参构造函数,
1 | abstract class HashIterator { |
我们忽略了它的一些方法,执行完了HashIterator
无参构造函数,我们知道这些是进行迭代器的数据初始化,
把next指针指向了下一个不为空的Node节点的下标,并把index+1指向下一个下标(用于下一次循环),把结构记录数记录下来(用于判断并发修改异常).
这时执行完iterator()
方法返回一个EntryIterator
对象,也就是我们所需的迭代器,
1 | Set<Map.Entry<String, String>> entries = hashMap.entrySet(); |
我们初始化了迭代器(迭代器 = new EntryIterator()
),开始进行第一次循环,在第一次进入循环体{}中之前,
先判断迭代器是否可以循环,也就是调用迭代器的hasNext()
方法,返回true,才能进行第一次迭代,
我们上面分析了迭代器对象就是new EntryIterator()
对象,在EntryIterator
中没有hasNext()
方法,
根据继承性,就往父类HashIterator
中找,找到hasNext()
方法,
1 | public final boolean hasNext() { |
如果next
指向的下一个节点不为空,就继续往下执行,调用迭代器的next()
方法,获取第一次迭代的Node数据,
(这里会不会有人不知道为什么迭代的是Node数据? 因为上面我们说了EntryIterator
实现了Iterator<Map.Entry<K,V>>
,而迭代器Iterator
接口中封装了Map.Entry<K,V>
,也就是封装了Node节点,所以迭代的就是Node数据.)
此时执行了EntryIterator
类中的public final Map.Entry<K,V> next() { return nextNode(); }
方法,
而nextNode()
方法是在父类HashIterator
中的,
1 | final Node<K,V> nextNode() { |
主要是理解步骤3,它是对当前循环的位置是否有链表,或红黑树存在,而做出的处理.
这个返回的数据e
节点,会指向for (Map.Entry<String,String> e:entries)
中的循环变量e
,这时候才可以进入for的循环体{}中.
执行完循环体{},会再次判断hasNext()
方法,返回true,再执行next()
方法并把返回的Node节点给循环变量e
…
综上所知(下面两种循环的效果相同):
1 | HashMap<String,String> hashMap=new HashMap<>(); |
foreach循环
但是如果使用的是foreach
循环,
我们可知entrySet
是HashMap的全局变量,它的指针指向的是一个new EntrySet()
对象(迭代器),
使用entrySet.foreach()
就是调用EntrySet
类中的forEach(...)
方法.
1 | HashMap<String,String> hashMap=new HashMap<>(); |
1 | public final void forEach(Consumer<? super Map.Entry<K,V>> action) { |
由上我们可以看出,foreach和for循环的道理都是差不多的,可以说几乎相同,只不过方式不太一样,
for是调用了迭代器进行循环,其内部还是操作的HashMap的成员变量,
foreach则就是一个方法,直接进行成员变量的操作,更为简洁.
总结
由上我们可以知道entrySet()
方法返回的是Set
集合,这个Set
集合指向的EntrySet
对象,
而EntrySet
对象可以作为迭代器进行使用,从而实现遍历的效果,
其内部并没保存任何数据,操作的还是HashMap的成员变量(table数组
size大小
等数据),
遍历的Node节点里面封装了我们需要的数据(key,value),HashMap是排序是无序的,所以迭代器遍历起来也是无序的.
keySet
1 | public Set<K> keySet() { |
我粘贴出部分不同的地方,同理也可分析出ketSet()
获取,和迭代也是和entrySet()
是同样的,
不同的地方就在于它迭代的是Node节点的key值,而不是Node节点.
所以,如果你需要用到key和value,就用entrySet()
进行获取.
这样避免了获取到key,再使用HashMap的get(Object key)
方法去获取其对应的value所带来的损耗.
1 | HashMap<String, String> hashMap = new HashMap<>(); |
values
原理同上.
1 | public Collection<V> values() { |
总结
- 由上面分析,我们知道了实际上
entrySet()
keySet()
values()
,
这三个方法遍历的其实是用来维护HashMap的内部数组table中的数据. - 因为其插入是(散列)随机的,所以,遍历起来也是无序的.
- 其中
entrySet()
keySet()
是Set
视图,所以里面保存的都是不重复的数据,
而values()
则不同,它是Collection
视图,里面保存的数据是可重复的.