HashMap中推荐使用entrySet方式遍历Map类集合KV而不是keySet方式遍历

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类型,意思是,在进行遍历的时候已经把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方式进行遍历。

你可能感兴趣的:(JDK源码,java)