package java.util
包中的Collection相关接口和类如下图:
仅讨论Java.util包中的常见集合类,不涉及java.util的子包concurrent中的并发集合类。
可以这样简单的来对待容器中集合:
1、 List、Set、Queue三个接口的意义
首先,接口是一种规范,按照接口规范实现接口的的方法,就能提供所期望的功能。
三个接口只是规定了三类不同特性的集合,有时候实现类会同时实现其中的多个特性,如LinkedList:
LinkedList既实现了List接口,又(间接)实现了Queue接口。它是List还是Queue呢?所以要理解这三个接口存在的意义。
- List:元素有序,元素可重复,添加的元素放在最后(按照插入顺序保存元素)
//Appends the specified element to the end of this list
boolean add(E e);
List是有序的Collection,使用此接口能够精确的控制每个元素插入的位置。用户能够使用索引(元素在List中的位置,类似于数组下标)来访问List中的元素,类似于Java的数组。List允许有相同的元素。除了具有Collection接口必备的iterator()方法外,List还提供一个listIterator()方法,返回一个 ListIterator接口,和标准的Iterator接口相比,ListIterator多了一些add()之类的方法,允许添加,删除,设定元素, 还能向前或向后进行双向遍历。
- **Set:元素无序并且不允许重复元素 **
// If this set already contains the element, the call leaves the set
//unchanged and returns false. In combination with the
//restriction on constructors, this ensures that sets never contain
//duplicate elements.
boolean add(E e);
Set是一种不包含重复的元素的Collection,即任意的两个元素e1和e2都有e1.equals(e2)=false,Set最多有一个null元素。
很明显,Set的构造函数有一个约束条件,传入的Collection参数不能包含重复的元素。
请注意:必须小心操作可变对象(Mutable Object)。如果一个Set中的可变元素改变了自身状态导致Object.equals(Object)=true将导致一些问题。
- Queue:元素有序,先进先出
它们都是通过扩展Collection接口而来。Collection接口规范了存放一组对象的方式,而这组对象在存放时具有哪些特点(元素有序还是无序,元素允不允许重复等),则通过这三个接口进一步规范,实现了某个接口就具有某个接口规定的特性。而具体的实现方式的不同,使得各实现类的应用场景不同。
2、各实现类的特点
先看List:
-
ArrayList
数据结构为数组,访问快(可以直接通过下标访问),增删慢,未实现线程同步。
ArrayList实现了可变大小的数组。它允许所有元素,包括null。ArrayList没有同步。size,isEmpty,get,set方法运行时间为常数。但是add方法开销为分摊的常数,添加n个元素需要O(n)的时间。其他的方法运行时间为线性。</br>
每个ArrayList实例都有一个容量(Capacity),即用于存储元素的数组的大小。这个容量可随着不断添加新元素而自动增加,但是增长算法 并没有定义。当需要插入大量元素时,在插入前可以调用ensureCapacity方法来增加ArrayList的容量以提高插入效率。和LinkedList一样,ArrayList也是非同步的(unsynchronized)。
-
LinkedList
数据结构为链表,增删速度快,查询慢,未实现线程同步
LinkedList实现了List接口,允许null元素。此外LinkedList提供额外的get,remove,insert方法在 LinkedList的首部或尾部。这些操作使LinkedList可被用作堆栈(stack),队列(queue)或双向队列(deque)。
-
Vector类
数据结构为数组,访问快(可以直接通过下标访问),增删慢,实现线程同步
Vector非常类似ArrayList,但是Vector是同步的。由Vector创建的Iterator,虽然和 ArrayList创建的Iterator是同一接口,但是,因为Vector是同步的,当一个Iterator被创建而且正在被使用,另一个线程改变了 Vector的状态(例如,添加或删除了一些元素),这时调用Iterator的方法时将抛出 ConcurrentModificationException。
再看Set
-
HashSet
数据结构为哈希表,元素无序、不重复,至多有一个null元素
底层使用 HashMap 来保存所有元素,因此 HashSet 的实现比较简单,相关 HashSet 的操作,基本上都是直接调用底层 HashMap 的相关方法来完成。
特点如下
不能保证元素的排列顺序,顺序有可能发生变化
不是同步的
集合元素可以是null,但只能放入一个null
当向HashSet结合中存入一个元素时,HashSet会调用该对象的hashCode()方法来得到该对象的hashCode值,然后根据 hashCode值来决定该对象在HashSet中存储位置。
简单的说,HashSet集合判断两个元素相等的标准是两个对象通过equals方法比较相等,并且两个对象的hashCode()方法返回值相 等
注意,如果要把一个对象放入HashSet中,重写该对象对应类的equals方法,也应该重写其hashCode()方法。其规则是如果两个对 象通过equals方法比较返回true时,其hashCode也应该相同。另外,对象中用作equals比较标准的属性,都应该用来计算 hashCode的值。
-
LinkedHashSet
数据结构是哈希表和链表,与HashSet相比访问更快,插入时性能稍微
LinkedHashSet继承自HashSet。同样是根据元素的hashCode值来决定元素的存储位置,但是它同时使用链表维护元素的次序。这样使得元素看起 来像是以插入顺序保存的,也就是说,当遍历该集合时候,LinkedHashSet将会以元素的添加顺序访问集合的元素。
LinkedHashSet在迭代访问Set中的全部元素时,性能比HashSet好,但是插入时性能稍微逊色于HashSet。
-
TreeSet
数据结构是二叉树(红黑树),元素可排序、不重复
TreeSet是SortedSet接口的唯一实现类,TreeSet可以确保集合元素处于排序状态。TreeSet支持两种排序方式:自然排序和定制排序,其中自然排序为默认的排序方式。向TreeSet中加入的应该是同一个类的对象。
TreeSet判断两个对象不相等的方式是两个对象通过equals方法返回false,或者通过CompareTo方法比较没有返回0
</br>自然排序
(1)TreeSet内的元素实现Comparable
接口,重写该接口的compareTo(Object obj)
方法,以此确定排序。(元素必须实现该接口,否则程序会抛出异常)。
(2)当重写元素对应类的equals()
方法时,应该保证该方法与compareTo(Object obj)
方法有一致的结果,即如果两个对象通过equals()
方法比较返回true时,这两个对象通过compareTo(Object obj)
方法比较结果应该也为0(即相等)
</br>定制排序--
自然排序是根据集合元素的大小,以升序排列,如果要定制排序,应该使用Comparator
接口,实现int compare(T o1,T o2)
方法
个人看法:两种方式都是为了排序,一种是元素本身实现`Comparable`接口,另一种则是当元素本身没有实现`Comparable`接口时,可以通过`Comparator`接口(传入构造器`TreeSet(Comparator comparator)`),两者本身没有什么区别。
Java : Comparable vs Comparator
In short, there isn't much difference. They are both ends to similar means. In general implement comparable for natural order, (natural order definition is obviously open to interpretation), and write a comparator for other sorting or comparison needs.
最后看Queue
-
ArrayDeque
数据结构为数组,双端队列,在队头队尾均可心插入或删除元素
实现了DeQueue接口。DeQueue(Double-ended queue)继承了Queue接口,创建双向队列,灵活性更强,可以前向或后向迭代,
-
PriorityQueue
数据结构为优先级队列,元素不允许null,非同步
优先级队列是一种什么样的数据结构
优先级队列是不同于先进先出队列的另一种队列。每次从队列中取出的是具有最高优先权的元素。
(1)优先级队列不是同步的(线程安全版本为PriorityBlockingQueue)
队列的获取操作如poll(),peek()和element()是访问的队列的头,保证获取的是最小的元素(根据指定的排序规则)
(2)返回的迭代器并不保证提供任何的有序性
(3)优先级队列不允许null元素,否则抛出NullPointException。
3、有助于理解的问答
- 遍历一个List有哪些不同的方式?
List<String> strList = new ArrayList<>();
//使用for-each循环
for(String obj : strList){
System.out.println(obj);
}
//using iterator
Iterator<String> it = strList.iterator();
while(it.hasNext()){
String obj = it.next();
System.out.println(obj);
}
使用迭代器更加线程安全,因为它可以确保,在当前遍历的集合元素被更改的时候,它会抛出ConcurrentModificationException。
- 在迭代一个集合的时候,如何避免ConcurrentModificationException?
在遍历一个集合的时候,我们可以使用并发集合类来避免ConcurrentModificationException,比如使用CopyOnWriteArrayList,而不是ArrayList。
4、常见面试题
- List和Set的区别
List有序,允许重复,Set无序,不允许重复 - ArrayList与LinkedList的区别
ArrayList为数组结构,LinkedList为链表结构。所以,一个访问快,一个增删快。均未实现线程同步或者说都是线程不安全的。
(1)ArrayList是由Array所支持的基于一个索引的数据结构,所以它提供对元素的随机访问,复杂度为O(1),但LinkedList存储一系列的节点数据,每个节点都与前一个和下一个节点相连接。所以,尽管有使用索引获取元素的方法,内部实现是从起始点开始遍历,遍历到索引的节点然后返回元素,时间复杂度为O(n),比ArrayList要慢。
(2)与ArrayList相比,在LinkedList中插入、添加和删除一个元素会更快,因为在一个元素被插入到中间的时候,不会涉及改变数组的大小,或更新索引。
(3)LinkedList比ArrayList消耗更多的内存,因为LinkedList中的每个节点存储了前后节点的引用。
- ArrayList与Vector的区别
Vector同步,ArrayList不同步。
ArrayList和Vector在很多时候都很类似。
(1)两者都是基于索引的,内部由一个数组支持。
(2)两者维护插入的顺序,我们可以根据插入顺序来获取元素。
(3)ArrayList和Vector的迭代器实现都是fail-fast的。
(4)ArrayList和Vector两者允许null值,也可以使用索引值对元素进行随机访问。</br>
以下是ArrayList和Vector的不同点。
(1)Vector是同步的,而ArrayList不是。然而,如果你寻求在迭代的时候对列表进行改变,你应该使用CopyOnWriteArrayList。
(2)ArrayList比Vector快,它因为有同步,不会过载。
(3)ArrayList更加通用,因为我们可以使用Collections工具类轻易地获取同步列表和只读列表。
- Array和ArrayList有何区别?什么时候更适合用Array?
Array可以容纳基本类型和对象,而ArrayList只能容纳对象。
Array是指定大小的,而ArrayList大小是不固定的。
Array没有提供ArrayList那么多功能,比如addAll、removeAll和iterator等。尽管ArrayList明显是更好的选择,但也有些时候Array比较好用。</br>
(1)如果列表的大小已经指定,大部分情况下是存储和遍历它们。
(2)对于遍历基本数据类型,尽管Collections使用自动装箱来减轻编码任务,在指定大小的基本类型的列表上工作也会变得很慢。
(3)如果你要使用多维数组,使用[][]比List<List<>>更容易。
5、简单总结
如果元素唯一(不允许重复),使用Set:
支持排序则选择TreeSet,不支持排序选择HashSet。
如果元素不唯一(允许重复),使用List:
查询多则选择ArrayList,增删多则选择:LinkedList。
Vector虽然是ArrayList的线程同步版本,但还有更好地选择。
关于Queue,由于工作中从未接触过,待补充。
参考
未一一列出,待补充