一、集合类的层次关系
主要容器集合类的特点:
ArrayList 一种可以动态增长和缩减的索引序列
LinkedList 一种可以在任何位置进行高效地插入和删除的有序序列
ArrayDeque 一种用循环数组实现的双端队列
HashSet 一种没有重复元素的无序集合
TreeSet 一种有序集
EnumSet 一种包含枚举类型值的集
LinkedHashSet 一种可以记住元素插入次序的集
PriorityQueue 一种允许高效删除最小元素的集合(参考 堆)
HashMap 一种存储键/值关联的数据结构
TreeMap 一种键值有序排列的映射表
EnumMap 一种键值属于枚举类型的映射表
LinkedHashMap 一种可以记住键/值项添加次序的映射表
WeakHashMap 一种其值没有用处之后可以被GC回收的映射表
IdentityHashMap 一种用==,而不是equals比较键值的映射表
以上这些集合类的继承关系如下图所示:
如上图所示,实线边框的是实现类,折线边框的是抽象类,而点线边框的是接口。
Collection接口是集合类的根接口,Java中没有提供这个接口的直接的实现类。但是却让其被继承产生了两个接口,就是Set和List(Queue也是继承了Collection接口)。Set中不能包含重复的元素。List是一个有序的集合,可以包含重复的元素,提供了按索引访问的方式。
Map是Java.util包中的另一个接口,它和Collection接口没有关系,是相互独立的,但是都属于集合类的一部分。Map包含了key-value对。Map不能包含重复的key,但是可以包含相同的value。
Iterator,所有的集合类都实现了Iterator接口,这是一个用于遍历集合中元素的接口,主要包含以下三种方法:
1.hasNext()是否还有下一个元素。
2.next()返回下一个元素。
3.remove()删除当前元素
常用的一些集合的框架图
二、几种重要的接口和类简介
1、List(有序、可重复)
List里存放的对象是有序的,同时也是可以重复的,List关注的是索引,拥有一系列和索引相关的方法,查询速度快。因为往list集合里插入或删除数据时,会伴随着后面数据的移动,所有插入删除数据速度慢。
2、Set(无序、不能重复)
Set里存放的对象是无序,不能重复的,集合中的对象不按特定的方式排序,只是简单地把对象加入集合中。
3、Map(键值对、键唯一、值不唯一)
Map集合中存储的是键值对,键不能重复,值可以重复。根据键得到值,对map集合遍历时先得到键的set集合,对set集合进行遍历,得到相应的值。
三、集合类的遍历
在类集中提供了以下四种的常见输出方式:
1)Iterator:迭代输出,是使用最多的输出方式。
2)ListIterator:是Iterator的子接口,专门用于输出List中的内容。
- foreach:JDK1.5之后提供的新功能,可以输出数组或集合。
4)for循环。
四、集合类内部源码分析
如果你没看过JDK源码,建议打开IDEA边看源码边看这篇文章,看过的可以把这篇文章当成是知识点备忘录。
JDK容器类中有大量的空指针、数组越界、状态异常等异常处理,这些不是重点,我们关注的应该是它的一些底层的具体实现。
可以参考这个博客,对于集合类写得非常详细:
http://www.cnblogs.com/skywang12345/p/3323085.html
我根据源码以及参考了以上博客,把我认为比较重要的内容记录在这里。
4.1 ArrayList
4.1.1 介绍
ArrayList 是一个数组队列,相当于动态数组。与Java中的普通数组相比,它的容量能动态增长。它继承于AbstractList,实现了List, RandomAccess, Cloneable, java.io.Serializable这些接口。
- ArrayList 继承了AbstractList,实现了List接口。它是一个数组队列,提供了相关的添加、删除、修改、遍历等功能。
- ArrayList 实现了RandmoAccess接口,即提供了随机访问功能。
- ArrayList 实现了Cloneable接口,即覆盖了函数clone(),能被克隆。
- ArrayList 实现java.io.Serializable接口,这意味着ArrayList支持序列化,能通过序列化去传输。
和Vector不同,ArrayList中的操作不是线程安全的!所以,建议在单线程中才使用ArrayList,而在多线程中可以选择Vector或者CopyOnWriteArrayList。
4.1.2 源码总结
- ArrayList 实际上是通过一个数组去保存数据的。当我们构造ArrayList时;若使用默认构造函数,则ArrayList的默认容量大小是10。
- 当ArrayList容量不足以容纳全部元素时,ArrayList会重新设置容量:新的容量=“(原始容量x3)/2 + 1”,当数组容量扩增到Integer.MAX_VALUE-8的时候,就会开始限制数组扩充,超过Integer.MAX_VALUE的时候,抛内存溢出异常;
- ArrayList的克隆函数,即是将全部元素克隆到一个数组中。
- ArrayList实现java.io.Serializable的方式。当写入到输出流时,先写入“容量”,再依次写入“每一个元素”;当读出输入流时,先读取“容量”,再依次读取“每一个元素”。
- ArrayList 中实现了ListIterator迭代器,它和普通迭代器类似,只是多了双向迭代的功能(不仅有hasNext(),还有hsaPrevious(),可以从后向前遍历)。
- 迭代器中的expectedModCount和modCount:迭代器初始化时,会用modCount去初始化expectedModCount,在迭代器的操作中都要检查这两个值是否相等,若不相等则抛出ConcurrentModificationException异常表示当前List正在被修改,原因在于List本身的add和remove操作时都会修改modCount的值,如果在迭代器循环迭代过程中调用了List本身的add和remove方法就会导致两个值不相等而抛出异常,但是迭代器本身的remove则不会有这个问题,它会在remove完成后重新更新一遍expectedModCount保持两个值的等值性。
- ArrayList本身的sort其实是调用Arrays.sort传入比较器实现的。
4.1.3 遍历
ArrayList支持3种遍历方式:
(1)迭代器遍历(Iterator)
(2)随机访问,通过索引值遍历(因为实现了RandomAccess接口)
Integer value = null;
int size = list.size();
for (int i=0; i<size; i++) {
value = (Integer)list.get(i);
}
(3)增强for循环遍历
Integer value = null;
for (Integer integ:list) {
value = integ;
}
用这三种方式遍历ArrayList时,使用随机访问(即,通过索引序号访问)效率最高,而使用迭代器的效率最低。
4.2 LinkedList
4.2.1 介绍
LinkedList 是一个继承于AbstractSequentialList的双向链表。它也可以被当作堆栈、队列或双端队列进行操作。
LinkedList 实现了List 接口,能对它进行队列操作。
LinkedList 实现了Deque 接口,即能将LinkedList当作双端队列使用。
LinkedList 实现了Cloneable接口,即覆盖了函数clone(),能克隆。
LinkedList 实现了java.io.Serializable接口,这意味着LinkedList支持序列化,能通过序列化去传输。
LinkedList 是线程不安全的。
4.2.2 源码总结
1. LinkedList 实际上是通过双向链表去实现的,它支持链表头增删,链表尾增删,指定节点前或后增删。它包含一个非常重要的内部类:Entry。Entry是双向链表节点所对应的数据结构,它包括的属性有:当前节点所包含的值,上一个节点,下一个节点。
2. LinkedList不存在LinkedList容量不足的问题,不像ArrayList一样需要扩容,维护好指针的指向关系即可。
3. LinkedList的克隆函数,即是将全部元素克隆到一个新的LinkedList对象中。
4. LinkedList实现java.io.Serializable。当写入到输出流时,先写入“容量”,再依次写入“每一个节点保护的值”;当读出输入流时,先读取“容量”,再依次读取“每一个元素”。
5. LinkedList中的Element元素支持插入空值,和HashMap一样。
6. LinkedList的迭代器中也有expectedModCount检测链表的修改状态,迭代器循环迭代时只能用迭代器的remove方法。
7. 由于LinkedList实现了Deque,而Deque接口定义了在双端队列两端访问元素的方法。提供插入、删除和获取元素的方法。每种方法都存在两种形式:一种形式在操作失败时抛出异常,另一种形式返回一个特殊值(null 或 false,具体取决于操作)。总结起来如下表格:
针对第一个元素(头部) | 针对第一个元素(头部) | 针对最后一个元素(尾部) | 针对最后一个元素(尾部) | |
| 抛异常 | 返回特殊值 | 抛异常 | 返回特殊值 |
插入 | addFirst(e) | offerFirst(e) | addLast(e) | offerLast(e) |
删除 | removeFirst() | pollFirst() | removeLast() | pollLast() |
获取 | getFirst() | peekFirst() | getLast() | peekLast() |
8. LinkedList可以作为FIFO(先进先出)的队列,作为FIFO的队列时,下表的方法等价:
队列中的方法 | LinkedList中的等效方法 |
add(e) | addLast() |
offer(e) | offerLast() |
remove() | removeFirst() |
poll() | pollFirst() |
element() | getFirst() |
peek() | peekFirst() |
9. LinkedList可以作为LIFO(后进先出)的栈,作为LIFO的栈时,下表的方法等价:
栈中的方法 | LinkedList中的等效方法 |
push(e) | addFirst() |
pop() | removeFirst() |
peek() | peekFirst() |
4.2.3 遍历
LinkedList支持以下几种方式遍历:
1.通过迭代器遍历,即通过Iterator去遍历。
2.通过快速随机访问遍历LinkedList
int size = list.size();
for (int i=0; i<size; i++) {
list.get(i);
}
3.通过增强for循环来遍历
4.通过pollFirst()、pollLast()、removeFirst()、removeLast()遍历。如:
while(list.pollFirst() != null) {}
使用removeFirst()或removeLast()效率最高。但用它们遍历时,会删除原始数据;若单纯只读取,而不删除,应该使用增强for循环遍历方式。无论如何,千万不要通过随机访问去遍历,效率很低。
4.3 Vector
4.3.1 介绍
Vector 是矢量队列,它是JDK1.0版本添加的类。继承于AbstractList,实现了List, RandomAccess, Cloneable这些接口。
Vector 继承了AbstractList,实现了List;所以,它是一个队列,支持相关的添加、删除、修改、遍历等功能。
Vector 实现了RandmoAccess接口,即提供了随机访问功能。
Vector 实现了Cloneable接口,即实现clone()函数。它能被克隆。
和ArrayList不同,Vector中的操作是线程安全的。
4.3.2 源码总结
- Vector实际上是通过一个数组去保存数据的,当我们构造Vecotr时;若使用默认构造函数,则Vector的默认容量大小是10。Vector的数据结构和ArrayList差不多,它包含了3个成员变量:elementData、elementCount和capacityIncrement。elementData 是"Object[]类型的数组",它保存了添加到Vector中的元素。随着Vector中元素的增加,Vector的容量也会动态增长,capacityIncrement是与容量增长相关的增长系数。elementCount 是动态数组的实际大小。
- 当Vector容量不足以容纳全部元素时,Vector的容量会增加。若容量增加系数n>0,则将容量的值增加n;否则,将容量大小增加一倍。
- Vector的克隆函数,即是将全部元素克隆到一个数组中。
- 它的特点和ArrayList都一样,但是提供了方法粒度级别的同步机制,大量的synchronized方法。
4.3.3 遍历
Vector支持4种遍历方式:
- 通过迭代器遍历,即通过Iterator去遍历。
- 随机访问,通过索引值去遍历(实现了RandomAccess接口)。
- 增强for循环
- Enumeration遍历,如:
Integer value = null;
Enumeration enu = vec.elements();
while (enu.hasMoreElements()) {
value = (Integer)enu.nextElement();
}
遍历Vector,使用索引的随机访问方式最快,使用迭代器最慢。
4.4 Stack
4.4.1 介绍
Stack是栈。它的特性是:先进后出(FILO, First In Last Out)。
Java工具包中的Stack是继承于Vector(矢量队列)的,由于Vector是通过数组实现的,这就意味着,Stack也是通过数组实现的,而非链表。当然,我们也可以将LinkedList当作栈来使用。
4.4.2 源码总结
1. Stack实际上也是通过数组去实现的。
- 执行push时(即,将元素推入栈中),是通过将元素追加的数组的末尾中。
- 执行peek时(即,取出栈顶元素,不执行删除),是返回数组末尾的元素。
- 执行pop时(即,取出栈顶元素,并将该元素从栈中删除),是取出数组末尾的元素,然后将该元素从数组中删除。
2. Stack继承于Vector,意味着Vector拥有的属性和功能,Stack都拥有。
3. Stack提供整栈元素搜索功能,查找元素在栈中的位置(由栈底向栈顶数),具体获取元素索引的elementAt()实现也是在Vector.java中。
4.5 HashMap
4.5.1 介绍
HashMap 是一个散列表,它存储的内容是键值对(key-value)映射。
HashMap 继承于AbstractMap,实现了Map、Cloneable、java.io.Serializable接口。
HashMap 的实现不是同步的,这意味着它不是线程安全的。
HashMap 的key、value都可以为null。
HashMap中的映射不是有序的。
HashMap 的实例有两个参数影响其性能:“初始容量” 和 “加载因子”。容量 是哈希表中桶的数量,初始容量 只是哈希表在创建时的容量(默认是16,且必须是2的幂次方,最大是2的30次方)。加载因子 是哈希表在其容量自动增加之前可以达到多满的一种尺度。当哈希表中的条目数超出了加载因子与当前容量的乘积时,则要对该哈希表进行 rehash 操作(即重建内部数据结构),从而哈希表将具有大约两倍的桶数。
通常,默认加载因子是 0.75, 这是在时间和空间成本上寻求一种折衷(更深层原因是与泊松分布有关)。加载因子过高虽然减少了空间开销,但同时也增加了查询成本(在大多数 HashMap 类的操作中,包括 get 和 put 操作,都反映了这一点)。在设置初始容量时应该考虑到映射中所需的条目数及其加载因子,以便最大限度地减少 rehash 操作次数。如果初始容量大于最大条目数除以加载因子,则不会发生 rehash 操作。
4.5.2 源码总结
1. HashMap中的key-value都是存储在Entry数组中的,Entry 实际上就是一个单向链表。
2. clear() 的作用是清空HashMap。它是通过将所有的元素设为null来实现的。
3. containsKey() 的作用是判断HashMap是否包含key。containsKey() 首先通过getEntry(key)获取key对应的Entry,然后判断该Entry是否为null。getEntry() 的作用就是返回“键为key”的键值对。注意:HashMap将“key为null”的元素都放在table的位置0处,即table[0]中;“key不为null”的放在table的其余位置!
4. containsValue() 的作用是判断HashMap是否包含“值为value”的元素。containsValue()分为两步进行处理:第一,若“value为null”,则调用containsNullValue()。第二,若“value不为null”,则查找HashMap中是否有值为value的节点。
5. get()方法:计算key的hashCode,该hash值对应的链表”上查找“键值等于key”的元素。(JDK1.8中引入了红黑树。而当链表长度太长(默认超过8)时,链表就转换为红黑树,如果有红黑树就按照红黑树的查找方式进行)。
6. put()方法:
- hashCode()计算Key的哈希值,从而找到数组中的元素位置,具体的定位方法见:https://www.cnblogs.com/wupa/p/7171143.html
- 该位置中没元素,直接放入;
- 若该位置中有元素(hash冲突),则调用equals,对比Key是否真的相等,若相等,用新的Value替代旧的Value;
- 若key不相等,则把这个元素插入到原来的元素后面(链表)
7. resize():重新调整数组大小,新旧数组复制,较耗时;
8. remove()方法:通过hashCode找到数组位置,对比第一个节点,再对比红黑树或链表,找到节点之后删除节点并返回;
9. HashMap的增删方法也和List一样有ConcurrentModificationException异常,使用迭代器需注意;
10. JDK1.8对HashMap的改进:除了引入红黑树,还改进了扩容机制,利用位计算非常巧妙的回避了再扩容时每个元素重新计算hash,在扩充HashMap的时候,不需要像JDK1.7的实现那样重新计算hash,只需要看看原来的hash值新增的那个bit是1还是0就好了,是0的话索引没变,是1的话索引变成“原索引+oldCap”。
PS:JDK1.8对HashMap的改进强烈建议看一下这篇博文:
http://blog.csdn.net/zdsicecoco/article/details/51775545
11. HashMap实现了Cloneable接口,即实现了clone()方法。clone()方法的作用很简单,就是克隆一个HashMap对象并返回。
12. HashMap实现java.io.Serializable,分别实现了串行读取、写入功能。
4.6 LinkedHashMap
4.6.1 介绍
1. HashMap的子类;与HashMap区别是,在HashMap基础上LinkedHashMap内部多了一个双向循环链表的维护,该链表是有序的,可以按元素插入顺序或元素最近访问顺序(LRU)排列;
2. 存储方式和HashMap都一样,都是直接继承的,只是在put元素的时候顺带存储了元素的存入先后顺序,在迭代器遍历的时候,输出的结果顺序是和插入元素时的插入先后顺序一致的;
3. 线程不安全(有ConcurrentModificationException);
4. 可以设置插入元素时自动删除最旧的元素。
4.7 Hashtable
4.7.1 介绍
1. 和HashMap一样,Hashtable 也是一个散列表,它存储的内容是键值对(key-value)映射。
2. Hashtable继承于Dictionary(因为遍历该集合要用到Enumeration迭代器),实现了Map、Cloneable、java.io.Serializable接口。
3. Hashtable的函数都是同步的,这意味着它是线程安全的。它的key、value都不可以为null。此外,Hashtable中的映射不是有序的。
4. Hashtable 的实例有两个参数影响其性能:初始容量 和 加载因子。容量 是哈希表中桶 的数量,初始容量 就是哈希表创建时的容量。注意,哈希表的状态为 open:在发生“哈希冲突”的情况下,单个桶会存储多个条目,这些条目必须按顺序搜索。加载因子 是对哈希表在其容量自动增加之前可以达到多满的一个尺度。
5. 遍历迭代器使用Enumeration方式;(Iterator是Enumeration的升级版,在其中加入了ConcurrentModificationException保证Fail-Fast,但是遍历性能却下降了。在HashTable、Vector中已经用synchronized保证了线程安全,故仅需使用更高效的Enumeration遍历即可)
4.8 TreeMap
4.8.1 介绍
1. TreeMap 是一个有序的key-value集合,它是通过红黑树实现的。
2. TreeMap 继承于AbstractMap,所以它是一个Map,即一个key-value集合。
3. TreeMap 实现了NavigableMap接口,意味着它支持一系列的导航方法。比如返回有序的key集合。
4. TreeMap 实现了Cloneable接口,意味着它能被克隆。
5. TreeMap 实现了java.io.Serializable接口,意味着它支持序列化。
6. TreeMap基于红黑树(Red-Black tree)实现。该映射根据其键的自然顺序进行排序,或者根据创建映射时提供的 Comparator 进行排序,具体取决于使用的构造方法。
7. TreeMap的基本操作 containsKey、get、put 和 remove 的时间复杂度是 log(n) 。
8. TreeMap是非同步的。 它的iterator 方法返回的迭代器是fail-fast的。
9. TreeMap的本质是R-B Tree(红黑树),它包含几个重要的成员变量:root、size、 comparator。root 是红黑数的根节点。它是Entry类型,Entry是红黑数的节点,它包含了红黑数的6个基本组成成分:key(键)、value(值)、left(左孩子)、right(右孩子)、parent(父节点)、color(颜色)。Entry节点根据key进行排序,Entry节点包含的内容为value。 红黑数排序时,根据Entry中的key进行排序;Entry中的key比较大小是根据比较器comparator来进行判断的。size是红黑数中节点的个数。
4.8.2 源码总结
TreeMap的源码大致可以分成几个部分来解读:
- TreeMap的核心,即红黑树相关部分。
- TreeMap的主要函数。
- TreeMap实现的几个接口。
- TreeMap的其它内容。
1. 红黑树相关内容:
(1)数据结构:
红黑树节点的颜色——红色;
红黑树节点的颜色——黑色;
红黑树节点对应的类——Entry,Entry包含了6个部分内容:key(键)、value(值)、left(左孩子)、right(右孩子)、parent(父节点)、color(颜色)
Entry节点根据key进行排序,Entry节点包含的内容为value。
(2)相关操作:
包括 左旋、右旋、插入、插入修正、删除、删除修正。
关于红黑树部分的详细内容,可以参考:
https://my.oschina.net/u/3342874/blog/1833893
2. 主要函数:
(1)构造函数
(2)Entry相关函数,包括firstEntry()、lastEntry()、lowerEntry()、higherEntry()、floorEntry()、ceilingEntry()、 pollFirstEntry()、pollLastEntry()等。
(3)key相关函数,包括firstKey()、lastKey()、lowerKey()、higherKey()、floorKey()、ceilingKey()等。
(4)values函数,返回“TreeMap中值的集合”。
(5)entrySet()函数,entrySet()返回“键值对集合”。顾名思义,它返回的是一个集合,集合的元素是“键值对”。
3. 实现的几个接口:
(1)Cloneable接口,支持浅拷贝。
(2)Serializable接口,支持序列化。
(3)NavigableMap接口,支持一系列的导航方法。比如返回有序的key集合。
4. TreeMap的其它内容:
(1)顺序遍历,从第一个元素开始,逐个向后遍历。通过 keyIterator()方法来实现,在这个方法中,先通过getFirstEntry()得到第一个元素作为起始元素,再通过next()不断获取下一个元素。
(2)逆序遍历,从最后一个元素开始,逐个往前遍历。通过descendingKeyIterator()方法来实现,在这个方法中,先通过getLastEntry()得到最后一个元素作为起始元素,再通过next()不断获取下一个元素。
4.8.3 遍历
1.遍历TreeMap的键值对
第一步:根据entrySet()获取TreeMap的“键值对”的Set集合。
第二步:通过Iterator迭代器遍历“第一步”得到的集合。
2.遍历TreeMap的键
第一步:根据keySet()获取TreeMap的“键”的Set集合。
第二步:通过Iterator迭代器遍历“第一步”得到的集合。
3.遍历TreeMap的值
第一步:根据value()获取TreeMap的“值”的集合。
第二步:通过Iterator迭代器遍历“第一步”得到的集合。
4.9 WeakHashMap
4.9.1 介绍
1. WeakHashMap 继承于AbstractMap,实现了Map接口。
2. 和HashMap一样,WeakHashMap 也是一个散列表,它存储的内容也是键值对(key-value)映射,而且键和值都可以是null。
3. WeakHashMap的键是“弱键”。在 WeakHashMap 中,当某个键不再正常使用时,会被从WeakHashMap中被自动移除。更精确地说,对于一个给定的键,其映射的存在并不阻止垃圾回收器对该键的回收,某个键被终止时,它对应的键值对也就从映射中有效地移除了。
PS:“弱键”是怎么实现的?
通过WeakReference和ReferenceQueue实现的。 WeakHashMap的key是“弱键”,即是WeakReference类型的;ReferenceQueue是一个队列,它会保存被GC回收的“弱键”。
具体实现步骤是:
(01) 新建WeakHashMap,将“键值对”添加到WeakHashMap中。实际上,WeakHashMap是通过数组table保存Entry(键值对);每一个Entry实际上是一个单向链表,即Entry是键值对链表。
(02) 当某“弱键”不再被其它对象引用,并被GC回收时,这个“弱键”也同时会被添加到ReferenceQueue(queue)队列中。
(03) 当下一次我们需要操作WeakHashMap时,会先同步table和queue。table中保存了全部的键值对,而queue中保存被GC回收的键值对;同步它们,就是删除table中被GC回收的键值对。
4. 和HashMap一样,WeakHashMap是不同步的。
5. 主要应用场景:做缓存。
6. WeakHashMap和HashMap都是通过"拉链法"实现的散列表。它们的源码绝大部分内容都一样。
4.10 HashSet
4.10.1 介绍
1. HashSet 是一个没有重复元素的集合。
2. HashSet允许使用 null 元素。
3. HashSet是非同步的。
4. HashSet底层是由HashMap提供支持的,其实就是定义一个私有的HashMap变量,但HashSet中只需要用到key,而HashMap是key-value键值对,因此向map中添加键值对时,值固定用Object类型的叫PRESENT的常量代替。
5. HashSet其实就是利用了HashMap的key的唯一性。
4.11 TreeSet
4.11.1 介绍
1. TreeSet 是一个有序的集合,它的作用是提供有序的Set集合,比如提取比某个值大的子集合,提取在某一范围内的子集合等有序才具有的操作。
2. TreeSet 继承于AbstractSet,所以它是一个Set集合,具有Set的属性和方法。
3. TreeSet 实现了NavigableSet接口,意味着它支持一系列的导航方法。比如查找与指定目标最匹配项。
4. TreeSet 实现了Cloneable接口,意味着它能被克隆。
5. TreeSet 实现了java.io.Serializable接口,意味着它支持序列化。
6. TreeSet的底层是基于TreeMap实现的。Set中的元素其实就是Map的Key;value用一个Object类型的叫做PRESENT的常量代替。
7. TreeSet中的元素支持2种排序方式:自然排序或者实现Comparable接口(要求compareTo和equals的返回结果一致)。
8. TreeSet为基本操作(add、remove 和 contains)提供受保证的 log(n) 时间开销。
9. TreeSet是非同步的,它的iterator方法返回的迭代器是fail-fast的。
4.12 Queue
单向队列接口,队头取出,队尾加入。实现子类是LinkedList
4.13 Deque
双向队列接口,双向添加删除,分别有两个方向的迭代器。继承自Queue,实现子类也是LinkedList
五、一些集合类的比较
5.1 List总结
1.ArrayList, LinkedList, Vector, Stack是List的4个实现类。
(1)ArrayList 是一个数组队列,相当于动态数组。它由数组实现,随机访问效率高,随机插入、随机删除效率低。
(2)LinkedList 是一个双向链表。它也可以被当作堆栈、队列或双端队列进行操作。LinkedList随机访问效率低,但随机插入、随机删除效率低。
(3)Vector 是矢量队列,和ArrayList一样,它也是一个动态数组,由数组实现。但是ArrayList是非线程安全的,而Vector是线程安全的。
(4)Stack 是栈,它继承于Vector。它的特性是:先进后出(FILO, First In Last Out)。
2.使用场景
如果涉及到“栈”、“队列”、“链表”等操作,应该考虑用List,具体的选择哪个List,根据下面的标准来取舍:
(01) 对于需要快速插入、删除元素,应该使用LinkedList。
(02) 对于需要快速随机访问元素,应该使用ArrayList。
(03) 对于“单线程环境” 或者 “多线程环境,但List仅仅只会被单个线程操作”,此时应该使用非同步的类(如ArrayList)。
(04) 对于“多线程环境,且List可能同时被多个线程操作”,此时,应该使用同步的类(如Vector)。
3.Vector和ArrayList比较
相同之处:
- 它们都是List。
- 它们都实现了RandomAccess(支持快速随机访问)和Cloneable接口。
- 它们都是通过数组实现的,本质上都是动态数组。
- 它们的默认数组容量是10。
- 它们都支持Iterator和listIterator遍历。
不同之处:
- 线程安全性不一样,Vector安全,ArrayList不安全。
- 对序列化支持不同,Vector不支持,ArrayList支持(实现Serializable接口)。
- 构造函数个数不同,ArrayList有3个,Vector有4个(指定容量增加系数)。
- 容量增加方式不同:
- ArrayList容量不足时,新的容量 = (原始容量x 3) / 2 + 1
- Vector的容量增长与“增长系数有关”,若指定了“增长系数”,且“增长系数有效(即,大于0)”,那么,每次容量不足时,新的容量 = 原始容量 + 增长系数。若增长系数无效(即,小于/等于0),则 新的容量 = 原始容量 x 2。
- 对Enumeration的支持不同。Vector支持通过Enumeration去遍历,而ArrayList不支持。
5.2 Map总结
1.HashMap, Hashtable, TreeMap, WeakHashMap这4个类是“键值对”映射的实现类。
(1)HashMap 是基于“拉链法”实现的散列表。一般用于单线程场景。
(2)Hashtable 也是基于“拉链法”实现的散列表。它一般用于多线程场景。
(3)WeakHashMap 也是基于“拉链法”实现的散列表,它一般也用于单线程场景。相比HashMap,WeakHashMap中的键是“弱键”,当“弱键”被GC回收时,它对应的键值对也会被从WeakHashMap中删除;而HashMap中的键是强键。
(4)TreeMap 是有序的散列表,它是通过红黑树实现的。它一般用于单线程中存储有序的映射。
2.HashMap和Hashtable比较
相同之处:
(1)存储方式
HashMap和Hashtable都是存储“键值对(key-value)”的散列表,而且都采用拉链法实现。具体思想都是:通过table数组存储,数组的每一个元素都是一个Entry;而一个Entry就是一个单向链表,Entry链表中的每一个节点就保存了key-value键值对数据。
(2)添加key-value键值对
首先,根据key值计算出哈希值,再计算出数组索引(即,该key-value在table中的索引)。然后,根据数组索引找到Entry(即,单向链表),再遍历单向链表,将key和链表中的每一个节点的key进行对比。若key已经存在Entry链表中,则用该value值取代旧的value值;若key不存在Entry链表中,则新建一个key-value节点,并将该节点插入Entry链表的表头位置。
(3)删除key-value键值对
删除键值对,相比于“添加键值对”来说,简单很多。首先,还是根据key计算出哈希值,再计算出数组索引(即,该key-value在table中的索引)。然后,根据索引找出Entry(即,单向链表)。若节点key-value存在与链表Entry中,则删除链表中的节点即可。
不同之处:
(1)继承和实现方式不同
HashMap 继承于AbstractMap,实现了Map、Cloneable、java.io.Serializable接口。
Hashtable 继承于Dictionary,实现了Map、Cloneable、java.io.Serializable接口。
Dictionary一般是通过Enumeration(枚举类)去遍历,Map则是通过Iterator(迭代器)去遍历。 然而由于Hashtable也实现了Map接口,所以,它即支持Enumeration遍历,也支持Iterator遍历,而HashMap 仅支持Iterator遍历。
(2)线程安全不同
Hashtable的几乎所有函数都是同步的,即它是线程安全的,支持多线程。
而HashMap的函数则是非同步的,它不是线程安全的。若要在多线程中使用HashMap,需要我们进行额外的同步处理。 对HashMap的同步处理可以使用Collections类提供的synchronizedMap静态方法,或者直接使用JDK 5.0之后提供的java.util.concurrent包里的ConcurrentHashMap类。
(3)对null值的处理不同
Hashtable的key、value都不可以为null。否则,会抛出异常NullPointerException。
HashMap的key、value都可以为null。当HashMap的key为null时,HashMap会将其固定的插入table[0]位置(即HashMap散列表的第一个位置);而且table[0]处只会容纳一个key为null的值,当有多个key为null的值插入的时候,table[0]会保留最后插入的value。
(4)支持的遍历种类不同
HashMap只支持Iterator(迭代器)遍历。
Hashtable支持Iterator(迭代器)和Enumeration(枚举器)两种方式遍历。
(5)通过Iterator迭代器遍历时,遍历的顺序不同
HashMap是“从前向后”的遍历数组;再对数组具体某一项对应的链表,从表头开始进行遍历。
Hashtable是“从后往前”的遍历数组;再对数组具体某一项对应的链表,从表头开始进行遍历。
(6)容量的初始值 和 增加方式都不一样
HashMap默认的容量大小是16;增加容量时,每次将容量变为“原始容量x2”。
Hashtable默认的容量大小是11;增加容量时,每次将容量变为“原始容量x2 + 1”。
PS:何时增加容量?实际容量 >= 阈值(阈值 = 总的容量 * 加载因子)
(7)添加key-value时的hash值算法不同
HashMap添加元素时,是使用自定义的哈希算法。
Hashtable没有自定义哈希算法,而直接采用的key的hashCode()。
(8)部分API不同
Hashtable支持contains(Object value)方法,而且重写了toString()方法;
HashMap不支持contains(Object value)方法,没有重写toString()方法。
3.HashMap和WeakHashMap比较
相同之处:
(1)它们都是散列表,存储的是“键值对”映射。
(2)它们都继承于AbstractMap,并且实现Map基础。
(3)它们的构造函数都一样,都包括4个构造函数,而且函数的参数都一样。
(4)默认的容量大小是16,默认的加载因子是0.75。
(5)它们的“键”和“值”都允许为null。
(6)它们都是“非同步的”。
不同之处:
(1)实现接口不同
HashMap实现了Cloneable和Serializable接口,它能通过clone()克隆自己、支持序列化。而WeakHashMap没有实现这两个接口。
(2)HashMap的键是强引用(StrongReference),而WeakHashMap的键是弱引用(WeakReference)。