HashMap中EntrySet和KeySet的比较
前言
阅读《阿里巴巴Java开发手册终极版v1.3.0》时,看到如下一句话:
【推荐】使用entrySet遍历Map类集合KV,而不是keySet方式进行遍历。
说明:keySet其实是遍历了2次,一次是转为Iterator对象,另一次是从hashMap中取出key所对应的value。而entrySet只是遍历了一次就把key和value都放到了entry中,效率更高。如果是JDK8,使用Map.foreach方法。
正例:values()返回的是V值集合,是一个list集合对象;keySet()返回的是K值集合,是一个Set集合对象;entrySet()返回的是K-V值组合集合。
心生好奇,便来探究为什么?
探究
有这样一个例子,HashMap里面存入400000个数据,来进行两种entrySet、keySet方式遍历,并且输出运行时间,例子如下所示:
package vip.wulang.springdatajpa;
import org.junit.Before;
import org.junit.Test;
import java.util.HashMap;
import java.util.Iterator;
import java.util.Map;
/**
* @author CoolerWu on 2018/11/11.
* @version 1.0
*/
public class HashMapTest {
private HashMap<String, String> map = new HashMap<>();
@Before
public void beforeAllMethodTestInClass() {
for (int i = 0; i < 100000; i++) {
map.put("a" + i, "aa" + i);
map.put("b" + i, "bb" + i);
map.put("c" + i, "cc" + i);
map.put("d" + i, "dd" + i);
}
}
@Test
public void entrySetTest() {
Iterator<Map.Entry<String, String>> it = map.entrySet().iterator();
long startTime = System.currentTimeMillis();
while (it.hasNext()) {
Map.Entry<String, String> entry = it.next();
System.out.println(entry.getKey() + "=" + entry.getValue());
}
long endTime = System.currentTimeMillis();
System.out.println(endTime - startTime);
}
@Test
public void keySetTest() {
Iterator<String> it = map.keySet().iterator();
long startTime = System.currentTimeMillis();
while (it.hasNext()) {
String key = it.next();
System.out.println(key + "=" + map.get(key));
}
long endTime = System.currentTimeMillis();
System.out.println(endTime - startTime);
}
}
多次测试,我们可以发现方法keySetTest()时间大约为2s809ms,而entrySetTest()只有2s98ms,从测试上来说,后者运行时间小于前者。查看HashMap.java源码,如下所示:
// 这是HashMap的KeyIterator、ValueIterator、EntryIterator的基本实现抽象类
abstract class HashIterator {
// 下一项返回
Node<K,V> next;
// 当前项
Node<K,V> current;
// fail-fast 机制是java集合(Collection)中的一种错误机制。
// 当多个线程对同一个集合的内容进行操作时,就可能会产生fail-fast事件。
// 例如:当某一个线程A通过iterator去遍历某集合的过程中,若该集合的内容被其他线程所改变了;
// 那么线程A访问集合时,就会抛出ConcurrentModificationException异常,产生fail-fast事件。
int expectedModCount;
// 当前的位置
int index;
// 无参构造函数
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;
}
// 获取下一项的Node节点
final Node<K,V> nextNode() {
Node<K,V>[] t;
Node<K,V> e = next;
// 如果modCount不等于expectedModCount,说明内容改变了,应执行 fail-fast 机制
if (modCount != expectedModCount)
throw new ConcurrentModificationException();
if (e == null)
throw new NoSuchElementException();
// 先将next的引用赋值给current,并去寻找新的next。
if ((next = (current = e).next) == null && (t = table) != null) {
// 不停地寻找每一个Node节点,直到Node节点具有数据,终止循环并赋值给next
do {} while (index < t.length && (next = t[index++]) == null);
}
return e;
}
// 移除Node节点数据
public final void remove() {
Node<K,V> p = current;
if (p == null)
throw new IllegalStateException();
// 如果modCount不等于expectedModCount,说明内容改变了,应执行 fail-fast 机制
if (modCount != expectedModCount)
throw new ConcurrentModificationException();
current = null;
K key = p.key;
// 调用HashMap.java里面的方法,删除该Node节点p
removeNode(hash(key), key, null, false, false);
// 把变化后的modCount赋值给expectedModCount
expectedModCount = modCount;
}
}
// KeySet的iterator()方法的返回类
final class KeyIterator extends HashIterator
implements Iterator<K> {
public final K next() { return nextNode().key; }
}
// Values的iterator()方法的返回类
final class ValueIterator extends HashIterator
implements Iterator<V> {
public final V next() { return nextNode().value; }
}
// EntrySet的iterator()方法的返回类
final class EntryIterator extends HashIterator
implements Iterator<Map.Entry<K,V>> {
public final Map.Entry<K,V> next() { return nextNode(); }
}
我们差不多已经把主要的步骤都梳理清楚了,那么为什么entrySet遍历的时间 < keySet遍历的时间?
因为entrySet遍历的时候,存放的是Map.Entry<T, T>类型,意思是,在进行遍历的时候已经把key、value放入其中。而keySet遍历的时候,存放的是T类型,意思是,在进行遍历的时候只放了key值,倘若我还需要value,就还需要使用 public V get(Object key) 方法,而这个方法具体实现如下:
public V get(Object key) {
Node<K,V> e;
return (e = getNode(hash(key), key)) == null ? null : e.value;
}
重点来了,请看这一句 getNode(hash(key), key) ,而这个方法的源码如下:
// 该方法是根据hash值和key值来查找的
final Node<K,V> getNode(int hash, Object key) {
Node<K,V>[] tab; Node<K,V> first, e; int n; K k;
if ((tab = table) != null && (n = tab.length) > 0 && (first = tab[(n - 1) & hash]) != null) {
if (first.hash == hash && ((k = first.key) == key || (key != null && key.equals(k))))
return first;
// 判断first.next是否为空指针
if ((e = first.next) != null) {
// 判断first是否属于红黑树
if (first instanceof TreeNode)
return ((TreeNode<K,V>)first).getTreeNode(hash, key);
// 若first不是红黑树,则进行链表的遍历,直到找到为止
do {
if (e.hash == hash &&
((k = e.key) == key || (key != null && key.equals(k))))
return e;
} while ((e = e.next) != null);
}
}
return null;
}
由上可知,还进行了一次遍历。所以keySet遍历的时间会 > entrySet遍历的时间。推荐使用entrySet遍历Map类集合KV,而不是keySet方式进行遍历。