HashMap的遍历方式以及性能

本文将介绍HashMap常见的遍历方式、性能对比以及产生性能差异的原因。

遍历方式

HashMap的遍历方式主要有四种:

一、entrySet

获得一个包含entry的Set集合效率最高

Map<String, Integer> map = new HashMap<String, Integer>();
for (Entry<String, Integer> entry : map.entrySet()) {
    
    
    entry.getKey();
    entry.getValue();
}

二、keySet

获得一个包含key的Set集合

Map<String, Integer> map = new HashMap<String, Integer>();
for (String key : map.keySet()) {
    
    
    map.get(key);
}

三、Iterator:显示调用entrySet()的迭代器

迭代器来遍历HashMap,游标每次移动都会获得一个entry<key ,value>

Iterator<Map.Entry<String, Integer>> iterator = map.entrySet().iterator();
while (iterator.hasNext()) {
    
    
    entry.getKey();
    entry.getValue();
}

四、foreach

借助了Java8的新特性lambda表达式

map.forEach((k,v)-> System.out.print(k+v));

性能测试

对比这四种遍历方式在Map容量10万、100万、1000万的情况下的耗时

size 10万 100万 1000万
entrySet 18ms 55ms 511ms
keySet 24ms 116ms 1484ms
Iterator entrySet 12ms 75ms 684ms
foreach 560ms 3483ms >>3483ms

测试用的代码

public class TestHashMap {
    
    

    private static final int size=1000000;
    public static void main(String[] args) {
    
    
        Map<String,Integer> map=new HashMap<>();
        for(int i=0;i<size;i++){
    
    
            String uuid= UUID.randomUUID().toString();
            map.put(uuid,i);
        }
        long startTime=System.currentTimeMillis();
        map.forEach((k,v)-> System.out.print(k+v));
        /*
        for (Map.Entry<String, Integer> entry : map.entrySet()) {
            entry.getKey();
            entry.getValue();
        }
        */
        long endTime=System.currentTimeMillis();
        System.out.println((endTime-startTime));
    }
}

从上表可以看出,性能从大到小排序:
entrySet >= Iterator >keySet > foreach

分析一下其中的原因

entrySet源码

final class EntryIterator extends HashIterator implements Iterator<Map.Entry<K,V>> {
    
    
    public final Map.Entry<K,V> next() {
    
     return nextNode(); }
}

keySet源码

final class KeyIterator extends HashIterator implements Iterator<K> {
    
    
    public final K next() {
    
     return nextNode().key; }
}

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) {
    
    

/*
	遍历数组里面的链表,HashMap是数组+链表的结构;
	如果当前table[index]的链表遍历完了之后,next移到table[index++]的位置,也就是下一条链表的开头
*/
        do {
    
    } while (index < t.length && (next = t[index++]) == null);
    }
    return e;
}

性能entrySet > keySet的原因:
entrySet和keySet遍历HashMap都是通过nextNode()方法来遍历,一个table[index]对应一条链表(数组+链表结构)。但是keySet要获得value的话,需要额外去调用get()方法 。代码如下:

public V get(Object key) {
    
    
    Node<K,V> e;
    return (e = getNode(hash(key), key)) == null ? null : e.value;
}

final Node<K,V> getNode(int hash, Object key) {
    
    
    Node<K,V>[] tab; Node<K,V> first, e; int n; K k;
    //找到指定的桶,如果有hash冲突的话,table[index] 对应的就是一条链表或者树
    if ((tab = table) != null && (n = tab.length) > 0 &&(first = tab[(n - 1) & hash]) != null) {
    
    
        if (first.hash == hash && // always check first node
                ((k = first.key) == key || (key != null && key.equals(k))))
                return first;
        if ((e = first.next) != null) {
    
    
           if (first instanceof TreeNode)
                    return ((TreeNode<K,V>)first).getTreeNode(hash, key);
           do {
    
    
               if (e.hash == hash &&((k = e.key) == key || (key != null && key.equals(k))))
                  return e;
           } while ((e = e.next) != null);
        }
     }
     return null;
}

keySet()获取values的流程是:

  1. 先定位到具体的桶table[index]
  2. 再继续在对应的链表上循环遍历比较(如果是链表结点)
  3. 如果是树节点,则按照对半查找去遍历比较获取目标结点。

若hash散列算法较差造成hash冲突严重的时候(具体表现为一个table[index]对应的链表过长或红黑树的深度较深),会导致 keySet() 更加耗时。

map.forEach((k,v)-> System.out.print(k+v))的性能最差,应该是因为lambda表达式本身执行性能就不高( lambda表达式本身是为了编写的效率而考量的,而不是执行的效率 ),即使它的源码是按照entrySet()的方式遍历。

猜你喜欢

转载自blog.csdn.net/qq_44384533/article/details/108854045