list 、set 、map 粗浅性能对比分析

 list 、set 、map 粗浅性能对比分析


 

不知道有多少同学和我一样,工作五年了还没有仔细看过list、set的源码可怜,一直停留在老师教导的:“LinkedList插入性能比ArrayList好,LinkedList顺序遍历性能比ArrayList好”的世界里。可是真是如此么?本文很“肤浅”的对比和分析了几种常用的集合,“高手”可以就此打住不必往下阅读了。。。


本文分开介绍了List、Map、Set:

(测试环境:win7、jdk、4G、i3;文章示例为了节省篇幅,只会列出测试大体形式和遍历次数)

第一部分:List

1.add(E e)

==============================性能测试=================================

(1)表格统计的差异在于new的方式不同:

[java] view plaincopy
  1. for (int i = 0; i < 10000; i++) {  
  2.     List<Integer> list = new LinkedList<Integer>();  
  3.     for (int j = 0; j < 10000; j++) {  
  4.         list.add(j);  
  5.     }  
  6.     list = null;  
  7. }  

new方式 消耗时间(毫秒)
new LinkedList<Integer>() 3420
new ArrayList<Integer>() 3048
new ArrayList<Integer>(10000) 2280
new Vector<Integer>()  4045
new Vector<Integer>(10000) 3859

(2)我们变化一下集合的长度和遍历次数:

[java] view plaincopy
  1. for (int i = 0; i < 1000000; i++) {  
  2.     List<Integer> list = new LinkedList<Integer>();  
  3.     for (int j = 0; j < 100; j++) {  
  4.         list.add(j);  
  5.     }  
  6.     list = null;  
  7. }  
new方式 消耗时间(毫秒)
new LinkedList<Integer>() 2286
new ArrayList<Integer>() 2832
new ArrayList<Integer>(10000) 1714
new Vector<Integer>()  3937
new Vector<Integer>(10000) 3184


*************************************************源码分析*************************************************

似乎并没有看到期待的差异,若非“冒充下大牛”纠结一下区别:

1、若集合长度比较小(小于百位),LinkedList比ArrayList的插入性能稍胜一筹。

2、但是当长度达到五位数时,ArrayList的插入性能反而会比LinkedList好一些。

3、若你能预知ArrayList的长度,可以完胜LinkedList。(原因后面会说)

4、Vector的作用不在一个纬度上,不过也没有想象中的那么差。


LinkedList.add:

[java] view plaincopy
  1.    先调用了自身的add:  
  2.    public boolean add(E e) {  
  3. addBefore(e, header);  
  4.        return true;  
  5.    }  
  6.    然后又调用了addBefore,表示插入在谁之前(有点绕,因为是双项链):  
  7.    private Entry<E> addBefore(E e, Entry<E> entry) {  
  8. Entry<E> newEntry = new Entry<E>(e, entry, entry.previous);  
  9. newEntry.previous.next = newEntry;  
  10. newEntry.next.previous = newEntry;  
  11. size++;  
  12. modCount++;  
  13. return newEntry;  
  14.    }  
  15.    用于维护指定前后关系:  
  16.    private static class Entry<E> {  
  17. E element;  
  18. Entry<E> next;  
  19. Entry<E> previous;  
  20.   
  21. Entry(E element, Entry<E> next, Entry<E> previous) {  
  22.     this.element = element;  
  23.     this.next = next;  
  24.     this.previous = previous;  
  25. }  
  26.    }  
也就是说LinkedList的上下级维护关系是借助了指针,若任意位置插入,则是先查找到before前的entry,然后重指原有位置前后元素的指针。


ArrayList.add:

首先判断下是否超过了最大长度数组长度,若超过会重新扩展拷贝(这也就说明了,为什么当我们在newArrayList时指定合适的长度会提升性能)

[java] view plaincopy
  1.    相比LinkedList来说就简单了狠多:  
  2.    public boolean add(E e) {  
  3. ensureCapacity(size + 1);  // Increments modCount!!  
  4. elementData[size++] = e;  
  5. return true;  
  6.    }  
  7.    //首先判断下是否超过了最大长度数组长度,若超过会重新扩展拷贝(这也就说明了,为什么当我们在newArrayList时指定合适的长度会提升性能)  
  8.    public void ensureCapacity(int minCapacity) {  
  9. modCount++;  
  10. int oldCapacity = elementData.length;  
  11. if (minCapacity > oldCapacity) {  
  12.     Object oldData[] = elementData;  
  13.     int newCapacity = (oldCapacity * 3)/2 + 1;  
  14.         if (newCapacity < minCapacity)  
  15.     newCapacity = minCapacity;  
  16.            // minCapacity is usually close to size, so this is a win:  
  17.            elementData = Arrays.copyOf(elementData, newCapacity);  
  18. }  
  19.    }  
  20.    //默认长度10  
  21.    public ArrayList() {  
  22. this(10);  
  23.    }  

通过上面的源码可以理解出为什么当集合的长度会左右LinkedList和ArrayList的插入性能PK。影响ArrayList性能损耗的一大元凶就是数组的不断增长拷贝,LinkedList由于自身的特性当数据插入时需要借助特殊的内部类“Entry”来维护插入数据的上下级链。


2.add(int index, E element)

通过上面的分析出现了一个想法,如果是

随机插入位置呢?

==============================性能测试=================================

(1)

[java] view plaincopy
  1. for (int i = 0; i < 100; i++) {  
  2.     List<Integer> list = new LinkedList<Integer>();  
  3.     for (int j = 0; j < 10000; j++) {  
  4.         list.add(list.size() / 2, j);  
  5.     }  
  6.     list = null;  
  7. }  
new方式 消耗时间
new LinkedList<Integer>() 15782
new ArrayList<Integer>() 3513
new ArrayList<Integer>(10000) 3490

(2)我们变化一下集合的长度和遍历次数:

[java] view plaincopy
  1. for (int i = 0; i < 100000; i++) {  
  2.     List<Integer> list = new LinkedList<Integer>();  
  3.     for (int j = 0; j < 100; j++) {  
  4.         list.add(list.size() / 2, j);  
  5.     }  
  6.     list = null;  
  7. }  
new方式 消耗时间
new LinkedList<Integer>() 880
new ArrayList<Integer>() 1262
new ArrayList<Integer>(10000) 2308

*************************************************源码分析*************************************************

从纯插入性能上来说,随机插入LikedList要比ArrayList性能好:(至于为什么当LinkedList集合长度为10000时性能变化会如此之大,我们后面的get会有介绍)

LikedList.add(int index, E element) :

大体分为两个步骤

[java] view plaincopy
  1. public void add(int index, E element) {  
  2.     addBefore(element, (index==size ? header : entry(index)));  
  3. }   
  4. //第一步获取到addBefore需要的插入位置(entry),第二步如上  
  5. private Entry<E> entry(int index) {  
  6.     if (index < 0 || index >= size)  
  7.         throw new IndexOutOfBoundsException("Index: "+index+  
  8.                                             ", Size: "+size);  
  9.     Entry<E> e = header;  
  10.     if (index < (size >> 1)) {  
  11.         for (int i = 0; i <= index; i++)  
  12.             e = e.next;  
  13.     } else {  
  14.         for (int i = size; i > index; i--)  
  15.             e = e.previous;  
  16.     }  
  17.     return e;  
  18. }  
所以从代码上看,LikedList.add(int index, E element) 没有理由在集合长度为10000时会有这么大的差异,除非entry(int index)存在问题(我们后面可能要说的hash也正是为了避免遍历)。


ArrayList.add(int index, E element) :

大体分为三步

第二步暴漏了为什么现在初始指定了集合长度反而不一定是好事

[java] view plaincopy
  1.    public void add(int index, E element) {  
  2. if (index > size || index < 0)  
  3.     throw new IndexOutOfBoundsException(  
  4.     "Index: "+index+", Size: "+size);  
  5. //第一步:扩充长度  
  6. ensureCapacity(size+1);  // Increments modCount!!  
  7. //第二步:挪动数据(这一步决定了为什么现在初始指定了集合长度反而不一定是好事)  
  8. System.arraycopy(elementData, index, elementData, index + 1,  
  9.          size - index);  
  10. //第三步:插入  
  11. elementData[index] = element;  
  12. size++;  
  13.    }  

3.get(int index)

==============================性能测试=================================

[java] view plaincopy
  1. List<Integer> list = new LinkedList<Integer>();  
  2. for (int j = 0; j < 10000; j++) {  
  3.     list.add(j);  
  4. }  
  5.   
  6. for (int i = 0; i < 100; i++) {  
  7.     for (int j = 0; j < 10000; j++) {  
  8.         list.get(j);  
  9.     }  
  10. }  
new方式 消耗时间
new LinkedList<Integer>() 8349
new ArrayList<Integer>(10000) 15
*************************************************源码分析*************************************************

通过这个表首先能够知道如果直接或间接(如模板语言)调用for循环遍历,LikedList的性能要差很多。

LinkedList.get(int index):

[java] view plaincopy
  1.     public E get(int index) {  
  2.         return entry(index).element;  
  3.     }  
  4.     //性能损耗在这里,每次都需要线性搜索(遍历链定位位置)  
  5.     private Entry<E> entry(int index) {  
  6.         if (index < 0 || index >= size)  
  7.             throw new IndexOutOfBoundsException("Index: "+index+  
  8.                                                 ", Size: "+size);  
  9.         Entry<E> e = header;  
  10.         if (index < (size >> 1)) {  
  11.             for (int i = 0; i <= index; i++)  
  12.                 e = e.next;  
  13.         } else {  
  14.             for (int i = size; i > index; i--)  
  15.                 e = e.previous;  
  16.         }  
  17.         return e;  
  18.     }  

ArrayList.get(int index):

[java] view plaincopy
  1.    public E get(int index) {  
  2. //检查是否越界  
  3. RangeCheck(index);  
  4. //直接从数组中取出对应index的数据  
  5. return (E) elementData[index];  
  6.    }  


4.迭代器

==============================性能测试=================================

[java] view plaincopy
  1. List<Integer> list = new LinkedList<Integer>();  
  2. for (int j = 0; j < 100; j++) {  
  3.     list.add(j);  
  4. }  
  5.   
  6. for (int i = 0; i < 1000000; i++) {  
  7.     Iterator<Integer> it = list.iterator();  
  8.     while (it.hasNext()) {  
  9.         it.next();  
  10.     }  
  11.     it = null;  
  12. }  
new方式 消耗时间
new LinkedList<Integer>() 1335
new ArrayList<Integer>(); 3940


*************************************************源码分析*************************************************

LinkedList.iterator():

[java] view plaincopy
  1.    private class ListItr implements ListIterator<E> {  
  2. //header为LikedList的成员变量Entry  
  3. private Entry<E> lastReturned = header;  
  4. private Entry<E> next;  
  5. private int nextIndex;  
  6. private int expectedModCount = modCount;  
  7.   
  8. //这里我们会默认传入0,  
  9. ListItr(int index) {  
  10.     if (index < 0 || index > size)  
  11.     throw new IndexOutOfBoundsException("Index: "+index+  
  12.                             ", Size: "+size);  
  13.   
  14.         //下面的if和else很有意思,为了实现“所有操作都是按照双重链接列表的需要执行的。在列表中编索引的操作将从开头或结尾遍历列表(从靠近指定索引的一端)。”  
  15.     //size为外部LikedList的成员变量  
  16.     if (index < (size >> 1)) {  
  17.     next = header.next;  
  18.     for (nextIndex=0; nextIndex<index; nextIndex++)  
  19.         next = next.next;  
  20.     } else {  
  21.     next = header;  
  22.     for (nextIndex=size; nextIndex>index; nextIndex--)  
  23.         next = next.previous;  
  24.     }  
  25. }  
  26.   
  27. //判断是否已经超过长度  
  28. public boolean hasNext() {  
  29.     return nextIndex != size;  
  30. }  
  31.   
  32. //便宜至下一个元素并返回element  
  33. public E next() {  
  34.     checkForComodification();  
  35.     if (nextIndex == size)  
  36.     throw new NoSuchElementException();  
  37.   
  38.     lastReturned = next;  
  39.     next = next.next;  
  40.     nextIndex++;  
  41.     return lastReturned.element;  
  42. }  
  43.   
  44. ...  
  45.   
  46. final void checkForComodification() {  
  47.     if (modCount != expectedModCount)  
  48.     throw new ConcurrentModificationException();  
  49. }  
  50.    }  
也就是说我们的LinkedList的Iterator性能损耗主要在变换指针,所以对比for遍历方式来说性能提升了很多。


ArrayList.iterator():

[java] view plaincopy
  1.    public Iterator<E> iterator() {  
  2. return new Itr();  
  3.    }  
  4.   
  5.    private class Itr implements Iterator<E> {  
  6. /** 
  7.  * Index of element to be returned by subsequent call to next. 
  8.  */  
  9. int cursor = 0;  
  10.   
  11. /** 
  12.  * Index of element returned by most recent call to next or 
  13.  * previous.  Reset to -1 if this element is deleted by a call 
  14.  * to remove. 
  15.  */  
  16. int lastRet = -1;  
  17.   
  18. /** 
  19.  * The modCount value that the iterator believes that the backing 
  20.  * List should have.  If this expectation is violated, the iterator 
  21.  * has detected concurrent modification. 
  22.  */  
  23. int expectedModCount = modCount;  
  24.   
  25. public boolean hasNext() {  
  26.            return cursor != size();  
  27. }  
  28.   
  29. public E next() {  
  30.            checkForComodification();  
  31.     try {  
  32.     //主要内容在这里,又绕回了ArrayList的获取方式  
  33.     E next = get(cursor);  
  34.     lastRet = cursor++;  
  35.     return next;  
  36.     } catch (IndexOutOfBoundsException e) {  
  37.     checkForComodification();  
  38.     throw new NoSuchElementException();  
  39.     }  
  40. }  
  41.   
  42.    }  
ArrayList“很懒”的没有做什么,主要借助了父类AbstractList,获取方式采用了模板方法设计模式(Template Method Pattern)。

总结:无论你遍历ArrayList还是LinkedList都可以尽量采用迭代器。


通过add和get的分析,我们最常用的场景就是单页数据获取,然后利用jstl或velocity等遍历展示:

1、如果能确定遍历采用的for循环建议使用ArrayList并采用带参的构造器指定集合长度(也就是每页的个数);

2、若遍历采用迭代器建议采用LinkedList,因为我们还会有时对dao获取的数据采用缓存策略。

(jstl用的迭代器,可看源码org.apache.taglibs.standard.tag.common.core.ForEachSupport.toForEachIterator方法)


第二部分:Map


1.put(K key, V value)

==============================性能测试=================================

表格统计的差异在于new的方式不同:

[java] view plaincopy
  1. for (int i = 0; i < 100000; i++) {  
  2.     Map<Integer, String> map = new TreeMap<Integer, String>();  
  3.     for (int j = 0; j < 100; j++) {  
  4.         map.put(j, "s" + j);  
  5.     }  
  6.     map = null;  
  7. }  
new方式 运行时间
new TreeMap<Integer, String>() 2945
new HashMap<Integer, String>() 2029
new HashMap<Integer, String>(100) 1845
*************************************************源码分析*************************************************

TreeMap.put(K key, V value):

首先确认一点TreeMap采用的“红黑树”二叉查找方式。(具体算法请自行google)

该映射根据其键的自然顺序进行排序,或者根据创建映射时提供的 Comparator 进行排序,具体取决于使用的构造方法。此实现为 containsKey、get、put 和 remove 操作提供受保证的 log(n) 时间开销。

插入大体分为四步:

1、判断是否为初次;

2、获取指定Comparator,若没有指定获取key的父类的实现方式。

3、利用Comparator确定数据存放位置;

4、树的旋转。

[java] view plaincopy
  1.    public V put(K key, V value) {  
  2.        Entry<K,V> t = root;  
  3. //判断是否存在根节点,或者集合是否存在了数据  
  4.        if (t == null) {  
  5.            root = new Entry<K,V>(key, value, null);  
  6.            size = 1;  
  7.            modCount++;  
  8.            return null;  
  9.        }  
  10.        int cmp;  
  11.        Entry<K,V> parent;  
  12.        // split comparator and comparable paths  
  13. //这点是获取构造器传入的Comparator  
  14.        Comparator<? super K> cpr = comparator;  
  15.        if (cpr != null) {  
  16.            do {  
  17.                parent = t;  
  18.                cmp = cpr.compare(key, t.key);  
  19.                if (cmp < 0)  
  20.                    t = t.left;  
  21.                else if (cmp > 0)  
  22.                    t = t.right;  
  23.                else  
  24.                    return t.setValue(value);  
  25.            } while (t != null);  
  26.        }  
  27. //若没有指定Comparator,查找父类实现  
  28.        else {  
  29.            if (key == null)  
  30.                throw new NullPointerException();  
  31.            Comparable<? super K> k = (Comparable<? super K>) key;  
  32.     //遍历放入确切位置  
  33.            do {  
  34.                parent = t;  
  35.                cmp = k.compareTo(t.key);  
  36.                if (cmp < 0)  
  37.                    t = t.left;  
  38.                else if (cmp > 0)  
  39.                    t = t.right;  
  40.                else  
  41.                    return t.setValue(value);  
  42.            } while (t != null);  
  43.        }  
  44.        Entry<K,V> e = new Entry<K,V>(key, value, parent);  
  45.        if (cmp < 0)  
  46.            parent.left = e;  
  47.        else  
  48.            parent.right = e;  
  49. //树的旋转  
  50.        fixAfterInsertion(e);  
  51.        size++;  
  52.        modCount++;  
  53.        return null;  
  54.    }  

HashMap.put(K key, V value):

1、HashMap 的实例有两个参数影响其性能:初始容量 和加载因子。容量 是哈希表中桶的数量,初始容量只是哈希表在创建时的容量。加载因子 是哈希表在其容量自动增加之前可以达到多满的一种尺度。当哈希表中的条目数超出了加载因子与当前容量的乘积时,则要对该哈希表进行 rehash 操作(即重建内部数据结构),从而哈希表将具有大约两倍的桶数;

2、key的hashcode至关重要。

[java] view plaincopy
  1.    //构造一个带指定初始容量和加载因子的空 HashMap  
  2.    public HashMap(int initialCapacity, float loadFactor) {  
  3.        if (initialCapacity < 0)  
  4.            throw new IllegalArgumentException("Illegal initial capacity: " +  
  5.                                               initialCapacity);  
  6.        if (initialCapacity > MAXIMUM_CAPACITY)  
  7.            initialCapacity = MAXIMUM_CAPACITY;  
  8.        if (loadFactor <= 0 || Float.isNaN(loadFactor))  
  9.            throw new IllegalArgumentException("Illegal load factor: " +  
  10.                                               loadFactor);  
  11.   
  12.        // Find a power of 2 >= initialCapacity  
  13.        int capacity = 1;  
  14. //取大于等于初始容量最近的2的倍数  
  15.        while (capacity < initialCapacity)  
  16.            capacity <<= 1;  
  17.   
  18.        this.loadFactor = loadFactor;  
  19.        threshold = (int)(capacity * loadFactor);  
  20.        table = new Entry[capacity];  
  21.        init();  
  22.    }  
  23.    public V put(K key, V value) {  
  24.        if (key == null)  
  25.            return putForNullKey(value);  
  26. //计算key的hash值  
  27.        int hash = hash(key.hashCode());  
  28. //计算出在第几个“水桶”,计算方式是&与了table.length-1,这应该也是水桶数据是2的倍数的原因  
  29.        int i = indexFor(hash, table.length);  
  30. //通过这步可以理解为什么看到好多文档要求key的hashCode“均匀分布”便于均匀落入各个水桶  
  31.        for (Entry<K,V> e = table[i]; e != null; e = e.next) {  
  32.            Object k;  
  33.            if (e.hash == hash && ((k = e.key) == key || key.equals(k))) {  
  34.                V oldValue = e.value;  
  35.                e.value = value;  
  36.                e.recordAccess(this);  
  37.                return oldValue;  
  38.            }  
  39.        }  
  40.   
  41.        modCount++;  
  42.        addEntry(hash, key, value, i);  
  43.        return null;  
  44.    }  

2.get

==============================性能测试=================================

(1)表格统计的差异在于new的方式不同:

[java] view plaincopy
  1. Map<Integer, String> map = new TreeMap<Integer, String>();  
  2. for (int j = 0; j < 100; j++) {  
  3.     map.put(j, "s" + j);  
  4. }  
  5.   
  6. for (int i = 0; i < 100000; i++) {  
  7.     for (int j = 0; j < 100; j++) {  
  8.         map.get(j);  
  9.     }  
  10. }  
new方式 运行时间
new TreeMap<Integer, String>() 542
new HashMap<Integer, String>(100) 280

(2)变化下集合的大小(有时我们使用Map存放上万条乃至几十万条数据用于小数据本地快速缓存):

[java] view plaincopy
  1. Map<Integer, String> map = new HashMap<Integer, String>(300000);  
  2. for (int j = 0; j < 300000; j++) {  
  3.     map.put(j, "sasd");  
  4. }  
  5. beginTime = System.currentTimeMillis();  
  6. for (int i = 0; i < 100; i++) {  
  7.     for (int j = 0; j < 300000; j++) {  
  8.         map.get(j);  
  9.     }  
  10. }  
new方式 运行时间
new TreeMap<Integer, String>() 5100
new HashMap<Integer, String>(300000) 1400

(3)当hashcode为极端情况下:

[java] view plaincopy
  1. Map<HashKeyTest, String> map = new TreeMap<HashKeyTest, String>();  
  2. for (int j = 0; j < 10000; j++) {  
  3.     map.put(new HashKeyTest(j), "sasd");  
  4. }  
  5. beginTime = System.currentTimeMillis();  
  6. for (int i = 0; i < 10; i++) {  
  7.     for (int j = 0; j < 10000; j++) {  
  8.         map.get(new HashKeyTest(j));  
  9.     }  
  10. }  
  11.   
  12. public class HashKeyTest implements Comparable<HashKeyTest> {  
  13.   
  14.     private int value;  
  15.     ...  
  16.       
  17.     @Override  
  18.     public boolean equals(Object obj) {  
  19.         if (this == obj)  
  20.             return true;  
  21.         if (obj == null)  
  22.             return false;  
  23.         if (getClass() != obj.getClass())  
  24.             return false;  
  25.         HashKeyTest other = (HashKeyTest) obj;  
  26.         if (value != other.value)  
  27.             return false;  
  28.         return true;  
  29.     }  
  30.     //模拟hashCode的极端情况,返回相同的值  
  31.     @Override  
  32.     public int hashCode() {  
  33.         return 345345435;  
  34.     }  
  35.     //用于TreeMap的二叉树对比排序  
  36.     @Override  
  37.     public int compareTo(HashKeyTest o) {  
  38.         int thisVal = this.value;  
  39.         int anotherVal = o.getValue();  
  40.         return (thisVal < anotherVal ? -1 : (thisVal == anotherVal ? 0 : 1));  
  41.     }  
  42.   
  43. }  
new方式 消耗时间
new TreeMap<HashKeyTest, String>() 18
new HashMap<HashKeyTest, String>() 3480

*************************************************源码分析*************************************************

TreeMap.get(Object key):

基于红黑树查找

[java] view plaincopy
  1.    public V get(Object key) {  
  2.        Entry<K,V> p = getEntry(key);  
  3.        return (p==null ? null : p.value);  
  4.    }  
  5.    final Entry<K,V> getEntry(Object key) {  
  6.        // Offload comparator-based version for sake of performance  
  7.        if (comparator != null)  
  8.            return getEntryUsingComparator(key);  
  9.        if (key == null)  
  10.            throw new NullPointerException();  
  11. Comparable<? super K> k = (Comparable<? super K>) key;  
  12.        Entry<K,V> p = root;  
  13.        while (p != null) {  
  14.            int cmp = k.compareTo(p.key);  
  15.            if (cmp < 0)  
  16.                p = p.left;  
  17.            else if (cmp > 0)  
  18.                p = p.right;  
  19.            else  
  20.                return p;  
  21.        }  
  22.        return null;  
  23.    }  

HashMap.get(Object key):

代码中很简明的可以看出,如果hashCode不均为出现的问题

[java] view plaincopy
  1.    public V get(Object key) {  
  2.        if (key == null)  
  3.            return getForNullKey();  
  4.        int hash = hash(key.hashCode());  
  5. //若hashCode相同,会导致查找方式就是这里的不停的遍历  
  6.        for (Entry<K,V> e = table[indexFor(hash, table.length)];  
  7.             e != null;  
  8.             e = e.next) {  
  9.            Object k;  
  10.            if (e.hash == hash && ((k = e.key) == key || key.equals(k)))  
  11.                return e.value;  
  12.        }  
  13.        return null;  
  14.    }  

总结:

1、key如果是自定义对象,一定要有效地重载hashCode(),可参考《覆盖equals时总要覆盖hashCode》;

2、尽量保证hashCode“均为分布”,以便于均匀填充“水桶”;

3、当我们确定Map存放的数据比较多且hashCode分配严重不均匀,切记不要使用HashMap,建议使用TreeMap;

4、正常情况下,也就是我们常把表id作为key时,建议使用HashMap。


Set留给同学自己学习吧。(HashSet其实就是使用的HashMap作为容器)

原文地址:https://www.cnblogs.com/new0801/p/6175879.html