点击蓝色“Java建设者 ”关注我哟java
加个“星标”,及时阅读最新技术文章node
这篇文章历通过 5 次的打磨和修复,只为把最好的文章为你们分享。
web
集合在咱们平常开发使用的次数数不胜数,ArrayList
/LinkedList
/HashMap
/HashSet
······信手拈来,抬手就拿来用,在 IDE 上龙飞凤舞,可是做为一名合格的优雅的程序猿,仅仅了解怎么使用API
是远远不够的,若是在调用API
时,知道它内部发生了什么事情,就像开了透视
外挂同样,洞穿一切,这种感受才真的爽,并且这样就不是集合提供什么功能给咱们使用,而是咱们选择使用它的什么功能了。面试

这是Java建设者第 108 篇原创文章

1数组
集合框架总览 缓存
下图堪称集合框架的上帝视角,讲到集合框架不得不看的就是这幅图,固然,你会以为眼花缭乱,不知如何看起,这篇文章带你一步一步地秒杀上面的每个接口、抽象类和具体类。咱们将会从最顶层的接口开始讲起,一步一步往下深刻,帮助你把对集合的认知构建起一个知识网络。安全

工欲善其事必先利其器,让咱们先来过一遍整个集合框架的组成部分:服务器
-
集合框架提供了两个遍历接口: Iterator
和ListIterator
,其中后者是前者的优化版
,支持在任意一个位置进行先后双向遍历。注意图中的Collection
应当继承的是Iterable
而不是Iterator
,后面会解释Iterable
和Iterator
的区别 -
整个集合框架分为两个门派(类型): Collection
和Map
,前者是一个容器,存储一系列的对象;后者是键值对<key, value>
,存储一系列的键值对 -
在集合框架体系下,衍生出四种具体的集合类型: Map
、Set
、List
、Queue
-
Map
存储<key,value>
键值对,查找元素时经过key
查找value
-
Set
内部存储一系列不可重复的对象,且是一个无序集合,对象排列顺序不一 -
List
内部存储一系列可重复的对象,是一个有序集合,对象按插入顺序排列 -
Queue
是一个队列容器,其特性与List
相同,但只能从队头
和队尾
操做元素 -
JDK 为集合的各类操做提供了两个工具类 Collections
和Arrays
,以后会讲解工具类的经常使用方法 -
四种抽象集合类型内部也会衍生出许多具备不一样特性的集合类,不一样场景下择优使用,没有最佳的集合
上面了解了整个集合框架体系的组成部分,接下来的章节会严格按照上面罗列的顺序进行讲解,每一步都会有承上启下
的做用微信
学习
Set
前,最好最好要先学习Map
,由于Set
的操做本质上是对Map
的操做,往下看准没错网络
Iterator Iterable ListIterator
在第一次看这两个接口,真觉得是如出一辙的,没发现里面有啥不一样,存在即合理,它们两个仍是有本质上的区别的。
首先来看Iterator
接口:
public interface Iterator<E> {
boolean hasNext();
E next();
void remove();
}
提供的API接口含义以下:
-
hasNext()
:判断集合中是否存在下一个对象 -
next()
:返回集合中的下一个对象,并将访问指针移动一位 -
remove()
:删除集合中调用next()
方法返回的对象
在早期,遍历集合的方式只有一种,经过Iterator
迭代器操做
List<Integer> list = new ArrayList<>();
list.add(1);
list.add(2);
list.add(3);
Iterator iter = list.iterator();
while (iter.hasNext()) {
Integer next = iter.next();
System.out.println(next);
if (next == 2) { iter.remove(); }
}
再来看Iterable
接口:
public interface Iterable<T> {
Iterator<T> iterator();
// JDK 1.8
default void forEach(Consumer<? super T> action) {
Objects.requireNonNull(action);
for (T t : this) {
action.accept(t);
}
}
}
能够看到Iterable
接口里面提供了Iterator
接口,因此实现了Iterable
接口的集合依旧可使用迭代器
遍历和操做集合中的对象;
而在 JDK 1.8
中,Iterable
提供了一个新的方法forEach()
,它容许使用加强 for 循环遍历对象。
List<Integer> list = new ArrayList<>();
for (Integer num : list) {
System.out.println(num);
}
咱们经过命令:javap -c
反编译上面的这段代码后,发现它只是 Java 中的一个语法糖
,本质上仍是调用Iterator
去遍历。

翻译成代码,就和一开始的Iterator
迭代器遍历方式基本相同了。
Iterator iter = list.iterator();
while (iter.hasNext()) {
Integer num = iter.next();
System.out.println(num);
}
还有更深层次的探讨:为何要设计两个接口
Iterable
和Iterator
,而不是保留其中一个就能够了。简单讲解:
Iterator
的保留可让子类去实现本身的迭代器,而Iterable
接口更加关注于for-each
的加强语法。具体可参考:Java中的Iterable与Iterator详解
关于Iterator
和Iterable
的讲解告一段落,下面来总结一下它们的重点:
-
Iterator
是提供集合操做内部对象的一个迭代器,它能够遍历、移除对象,且只可以单向移动 -
Iterable
是对Iterator
的封装,在JDK 1.8
时,实现了Iterable
接口的集合可使用加强 for 循环遍历集合对象,咱们经过反编译后发现底层仍是使用Iterator
迭代器进行遍历
等等,这一章还没完,还有一个ListIterator
。它继承 Iterator 接口,在遍历List
集合时能够从任意索引下标开始遍历,并且支持双向遍历。
ListIterator 存在于 List 集合之中,经过调用方法能够返回起始下标为 index
的迭代器
List<Integer> list = new ArrayList<>();
// 返回下标为0的迭代器
ListIterator<Integer> listIter1 = list.listIterator();
// 返回下标为5的迭代器
ListIterator<Integer> listIter2 = list.listIterator(5);
ListIterator 中有几个重要方法,大多数方法与 Iterator 中定义的含义相同,可是比 Iterator 强大的地方是能够在任意一个下标位置返回该迭代器,且能够实现双向遍历。
public interface ListIterator<E> extends Iterator<E> {
boolean hasNext();
E next();
boolean hasPrevious();
E previous();
int nextIndex();
int previousIndex();
void remove();
// 替换当前下标的元素,即访问过的最后一个元素
void set(E e);
void add(E e);
}
Map 和 Collection 接口
Map 接口和 Collection 接口是集合框架体系的两大门派,Collection 是存储元素自己,而 Map 是存储<key, value>
键值对,在 Collection 门派下有一小部分弟子去偷师
,利用 Map 门派下的弟子来修炼本身。
是否是听的一头雾水哈哈哈,举个例子你就懂了:HashSet
底层利用了HashMap
,TreeSet
底层用了TreeMap
,LinkedHashSet
底层用了LinkedHashMap
。
下面我会详细讲到各个具体集合类哦,因此在这里,咱们先从总体上了解这两个门派
的特色和区别。

接口定义了存储的数据结构是
Map<key, value>
形式,根据 key 映射到 value,一个 key 对应一个 value ,因此key
不可重复,而value
可重复。
在Map
接口下会将存储的方式细分为不一样的种类:
-
SortedMap
接口:该类映射能够对<key, value>
按照本身的规则进行排序,具体实现有 TreeMap -
AbsractMap
:它为子类提供好一些通用的API实现,全部的具体Map如HashMap
都会继承它
而Collection
接口提供了全部集合的通用方法(注意这里不包括Map
):
-
添加方法: add(E e)
/addAll(Collection<? extends E> var1)
-
删除方法: remove(Object var1)
/removeAll(Collection<?> var1)
-
查找方法: contains(Object var1)
/containsAll(Collection<?> var1);
-
查询集合自身信息: size()
/isEmpty()
-
···
在Collection
接口下,一样会将集合细分为不一样的种类:
-
Set
接口:一个不容许存储重复元素的无序集合,具体实现有HashSet
/TreeSet
··· -
List
接口:一个可存储重复元素的有序集合,具体实现有ArrayList
/LinkedList
··· -
Queue
接口:一个可存储重复元素的队列,具体实现有PriorityQueue
/ArrayDeque
···

2
Map 集合体系详解
Map
接口是由<key, value>
组成的集合,由key
映射到惟一的value
,因此Map
不能包含重复的key
,每一个键至多映射一个值。下图是整个 Map 集合体系的主要组成部分,我将会按照平常使用频率从高到低一一讲解。
不得不提的是 Map 的设计理念:定位元素的时间复杂度优化到 O(1)
Map 体系下主要分为 AbstractMap 和 SortedMap两类集合
AbstractMap
是对 Map 接口的扩展,它定义了普通的 Map 集合具备的通用行为,能够避免子类重复编写大量相同的代码,子类继承 AbstractMap 后能够重写它的方法,实现额外的逻辑,对外提供更多的功能。
SortedMap
定义了该类 Map 具备 排序
行为,同时它在内部定义好有关排序的抽象方法,当子类实现它时,必须重写全部方法,对外提供排序功能。
HashMap
HashMap 是一个最通用的利用哈希表存储元素的集合,将元素放入 HashMap 时,将key
的哈希值转换为数组的索引
下标肯定存放位置,查找时,根据key
的哈希地址转换成数组的索引
下标肯定查找位置。
HashMap 底层是用数组 + 链表 + 红黑树这三种数据结构实现,它是非线程安全的集合。

发送哈希冲突时,HashMap 的解决方法是将相同映射地址的元素连成一条链表
,若是链表的长度大于8
时,且数组的长度大于64
则会转换成红黑树
数据结构。
关于 HashMap 的简要总结:
-
它是集合中最经常使用的 Map
集合类型,底层由数组 + 链表 + 红黑树
组成 -
HashMap不是线程安全的 -
插入元素时,经过计算元素的 哈希值
,经过哈希映射函数转换为数组下标
;查找元素时,一样经过哈希映射函数获得数组下标定位元素的位置
LinkedHashMap
LinkedHashMap 能够看做是 HashMap
和 LinkedList
的结合:它在 HashMap 的基础上添加了一条双向链表,默认
存储各个元素的插入顺序,但因为这条双向链表,使得 LinkedHashMap 能够实现 LRU
缓存淘汰策略,由于咱们能够设置这条双向链表按照元素的访问次序
进行排序

LinkedHashMap 是 HashMap 的子类,因此它具有 HashMap 的全部特色,其次,它在 HashMap 的基础上维护了一条双向链表
,该链表存储了全部元素,默认
元素的顺序与插入顺序一致。若accessOrder
属性为true
,则遍历顺序按元素的访问次序进行排序。
// 头节点
transient LinkedHashMap.Entry<K, V> head;
// 尾结点
transient LinkedHashMap.Entry<K, V> tail;
利用 LinkedHashMap 能够实现 LRU
缓存淘汰策略,由于它提供了一个方法:
protected boolean removeEldestEntry(java.util.Map.Entry<K, V> eldest) {
return false;
}
该方法能够移除最靠近链表头部
的一个节点,而在get()
方法中能够看到下面这段代码,其做用是挪动结点的位置:
if (this.accessOrder) {
this.afterNodeAccess(e);
}
只要调用了get()
且accessOrder = true
,则会将该节点更新到链表尾部
,具体的逻辑在afterNodeAccess()
中,感兴趣的可翻看源码,篇幅缘由这里再也不展开。
如今若是要实现一个LRU
缓存策略,则须要作两件事情:
-
指定 accessOrder = true
能够设定链表按照访问顺序排列,经过提供的构造器能够设定accessOrder
public LinkedHashMap(int initialCapacity, float loadFactor, boolean accessOrder) {
super(initialCapacity, loadFactor);
this.accessOrder = accessOrder;
}
-
重写 removeEldestEntry()
方法,内部定义逻辑,一般是判断容量
是否达到上限,如果则执行淘汰。
这里就要贴出一道大厂面试必考题目:146. LRU缓存机制,只要跟着个人步骤,就能顺利完成这道大厂题了。
关于 LinkedHashMap 主要介绍两点:
-
它底层维护了一条 双向链表
,由于继承了 HashMap,因此它也不是线程安全的 -
LinkedHashMap 可实现 LRU
缓存淘汰策略,其原理是经过设置accessOrder
为true
并重写removeEldestEntry
方法定义淘汰元素时需知足的条件
TreeMap
TreeMap 是 SortedMap
的子类,因此它具备排序功能。它是基于红黑树
数据结构实现的,每个键值对<key, value>
都是一个结点,默认状况下按照key
天然排序,另外一种是能够经过传入定制的Comparator
进行自定义规则排序。
// 按照 key 天然排序,Integer 的天然排序是升序
TreeMap<Integer, Object> naturalSort = new TreeMap<>();
// 定制排序,按照 key 降序排序
TreeMap<Integer, Object> customSort = new TreeMap<>((o1, o2) -> Integer.compare(o2, o1));
TreeMap 底层使用了数组+红黑树实现,因此里面的存储结构能够理解成下面这幅图哦。

图中红黑树的每个节点都是一个Entry
,在这里为了图片的简洁性,就不标明 key 和 value 了,注意这些元素都是已经按照key
排好序了,整个数据结构都是保持着有序
的状态!
关于天然
排序与定制
排序:
-
天然排序:要求 key
必须实现Comparable
接口。
因为Integer
类实现了 Comparable 接口,按照天然排序规则是按照key
从小到大排序。
TreeMap<Integer, String> treeMap = new TreeMap<>();
treeMap.put(2, "TWO");
treeMap.put(1, "ONE");
System.out.print(treeMap);
// {1=ONE, 2=TWO}
-
定制排序:在初始化 TreeMap 时传入新的 Comparator
,不要求key
实现 Comparable 接口
TreeMap<Integer, String> treeMap = new TreeMap<>((o1, o2) -> Integer.compare(o2, o1));
treeMap.put(1, "ONE");
treeMap.put(2, "TWO");
treeMap.put(4, "FOUR");
treeMap.put(3, "THREE");
System.out.println(treeMap);
// {4=FOUR, 3=THREE, 2=TWO, 1=ONE}
经过传入新的Comparator
比较器,能够覆盖默认的排序规则,上面的代码按照key
降序排序,在实际应用中还能够按照其它规则自定义排序。
compare()
方法的返回值有三种,分别是:0
,-1
,+1
(1)若是返回0
,表明两个元素相等,不须要调换顺序
(2)若是返回+1
,表明前面的元素须要与后面的元素调换位置
(3)若是返回-1
,表明前面的元素不须要与后面的元素调换位置
而什么时候返回+1
和-1
,则由咱们本身去定义,JDK默认是按照天然排序,而咱们能够根据key
的不一样去定义降序仍是升序排序。
关于 TreeMap 主要介绍了两点:
-
它底层是由 红黑树
这种数据结构实现的,因此操做的时间复杂度恒为O(logN)
-
TreeMap 能够对 key
进行天然排序或者自定义排序,自定义排序时须要传入Comparator
,而天然排序要求key
实现了Comparable
接口 -
TreeMap 不是线程安全的。
WeakHashMap
WeakHashMap 平常开发中比较少见,它是基于普通的Map
实现的,而里面Entry
中的键在每一次的垃圾回收
都会被清除掉,因此很是适合用于短暂访问、仅访问一次的元素,缓存在WeakHashMap
中,并尽早地把它回收掉。
当Entry
被GC
时,WeakHashMap 是如何感知到某个元素被回收的呢?
在 WeakHashMap 内部维护了一个引用队列queue
private final ReferenceQueue<Object> queue = new ReferenceQueue<>();
这个 queue 里包含了全部被GC
掉的键,当JVM开启GC
后,若是回收掉 WeakHashMap 中的 key,会将 key 放入queue 中,在expungeStaleEntries()
中遍历 queue,把 queue 中的全部key
拿出来,并在 WeakHashMap 中删除掉,以达到同步。
private void expungeStaleEntries() {
for (Object x; (x = queue.poll()) != null; ) {
synchronized (queue) {
// 去 WeakHashMap 中删除该键值对
}
}
}
再者,须要注意 WeakHashMap 底层存储的元素的数据结构是数组 + 链表
,没有红黑树哦,能够换一个角度想,若是还有红黑树,那干脆直接继承 HashMap ,而后再扩展就完事了嘛,然而它并无这样作:
public class WeakHashMap<K, V> extends AbstractMap<K, V> implements Map<K, V> {
}
因此,WeakHashMap 的数据结构图我也为你准备好啦。

图中被虚线标识的元素将会在下一次访问 WeakHashMap 时被删除掉,WeakHashMap 内部会作好一系列的调整工做,因此记住队列的做用就是标志那些已经被GC
回收掉的元素。
关于 WeakHashMap 须要注意两点:
-
它的键是一种弱键,放入 WeakHashMap 时,随时会被回收掉,因此不能确保某次访问元素必定存在 -
它依赖普通的 Map
进行实现,是一个非线程安全的集合 -
WeakHashMap 一般做为缓存使用,适合存储那些只需访问一次、或只需保存短暂时间的键值对
Hashtable
Hashtable 底层的存储结构是数组 + 链表
,而它是一个线程安全的集合,可是由于这个线程安全,它就被淘汰掉了。
下面是Hashtable存储元素时的数据结构图,它只会存在数组+链表,当链表过长时,查询的效率太低,并且会长时间锁住 Hashtable。

这幅图是否有点眼熟哈哈哈哈,本质上就是 WeakHashMap 的底层存储结构了。你千万别问为何 WeakHashMap 不继承 Hashtable 哦,Hashtable 的
性能
在并发环境下很是差,在非并发环境下能够用HashMap
更优。
HashTable 本质上是 HashMap 的前辈,它被淘汰的缘由也主要由于两个字:性能
HashTable 是一个 线程安全 的 Map,它全部的方法都被加上了 synchronized 关键字,也是由于这个关键字,它注定成为了时代的弃儿。
HashTable 底层采用 数组+链表 存储键值对,因为被弃用,后人也没有对它进行任何改进
HashTable 默认长度为 11
,负载因子为 0.75F
,即元素个数达到数组长度的 75% 时,会进行一次扩容,每次扩容为原来数组长度的 2
倍
HashTable 全部的操做都是线程安全的。

3
Collection 集合体系详解
Collection 集合体系的顶层接口就是Collection
,它规定了该集合下的一系列行为约定。
该集合下能够分为三大类集合:List,Set和Queue
Set
接口定义了该类集合不容许存储重复的元素,且任何操做时均须要经过哈希函数映射到集合内部定位元素,集合内部的元素默认是无序的。
List
接口定义了该类集合容许存储重复的元素,且集合内部的元素按照元素插入的顺序有序排列,能够经过索引访问元素。
Queue
接口定义了该类集合是以队列
做为存储结构,因此集合内部的元素有序排列,仅能够操做头结点元素,没法访问队列中间的元素。
上面三个接口是最普通,最抽象的实现,而在各个集合接口内部,还会有更加具体的表现,衍生出各类不一样的额外功能,使开发者可以对比各个集合的优点,择优使用。

Set 接口
Set
接口继承了Collection
接口,是一个不包括重复元素的集合,更确切地说,Set 中任意两个元素不会出现 o1.equals(o2)
,并且 Set 至多只能存储一个 NULL
值元素,Set 集合的组成部分能够用下面这张图归纳:

在 Set 集合体系中,咱们须要着重关注两点:
-
存入可变元素时,必须很是当心,由于任意时候元素状态的改变都有可能使得 Set 内部出现两个相等的元素,即
o1.equals(o2) = true
,因此通常不要更改存入 Set 中的元素,不然将会破坏了equals()
的做用! -
Set 的最大做用就是判重,在项目中最大的做用也是判重!
接下来咱们去看它的实现类和子类: AbstractSet
和 SortedSet
AbstractSet 抽象类
AbstractSet
是一个实现 Set 的一个抽象类,定义在这里能够将全部具体 Set 集合的相同行为在这里实现,避免子类包含大量的重复代码
全部的 Set 也应该要有相同的 hashCode()
和 equals()
方法,因此使用抽象类把该方法重写后,子类无需关心这两个方法。
public abstract class AbstractSet<E> implements Set<E> {
// 判断两个 set 是否相等
public boolean equals(Object o) {
if (o == this) { // 集合自己
return true;
} else if (!(o instanceof Set)) { // 集合不是 set
return false;
} else {
// 比较两个集合的元素是否所有相同
}
}
// 计算全部元素的 hashcode 总和
public int hashCode() {
int h = 0;
Iterator i = this.iterator();
while(i.hasNext()) {
E obj = i.next();
if (obj != null) {
h += obj.hashCode();
}
}
return h;
}
}
SortedSet 接口
SortedSet
是一个接口,它在 Set 的基础上扩展了排序的行为,因此全部实现它的子类都会拥有排序功能。
public interface SortedSet<E> extends Set<E> {
// 元素的比较器,决定元素的排列顺序
Comparator<? super E> comparator();
// 获取 [var1, var2] 之间的 set
SortedSet<E> subSet(E var1, E var2);
// 获取以 var1 开头的 Set
SortedSet<E> headSet(E var1);
// 获取以 var1 结尾的 Set
SortedSet<E> tailSet(E var1);
// 获取首个元素
E first();
// 获取最后一个元素
E last();
}
HashSet
HashSet 底层借助 HashMap
实现,咱们能够观察它的多个构造方法,本质上都是 new 一个 HashMap
这也是这篇文章为何先讲解 Map 再讲解 Set 的缘由!先学习 Map,有助于理解 Set
public class HashSet<E> extends AbstractSet<E> implements Set<E>, Cloneable, Serializable {
public HashSet() {
this.map = new HashMap();
}
public HashSet(int initialCapacity, float loadFactor) {
this.map = new HashMap(initialCapacity, loadFactor);
}
public HashSet(int initialCapacity) {
this.map = new HashMap(initialCapacity);
}
}
咱们能够观察 add()
方法和remove()
方法是如何将 HashSet 的操做嫁接到 HashMap 的。
private static final Object PRESENT = new Object();
public boolean add(E e) {
return this.map.put(e, PRESENT) == null;
}
public boolean remove(Object o) {
return this.map.remove(o) == PRESENT;
}
咱们看到 PRESENT
就是一个静态常量:使用 PRESENT 做为 HashMap 的 value 值,使用HashSet的开发者只需关注于须要插入的 key
,屏蔽了 HashMap 的 value

上图能够观察到每一个Entry
的value
都是 PRESENT 空对象,咱们就不用再理会它了。
HashSet 在 HashMap 基础上实现,因此不少地方能够联系到 HashMap:
-
底层数据结构:HashSet 也是采用 数组 + 链表 + 红黑树
实现 -
线程安全性:因为采用 HashMap 实现,而 HashMap 自己线程不安全,在HashSet 中没有添加额外的同步策略,因此 HashSet 也线程不安全 -
存入 HashSet 的对象的状态最好不要发生变化,由于有可能改变状态后,在集合内部出现两个元素 o1.equals(o2)
,破坏了equals()
的语义。
LinkedHashSet
LinkedHashSet 的代码少的可怜,不信我给你我粘出来

少归少,仍是不能闹,LinkedHashSet
继承了HashSet
,咱们跟随到父类 HashSet 的构造方法看看
HashSet(int initialCapacity, float loadFactor, boolean dummy) {
this.map = new LinkedHashMap(initialCapacity, loadFactor);
}
发现父类中 map 的实现采用LinkedHashMap
,这里注意不是HashMap
,而 LinkedHashMap 底层又采用 HashMap + 双向链表 实现的,因此本质上 LinkedHashSet 仍是使用 HashMap 实现的。
LinkedHashSet -> LinkedHashMap -> HashMap + 双向链表

而 LinkedHashMap 是采用 HashMap
和双向链表
实现的,这条双向链表中保存了元素的插入顺序。因此 LinkedHashSet 能够按照元素的插入顺序遍历元素,若是你熟悉LinkedHashMap
,那 LinkedHashSet 也就更不在话下了。
关于 LinkedHashSet 须要注意几个地方:
-
它继承了 HashSet
,而 HashSet 默认是采用 HashMap 存储数据的,可是 LinkedHashSet 调用父类构造方法初始化 map 时是 LinkedHashMap 而不是 HashMap,这个要额外注意一下 -
因为 LinkedHashMap 不是线程安全的,且在 LinkedHashSet 中没有添加额外的同步策略,因此 LinkedHashSet 集合也不是线程安全的
TreeSet
TreeSet 是基于 TreeMap 的实现,因此存储的元素是有序的,底层的数据结构是数组 + 红黑树
。

而元素的排列顺序有2
种,和 TreeMap 相同:天然排序和定制排序,经常使用的构造方法已经在下面展现出来了,TreeSet 默认按照天然排序,若是须要定制排序,须要传入Comparator
。
public TreeSet() {
this(new TreeMap<E,Object>());
}
public TreeSet(Comparator<? super E> comparator) {
this(new TreeMap<>(comparator));
}
TreeSet 应用场景有不少,像在游戏里的玩家战斗力排行榜
public class Player implements Comparable<Integer> {
public String name;
public int score;
@Override
public int compareTo(Student o) {
return Integer.compareTo(this.score, o.score);
}
}
public static void main(String[] args) {
Player s1 = new Player("张三", 100);
Player s2 = new Player("李四", 90);
Player s3 = new Player("王五", 80);
TreeSet<Player> set = new TreeSet();
set.add(s2); set.add(s1); set.add(s3);
System.out.println(set);
}
// [Student{name='王五', score=80}, Student{name='李四', score=90}, Student{name='张三', score=100}]
对 TreeSet 介绍了它的主要实现方式和应用场景,有几个值得注意的点。
-
TreeSet 的全部操做都会转换为对 TreeMap 的操做,TreeMap 采用红黑树实现,任意操做的平均时间复杂度为 O(logN)
-
TreeSet 是一个线程不安全的集合 -
TreeSet 常应用于对不重复的元素定制排序,例如玩家战力排行榜
注意:TreeSet判断元素是否重复的方法是判断compareTo()方法是否返回0,而不是调用 hashcode() 和 equals() 方法,若是返回 0 则认为集合内已经存在相同的元素,不会再加入到集合当中。

4
List 接口
List 接口和 Set 接口齐头并进,是咱们平常开发中接触的不少的一种集合类型了。整个 List 集合的组成部分以下图
List
接口直接继承 Collection 接口,它定义为能够存储重复元素的集合,而且元素按照插入顺序有序排列,且能够经过索引访问指定位置的元素。常见的实现有:ArrayList、LinkedList、Vector和Stack
AbstractList 和 AbstractSequentialList
AbstractList 抽象类实现了 List 接口,其内部实现了全部的 List 都需具有的功能,子类能够专一于实现本身具体的操做逻辑。
// 查找元素 o 第一次出现的索引位置
public int indexOf(Object o)
// 查找元素 o 最后一次出现的索引位置
public int lastIndexOf(Object o)
//···
AbstractSequentialList 抽象类继承了 AbstractList,在原基础上限制了访问元素的顺序只可以按照顺序访问,而不支持随机访问,若是须要知足随机访问的特性,则继承 AbstractList。子类 LinkedList 使用链表实现,因此仅能支持顺序访问,顾继承了 AbstractSequentialList
而不是 AbstractList。
Vector

在如今已是一种过期的集合了,包括继承它的
vectorStack
集合也如此,它们被淘汰的缘由都是由于性能低下。
JDK 1.0 时代,ArrayList 还没诞生,你们都是使用 Vector 集合,但因为 Vector 的每一个操做都被 synchronized 关键字修饰,即便在线程安全的状况下,仍然进行无心义的加锁与释放锁,形成额外的性能开销,作了无用功。
public synchronized boolean add(E e);
public synchronized E get(int index);
在 JDK 1.2 时,Collection 家族出现了,它提供了大量高性能、适用於不一样场合的集合,而 Vector 也是其中一员,但因为 Vector 在每一个方法上都加了锁,因为须要兼允许多老的项目,很难在此基础上优化Vector
了,因此渐渐地也就被历史淘汰了。
如今,在线程安全的状况下,不须要选用 Vector 集合,取而代之的是 ArrayList 集合;在并发环境下,出现了 CopyOnWriteArrayList
,Vector 彻底被弃用了。
Stack

是一种
Stack后入先出(LIFO)
型的集合容器,如图中所示,大雄
是最后一个进入容器的,top指针指向大雄,那么弹出元素时,大雄也是第一个被弹出去的。
Stack 继承了 Vector 类,提供了栈顶的压入元素操做(push)和弹出元素操做(pop),以及查看栈顶元素的方法(peek)等等,但因为继承了 Vector,正所谓跟错老大没福报,Stack 也渐渐被淘汰了。
取而代之的是后起之秀 Deque
接口,其实现有 ArrayDeque
,该数据结构更加完善、可靠性更好,依靠队列也能够实现LIFO
的栈操做,因此优先选择 ArrayDeque 实现栈。
Deque<Integer> stack = new ArrayDeque<Integer>();
ArrayDeque 的数据结构是:数组
,并提供头尾指针下标对数组元素进行操做。本文也会讲到哦,客官请继续往下看,莫着急!:smile:
ArrayList
ArrayList 以数组做为存储结构,它是线程不安全的集合;具备查询快、在数组中间或头部增删慢的特色,因此它除了线程不安全这一点,其他能够替代Vector
,并且线程安全的 ArrayList 可使用 CopyOnWriteArrayList
代替 Vector。

关于 ArrayList 有几个重要的点须要注意的:
-
具有随机访问特色,访问元素的效率较高,ArrayList 在频繁插入、删除集合元素的场景下效率较
低
。 -
底层数据结构:ArrayList 底层使用数组做为存储结构,具有查找快、增删慢的特色
-
线程安全性:ArrayList 是线程不安全的集合
-
ArrayList 首次扩容后的长度为
10
,调用add()
时须要计算容器的最小容量。能够看到若是数组elementData
为空数组,会将最小容量设置为10
,以后会将数组长度完成首次扩容到 10。
// new ArrayList 时的默认空数组
private static final Object[] DEFAULTCAPACITY_EMPTY_ELEMENTDATA = {};
// 默认容量
private static final int DEFAULT_CAPACITY = 10;
// 计算该容器应该知足的最小容量
private static int calculateCapacity(Object[] elementData, int minCapacity) {
if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA) {
return Math.max(DEFAULT_CAPACITY, minCapacity);
}
return minCapacity;
}
-
集合从第二次扩容开始,数组长度将扩容为原来的 1.5
倍,即:newLength = oldLength * 1.5

LinkedList
LinkedList 底层采用双向链表
数据结构存储元素,因为链表的内存地址非连续
,因此它不具有随机访问的特色,但因为它利用指针链接各个元素,因此插入、删除元素只须要操做指针
,不须要移动元素
,故具备增删快、查询慢的特色。它也是一个非线程安全的集合。

因为以双向链表做为数据结构,它是线程不安全的集合;存储的每一个节点称为一个Node
,下图能够看到 Node 中保存了next
和prev
指针,item
是该节点的值。在插入和删除时,时间复杂度都保持为 O(1)

关于 LinkedList,除了它是以链表实现的集合外,还有一些特殊的特性须要注意的。
-
优点:LinkedList 底层没有 扩容机制
,使用双向链表
存储元素,因此插入和删除元素效率较高,适用于频繁操做元素的场景 -
劣势:LinkedList 不具有 随机访问
的特色,查找某个元素只能从head
或tail
指针一个一个比较,因此查找中间的元素时效率很低 -
查找优化:LinkedList 查找某个下标 index
的元素时作了优化,若index > (size / 2)
,则从head
日后查找,不然从tail
开始往前查找,代码以下所示:
LinkedList.Node<E> node(int index) {
LinkedList.Node x;
int i;
if (index < this.size >> 1) { // 查找的下标处于链表前半部分则从头找
x = this.first;
for(i = 0; i < index; ++i) { x = x.next; }
return x;
} else { // 查找的下标处于数组的后半部分则从尾开始找
x = this.last;
for(i = this.size - 1; i > index; --i) { x = x.prev; }
return x;
}
}
-
双端队列:使用双端链表实现,而且实现了 Deque
接口,使得 LinkedList 能够用做双端队列。下图能够看到 Node 是集合中的元素,提供了前驱指针和后继指针,还提供了一系列操做头结点
和尾结点
的方法,具备双端队列的特性。

LinkedList 集合最让人树枝的是它的链表结构,可是咱们同时也要注意它是一个双端队列型的集合。
Deque<Object> deque = new LinkedList<>();

5
Queue接口
Queue
队列,在 JDK 中有两种不一样类型的集合实现:单向队列(AbstractQueue) 和 双端队列(Deque)

Queue 中提供了两套增长、删除元素的 API,当插入或删除元素失败时,会有两种不一样的失败处理策略。
方法及失败策略 | 插入方法 | 删除方法 | 查找方法 |
---|---|---|---|
抛出异常 | add() | remove() | get() |
返回失败默认值 | offer() | poll() | peek() |
选取哪一种方法的决定因素:插入和删除元素失败时,但愿抛出异常
仍是返回布尔值
add()
和 offer()
对比:
在队列长度大小肯定的场景下,队列放满元素后,添加下一个元素时,add() 会抛出 IllegalStateException
异常,而 offer()
会返回 false
。
可是它们两个方法在插入某些不合法的元素时都会抛出三个相同的异常。

和
remove()poll()
对比:
在队列为空的场景下, remove()
会抛出 NoSuchElementException
异常,而 poll()
则返回 null
。
get()
和peek()
对比:
在队列为空的状况下,get()
会抛出NoSuchElementException
异常,而peek()
则返回null
。
Deque 接口
Deque
接口的实现很是好理解:从单向队列演变为双向队列,内部额外提供双向队列的操做方法便可:

Deque 接口额外提供了针对队列的头结点和尾结点操做的方法,而插入、删除方法一样也提供了两套不一样的失败策略。除了add()
和offer()
,remove()
和poll()
之外,还有get()
和peek()
出现了不一样的策略
AbstractQueue 抽象类
AbstractQueue 类中提供了各个 API 的基本实现,主要针对各个不一样的处理策略给出基本的方法实现,定义在这里的做用是让子类
根据其方法规范
(操做失败时抛出异常仍是返回默认值)实现具体的业务逻辑。

LinkedList
LinkedList 在上面已经详细解释了,它实现了 Deque
接口,提供了针对头结点和尾结点的操做,而且每一个结点都有前驱和后继指针,具有了双向队列的全部特性。
ArrayDeque
使用数组实现的双端队列,它是无界的双端队列,最小的容量是8
(JDK 1.8)。在 JDK 11 看到它默认容量已是 16
了。

在平常使用得很少,值得注意的是它与
ArrayDequeLinkedList
的对比:LinkedList
采用链表实现双端队列,而 ArrayDeque
使用数组实现双端队列。
在文档中做者写到:ArrayDeque 做为栈时比 Stack 性能好,做为队列时比 LinkedList 性能好
因为双端队列只能在头部和尾部操做元素,因此删除元素和插入元素的时间复杂度大部分都稳定在 O(1)
,除非在扩容时会涉及到元素的批量复制操做。可是在大多数状况下,使用它时应该指定一个大概的数组长度,避免频繁的扩容。
我的观点:链表的插入、删除操做涉及到指针的操做,我我的认为做者是以为数组下标的移动要比指针的操做要廉价,并且数组采用连续的内存地址空间,而链表元素的内存地址是不连续的,因此数组操做元素的效率在寻址上会比链表要快。请批判看待观点。
PriorityQueue
PriorityQueue 基于优先级堆实现的优先级队列,而堆是采用数组实现:

文档中的描述告诉咱们:该数组中的元素经过传入 Comparator
进行定制排序,若是不传入Comparator
时,则按照元素自己天然排序
,但要求元素实现了Comparable
接口,因此 PriorityQueue 不容许存储 NULL 元素。
PriorityQueue 应用场景:元素自己具备优先级,须要按照优先级处理元素
-
例如游戏中的VIP玩家与普通玩家,VIP 等级越高的玩家越先安排进入服务器玩耍,减小玩家流失。
public static void main(String[] args) {
Student vip1 = new Student("张三", 1);
Student vip3 = new Student("洪七", 2);
Student vip4 = new Student("老八", 4);
Student vip2 = new Student("李四", 1);
Student normal1 = new Student("王五", 0);
Student normal2 = new Student("赵六", 0);
// 根据玩家的 VIP 等级进行降序排序
PriorityQueue<Student> queue = new PriorityQueue<>((o1, o2) -> o2.getScore().compareTo(o1.getScore()));
queue.add(vip1);queue.add(vip4);queue.add(vip3);
queue.add(normal1);queue.add(normal2);queue.add(vip2);
while (!queue.isEmpty()) {
Student s1 = queue.poll();
System.out.println(s1.getName() + "进入游戏; " + "VIP等级: " + s1.getScore());
}
}
public static class Student implements Comparable<Student> {
private String name;
private Integer score;
public Student(String name, Integer score) {
this.name = name;
this.score = score;
}
@Override
public int compareTo(Student o) {
return this.score.compareTo(o.getScore());
}
}
执行上面的代码能够获得下面这种有趣的结果,能够看到氪金
令人带来快乐。

VIP 等级越高(优先级越高)就越优先安排进入游戏(优先处理),相似这种有优先级的场景还有很是多,各位能够发挥本身的想象力。
PriorityQueue 总结:
-
PriorityQueue 是基于优先级堆实现的优先级队列,而堆是用数组维护的
-
PriorityQueue 适用于元素按优先级处理的业务场景,例如用户在请求人工客服须要排队时,根据用户的VIP等级进行
插队
处理,等级越高,越先安排客服。
章节结束各集合总结:(以 JDK1.8 为例)
数据类型 | 插入、删除时间复杂度 | 查询时间复杂度 | 底层数据结构 | 是否线程安全 |
---|---|---|---|---|
Vector | O(N) | O(1) | 数组 | 是(已淘汰) |
ArrayList | O(N) | O(1) | 数组 | 否 |
LinkedList | O(1) | O(N) | 双向链表 | 否 |
HashSet | O(1) | O(1) | 数组+链表+红黑树 | 否 |
TreeSet | O(logN) | O(logN) | 红黑树 | 否 |
LinkedHashSet | O(1) | O(1)~O(N) | 数组 + 链表 + 红黑树 | 否 |
ArrayDeque | O(N) | O(1) | 数组 | 否 |
PriorityQueue | O(logN) | O(logN) | 堆(数组实现) | 否 |
HashMap | O(1) ~ O(N) | O(1) ~ O(N) | 数组+链表+红黑树 | 否 |
TreeMap | O(logN) | O(logN) | 数组+红黑树 | 否 |
HashTable | O(1) / O(N) | O(1) / O(N) | 数组+链表 | 是(已淘汰) |
文末总结
这一篇文章对各个集合都有些点到即止
的味道,此文的目的是对整个集合框架有一个较为总体的了解,分析了最经常使用的集合的相关特性,以及某些特殊集合的应用场景例如TreeSet
、TreeMap
这种可定制排序的集合。
-
Collection
接口提供了整个集合框架最通用的增删改查以及集合自身操做的抽象方法,让子类去实现 -
Set
接口决定了它的子类都是无序、无重复元素的集合,其主要实现有HashSet、TreeSet、LinkedHashSet。 -
HashSet
底层采用HashMap
实现,而TreeSet
底层使用TreeMap
实现,大部分 Set 集合的操做都会转换为 Map 的操做,TreeSet 能够将元素按照规则进行排序。 -
List
接口决定了它的子类都是有序、可存储重复元素的集合,常见的实现有 ArrayList,LinkedList,Vector -
ArrayList
使用数组实现,而 LinkedList 使用链表实现,因此它们两个的使用场景几乎是相反的,频繁查询的场景使用 ArrayList,而频繁插入删除的场景最好使用 LinkedList -
LinkedList
和ArrayDeque
均可用于双端队列,而 Josh Bloch and Doug Lea 认为ArrayDeque
具备比LinkedList
更好的性能,ArrayDeque
使用数组实现双端队列,LinkedList
使用链表实现双端队列。 -
Queue
接口定义了队列的基本操做,子类集合都会拥有队列的特性:先进先出,主要实现有:LinkedList,ArrayDeque -
PriorityQueue
底层使用二叉堆维护的优先级队列,而二叉堆是由数组实现的,它能够按照元素的优先级进行排序,优先级越高的元素,排在队列前面,优先被弹出处理。 -
Map
接口定义了该种集合类型是以<key,value>
键值对形式保存,其主要实现有:HashMap,TreeMap,LinkedHashMap,Hashtable -
LinkedHashMap 底层多加了一条双向链表,设置 accessOrder
为true
并重写方法则能够实现LRU
缓存 -
TreeMap 底层采用数组+红黑树实现,集合内的元素默认按照天然排序,也能够传入 Comparator
定制排序
看到这里很是不容易,感谢你愿意阅读个人文章,但愿能对你有所帮助,你能够参考着文末总结的顺序,每当我提到一个集合时,回想它的重要知识点是什么,主要就是底层数据结构
,线程安全性
,该集合的一两个特有性质
,只要可以回答出来个大概,我相信以后运用这些数据结构,你可以熟能生巧。
本文对整个集合体系的全部经常使用的集合类都分析了,这里并无对集合内部的实现深刻剖析,我想先从最宏观的角度让你们了解每一个集合的的做用,应用场景,以及简单的对比,以后会抽时间对常见的集合进行源码剖析,尽情期待,感谢阅读!
最后有些话想说:这篇文章花了我半个月去写,也是意义重大,多谢
cxuan
哥一直指导我写文章,一步一步地去打磨出一篇好的文章真的很是不容易,写下的每个字都可以让别人看得懂是一件很是难的事情,总结出最精华的知识分享给大家也是很是难的一件事情,希望可以一直进步下去!不忘初心,热爱分享,喜好写做
往期精彩回顾

本文分享自微信公众号 - Java建设者(javajianshe)。
若有侵权,请联系 support@oschina.cn 删除。
本文参与“OSC源创计划”,欢迎正在阅读的你也加入,一块儿分享。