Array(循环数组)与Linked(链表)
常用的集合类型
常用集合类型 |
描述 |
ArrayList |
一种可以动态增长和缩减的索引序列 |
LinkedList |
一种可以在任何位置进行高效地插入和删除操作的有序序列 |
ArrayDeque |
一种循环数组实现的双端队列 |
HashSet |
一种没有重复元素的无序集合 |
TreeSet |
一种有序集 |
EnumSet |
一种包含枚举类型值的集 |
LinkedHashSet |
一种可以记住元素插入次序的集 |
PriorityQueue |
一种允许高效删除最小元素的集合 |
HashMap |
一种存储健值关联的数据结构 |
TreeMap |
一种健值有序排列的映射表 |
LinkedHashMap |
一种可以记住健值项添加次序的映射表 |
WeakHashMap |
一种其值无用武之地后可以被垃圾回收器回收的映射表 |
IdentityHashMap | 一种用==,而不使用equals比较健值的映射 |
对于数组和ArrayList都有一个重大的缺陷,就是从中间位置删除一个元素要付出很大的代价,其原因是数组中处于被删除元素之后的所有元素都要向数组前端移动。在数组中间的位置上插入一个元素也是如此。
为了了解这个缺陷,看一段代码:
@Test public void ArrayListAddAndRemoveTest() { ArrayList<Integer> arrayList = new ArrayList<Integer>(); // 向ArrayList中添加10万个整形。 for (int i = 0; i < 100000; i++) { arrayList.add(i); } }
向一个ArrayList集合中add了10万个整形,花费了0.011秒。接下来测试一下之前所说的,从中间位置删除一个元素要付出很大的代价,其原因是数组中处于被删除元素之后的所有元素都要向数组前端移动。
@Test public void ArrayListAddAndRemoveTest() { ArrayList<Integer> arrayList = new ArrayList<Integer>(); // 向ArrayList中添加10万个整形。 for (int i = 0; i < 100000; i++) { arrayList.add(i); } // 从头开始移除ArrayList集合中的元素,被删除元素之后的所有元素都要向数组前端移动。 for (int i = 0; i < 100000; i++) { arrayList.remove(0); } }
从头开始移除ArrayList集合中的元素,执行10万次,将ArrayList中的对象都删除掉,花费了1.045秒。
接下来,我们非常熟悉的数据结构——链表就解决了这个问题。数组在连续的存储位置上存放对象引用,但链表却将每个对象存放在独立的节点中。在Java语言中,所有链表实际上都是双向连接的。
@Test public void LinkedListAddAndRemoveTest() { LinkedList<Integer> linkedList = new LinkedList<Integer>(); // 向LinkedList集合中添加10万个整形。 for (int i = 0; i < 100000; i++) { linkedList.add(i); } // 从头开始移除LinkedList集合中的元素。 for (int i = 0; i < 100000; i++) { linkedList.remove(0); } }
结果是令人欣慰的,在链表中间删除一个元素是一个很轻松的操作,简单来说就是只需要对被删除的元素的前一个元素的next指针更新,对被删除元素的下一个元素的pervious指针更新即可。
但是链表不支持快速地随机访问。如果要查看链表中第n个元素,就必须从头开始,越过n-1个元素。没有捷径可走。鉴于这个原因,在程序需要采用整数索引访问元素时,通常不选用链表(尽管对链表做了优化,如果 索引大于size()/2就从列表尾端开始搜索)。
继续测试一下,看看链表是否不擅长快速地随机访问。
@Test public void LinkedListGetTest() { LinkedList<Integer> linkedList = new LinkedList<Integer>(); // 向LinkedList集合中添加10万个整形。 for (int i = 0; i < 100000; i++) { linkedList.add(i); } // 通过索引随机快速访问LinkedList集合。 for (int i = 0; i < 100000; i++) { linkedList.get(i); } }
花费了3.6秒左右来做这件事,看来链表确实是不太擅长快速地随机访问。若用迭代器顺序访问或者使用ArrayList来做随机访问会更好些吗?继续测试:
@Test public void LinkedListGetByIteratorTest() { LinkedList<Integer> linkedList = new LinkedList<Integer>(); // 向LinkedList集合中添加10万个整形。 for (int i = 0; i < 100000; i++) { linkedList.add(i); } // 使用迭代器按顺序获得链表中的值。 Iterator<Integer> iterator = linkedList.iterator(); while (iterator.hasNext()) { iterator.next(); } }
可以看到按照顺序使用迭代器或者链表中的数据是非常快的,但不能进行随机访问。接下来试试ArrayList的随机访问速度。
@Test public void ArrayListGetTest() { ArrayList<Integer> arrayList = new ArrayList<Integer>(); // 向ArrayList集合中添加10万个整形。 for (int i = 0; i < 100000; i++) { arrayList.add(i); } // 通过索引随机快速访问ArrayList集合。 for (int i = 0; i < 100000; i++) { arrayList.get(i); } }
到这里,已经测试完毕。
如果链表中只有很少几个元素,就完全没有必要为get方法和set方法的开销而烦恼。但是,有没有必要优先考虑使用链表呢?其实,使用链表的唯一理由就是尽可能地减少在列表中间插入或者删除元素所付出的代价。如果列表只有少数几个元素,就完全可以使用ArrayList(数组列表)。
建议避免使用以证书索引表示链表中位置的所有方法,如LinkedList.get(n)。如果需要对集合进行随机访问,就使用数组或者ArrayList就好。
附加一个使用LinkedList的例子。
package cn.net.bysoft.test; import java.util.Iterator; import java.util.LinkedList; import java.util.List; import java.util.ListIterator; import org.junit.Test; public class LinkedListTest { @Test public void Test() { List<String> a = new LinkedList<String>(); a.add("Amy"); a.add("Carl"); a.add("Erica"); List<String> b = new LinkedList<String>(); b.add("Bob"); b.add("Doug"); b.add("Frances"); b.add("Gloria"); // 将b集合的内容合并到a中。 ListIterator<String> aIter = a.listIterator(); Iterator<String> bIter = b.iterator(); // 移动b的迭代器,同时移动a的迭代其,将b的内容添加到a中。 while (bIter.hasNext()) { if (aIter.hasNext()) aIter.next(); aIter.add(bIter.next()); } System.out.println("将b集合的内容合并到a中。"); System.out.println(a); // 获得一个迭代其 bIter = b.iterator(); // 从b中隔一个一删除内容。 while (bIter.hasNext()) { bIter.next(); if (bIter.hasNext()) { bIter.next(); bIter.remove(); } } System.out.println("从b中隔一个一删除内容。"); System.out.println(b); // 批量删除a中与b相同的内容。 a.removeAll(b); System.out.println("批量删除a中与b相同的内容。"); System.out.println(a); } /* * output: * [Amy, Bob, Carl, Doug, Erica, Frances, Gloria] * [Bob, Frances] * [Amy, Carl, Doug, Erica, Gloria] * */ }
上面的代码用迭代器遍历了LinkedList集合,集合有两种访问元素的协议,一个是迭代器,另一个就是用get和set方法随机地访问每个元素。后者不适用于链表,但是对数组却很有用。集合类库提供了一种大家最常使用的ArrayList类,它实现List接口封装了一个动态再分配的对象数组。
动态数组还有一个Vector类,这个类的所有方法都是同步的,也就是说是线程安全的。但是,如果由一个线程访问Vector,代码要在同步操作上耗费大量时间。因此,在不需要同步的时候使用ArrayList更好些。