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



 

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

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

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

第一部分:List

1.add(E e)

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

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

[java] view
plain
copy
  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
plain
copy
  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
plain
copy
  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. Entry(E element, Entry<E> next, Entry<E> previous) {
  21. this.element = element;
  22. this.next = next;
  23. this.previous = previous;
  24. }
  25. }

也就是说LinkedList的上下级维护关系是借助了指针,若任意位置插入,则是先查找到before前的entry,然后重指原有位置前后元素的指针。

ArrayList.add:

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

[java] view
plain
copy
  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
plain
copy
  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
plain
copy
  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
plain
copy
  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
plain
copy
  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
plain
copy
  1. List<Integer> list = new LinkedList<Integer>();
  2. for (int j = 0; j < 10000; j++) {
  3. list.add(j);
  4. }
  5. for (int i = 0; i < 100; i++) {
  6. for (int j = 0; j < 10000; j++) {
  7. list.get(j);
  8. }
  9. }
new方式 消耗时间
new LinkedList<Integer>() 8349
new ArrayList<Integer>(10000) 15

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

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

LinkedList.get(int index):

[java] view
plain
copy
  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
plain
copy
  1. public E get(int index) {
  2. //检查是否越界
  3. RangeCheck(index);
  4. //直接从数组中取出对应index的数据
  5. return (E) elementData[index];
  6. }

4.迭代器

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

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

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

LinkedList.iterator():

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

也就是说我们的LinkedList的Iterator性能损耗主要在变换指针,所以对比for遍历方式来说性能提升了很多。

ArrayList.iterator():

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

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
plain
copy
  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
plain
copy
  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
plain
copy
  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. // Find a power of 2 >= initialCapacity
  12. int capacity = 1;
  13. //取大于等于初始容量最近的2的倍数
  14. while (capacity < initialCapacity)
  15. capacity <<= 1;
  16. this.loadFactor = loadFactor;
  17. threshold = (int)(capacity * loadFactor);
  18. table = new Entry[capacity];
  19. init();
  20. }
  21. public V put(K key, V value) {
  22. if (key == null)
  23. return putForNullKey(value);
  24. //计算key的hash值
  25. int hash = hash(key.hashCode());
  26. //计算出在第几个“水桶”,计算方式是&与了table.length-1,这应该也是水桶数据是2的倍数的原因
  27. int i = indexFor(hash, table.length);
  28. //通过这步可以理解为什么看到好多文档要求key的hashCode“均匀分布”便于均匀落入各个水桶
  29. for (Entry<K,V> e = table[i]; e != null; e = e.next) {
  30. Object k;
  31. if (e.hash == hash && ((k = e.key) == key || key.equals(k))) {
  32. V oldValue = e.value;
  33. e.value = value;
  34. e.recordAccess(this);
  35. return oldValue;
  36. }
  37. }
  38. modCount++;
  39. addEntry(hash, key, value, i);
  40. return null;
  41. }

2.get

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

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

[java] view
plain
copy
  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. for (int i = 0; i < 100000; i++) {
  6. for (int j = 0; j < 100; j++) {
  7. map.get(j);
  8. }
  9. }
new方式 运行时间
new TreeMap<Integer, String>() 542
new HashMap<Integer, String>(100) 280

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

[java] view
plain
copy
  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
plain
copy
  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. public class HashKeyTest implements Comparable<HashKeyTest> {
  12. private int value;
  13. ...
  14. @Override
  15. public boolean equals(Object obj) {
  16. if (this == obj)
  17. return true;
  18. if (obj == null)
  19. return false;
  20. if (getClass() != obj.getClass())
  21. return false;
  22. HashKeyTest other = (HashKeyTest) obj;
  23. if (value != other.value)
  24. return false;
  25. return true;
  26. }
  27. //模拟hashCode的极端情况,返回相同的值
  28. @Override
  29. public int hashCode() {
  30. return 345345435;
  31. }
  32. //用于TreeMap的二叉树对比排序
  33. @Override
  34. public int compareTo(HashKeyTest o) {
  35. int thisVal = this.value;
  36. int anotherVal = o.getValue();
  37. return (thisVal < anotherVal ? -1 : (thisVal == anotherVal ? 0 : 1));
  38. }
  39. }
new方式 消耗时间
new TreeMap<HashKeyTest, String>() 18
new HashMap<HashKeyTest, String>() 3480

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

TreeMap.get(Object key):

基于红黑树查找

[java] view
plain
copy
  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
plain
copy
  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作为容器)

上一篇:Java Properties工具类详解


下一篇:Java json工具类,jackson工具类,ObjectMapper工具类