JavaCollection
List
(对付顺序的好帮手): 存储的元素是有序的、可重复的。Set
(注重独一无二的性质): 存储的元素不可重复的。Queue
(实现排队功能的叫号机): 按特定的排队规则来确定先后顺序,存储的元素是有序的、可重复的。Map
(用 key 来搜索的专家): 使用键值对(key-value)存储,类似于数学上的函数 y = f(x),"x" 代表 key,"y" 代表 value,key 是无序的、不可重复的,value 是无序的、可重复的,每个键最多映射到一个值。
List
ArrayList
:Object[]
数组。Vector
:Object[]
数组。LinkedList
:双向链表(JDK1.6 之前为循环链表,JDK1.7 取消了循环)。
ArrayList
ArrayList
的底层是数组队列,相当于动态数组。与 Java 中的数组相比,它的容量能动态增长。在添加大量元素前,应用程序可以使用 ensureCapacity
操作来增加 ArrayList
实例的容量。这可以减少递增式再分配的数量。
public class ArrayList<E> extends AbstractList<E>
implements List<E>, RandomAccess, Cloneable, java.io.Serializable{}
List
: 表明它是一个列表,支持添加、删除、查找等操作,并且可以通过下标进行访问。RandomAccess
:这是一个标志接口,表明实现这个接口的List
集合是支持 快速随机访问 的。在ArrayList
中,我们即可以通过元素的序号快速获取元素对象,这就是快速随机访问。Cloneable
:表明它具有拷贝能力,可以进行深拷贝或浅拷贝操作。Serializable
: 表明它可以进行序列化操作,也就是可以将对象转换为字节流进行持久化存储或网络传输,非常方便。
ArrayList vs Vector vs Stack
ArrayList
是List
的主要实现类,底层使用Object[]
存储,适用于频繁的查找工作,线程不安全。Vector
是List
的古老实现类,底层使用Object[]
存储,线程安全。Vector
和Stack
两者都是线程安全的,都是使用synchronized
关键字进行同步处理。Stack
继承自Vector
,是一个后进先出的栈,而Vector
是一个列表。
随着 Java 并发编程的发展,
Vector
和Stack
已经被淘汰,推荐使用并发集合类(例如ConcurrentHashMap
、CopyOnWriteArrayList
等)或者手动实现线程安全的方法来提供安全的多线程操作支持。
ArrayList 扩容
- 计算新容量: 新的容量通常是旧容量的 1.5 倍(即旧容量的 50%)。这个增长率是一个折衷的选择,旨在平衡内存使用和数组复制的开销。不过,这个具体的增长率可能会根据不同的 Java 实现有所变化。
- 创建新数组: 一旦确定了新容量,
ArrayList
将创建一个新的、更大的数组来替代旧的数组。 - 复制元素: 然后,
ArrayList
使用System.arraycopy()
方法将旧数组中的所有元素复制到新数组中。 - 更新内部引用: 复制完成后,
ArrayList
更新其内部引用,使之指向新的数组。
LinkedList
LinkedList
是一个基于双向链表实现的集合类
public class LinkedList<E>
extends AbstractSequentialList<E>
implements List<E>, Deque<E>, Cloneable, java.io.Serializable
LinkedList
实现了以下接口:
List
: 表明它是一个列表,支持添加、删除、查找等操作,并且可以通过下标进行访问。Deque
:继承自Queue
接口,具有双端队列的特性,支持从两端插入和删除元素,方便实现栈和队列等数据结构。Cloneable
:表明它具有拷贝能力,可以进行深拷贝或浅拷贝操作。Serializable
: 表明它可以进行序列化操作,也就是可以将对象转换为字节流进行持久化存储或网络传输,非常方便。
不能实现 RandomAccess 接口?
RandomAccess
是一个标记接口,用来表明实现该接口的类支持随机访问(即可以通过索引快速访问元素)。由于 LinkedList
底层数据结构是链表,内存地址不连续,只能通过指针来定位,不支持随机快速访问,所以不能实现 RandomAccess
接口。
ArrayList vs LinkedList
- 是否保证线程安全:
ArrayList
和LinkedList
都是不同步的,也就是不保证线程安全; - 底层数据结构:
ArrayList
底层使用的是Object
数组;LinkedList
底层使用的是 双向链表 数据结构(JDK1.6 之前为循环链表,JDK1.7 取消了循环。注意双向链表和双向循环链表的区别,下面有介绍到!) - 插入和删除是否受元素位置的影响:
ArrayList
采用 数组存储,所以插入和删除元素的时间复杂度受元素位置的影响。 比如:执行add(E e)
方法的时候,ArrayList
会默认在将指定的元素 追加 到此列表的末尾,这种情况 时间复杂度就是 O(1)。但是如果要在指定位置 i 插入和删除元素 的话(add(int index, E element)
),时间复杂度就为 O(n)。因为在进行上述操作的时候集合中第 i 和第 i 个元素之后的(n-i)个元素都要执行向后位/向前移一位的操作。LinkedList
采用 链表存储,所以 在头尾插入或者删除元素 不受元素位置的影响(add(E e)
、addFirst(E e)
、addLast(E e)
、removeFirst()
、removeLast()
),时间复杂度为 O(1),如果是要在指定位置i
插入和删除元素 的话(add(int index, E element)
,remove(Object o)
,remove(int index)
),时间复杂度为 O(n) ,因为需要先移动到指定位置再插入和删除。
- 是否支持快速随机访问:
LinkedList
不支持高效的随机元素访问,而ArrayList
(实现了RandomAccess
接口) 支持。快速随机访问就是通过元素的序号快速获取元素对象(对应于get(int index)
方法)。 - 内存空间占用:
ArrayList
的空间浪费主要体现在在 list 列表的结尾会预留一定的容量空间,而 LinkedList 的空间花费则体现在它的每一个元素都需要消耗比 ArrayList 更多的空间(因为要存放直接后继和直接前驱以及数据)。
ArrayList vs Array(数组)
ArrayList
内部基于动态数组实现,比 Array
(静态数组) 使用起来更加灵活:
ArrayList
会根据实际存储的元素动态地扩容或缩容,而Array
被创建之后就不能改变它的长度了。ArrayList
允许你使用泛型来确保类型安全,Array
则不可以。ArrayList
中只能存储对象。对于基本类型数据,需要使用其对应的包装类(如 Integer、Double 等)。Array
可以直接存储基本类型数据,也可以存储对象。ArrayList
支持插入、删除、遍历等常见操作,并且提供了丰富的 API 操作方法,比如add()
、remove()
等。Array
只是一个固定长度的数组,只能按照下标访问其中的元素,不具备动态添加、删除元素的能力。ArrayList
创建时不需要指定大小,而Array
创建时必须指定大小。
Set
HashSet
(无序,唯一): 基于HashMap
实现的,底层采用HashMap
来保存元素。LinkedHashSet
:LinkedHashSet
是HashSet
的子类,并且其内部是通过LinkedHashMap
来实现的。TreeSet
(有序,唯一): 红黑树(自平衡的排序二叉树)。
Comparable vs Comparator
Comparable
接口和 Comparator
接口都是 Java 中用于排序的接口,它们在实现类对象之间比较大小、排序等方面发挥了重要作用:
Comparable
接口实际上是出自java.lang
包 它有一个compareTo(Object obj)
方法用来排序Comparator
接口实际上是出自java.util
包它有一个compare(Object obj1, Object obj2)
方法用来排序
一般我们需要对一个集合使用自定义排序时,我们就要重写 compareTo()
方法或 compare()
方法,当我们需要对某一个集合实现两种排序方式,比如一个 song
对象中的歌名和歌手名分别采用一种排序方法的话,我们可以重写 compareTo()
方法和使用自制的 Comparator
方法或者以两个 Comparator
来实现歌名排序和歌星名排序,第二种代表我们只能使用两个参数版的 Collections.sort()
Collections.sort(arrayList, new Comparator<Integer>() {
@Override
public int compare(Integer o1, Integer o2) {
return o2.compareTo(o1);
}
});
public class Person implements Comparable<Person> {
@Override
public int compareTo(Person o) {
if (this.age > o.getAge()) {
return 1;
}
if (this.age < o.getAge()) {
return -1;
}
return 0;
}
}
HashSet vs LinkedHashSet vs TreeSet
HashSet
、LinkedHashSet
和TreeSet
都是Set
接口的实现类,都能保证元素唯一,并且都不是线程安全的。HashSet
、LinkedHashSet
和TreeSet
的主要区别在于底层数据结构不同。HashSet
的底层数据结构是 哈希表(基于HashMap
实现)。LinkedHashSet
的底层数据结构是 链表和哈希表,元素的插入和取出顺序满足 FIFO。TreeSet
底层数据结构是 红黑树,元素是有序的,排序的方式有自然排序和定制排序。- 底层数据结构不同又导致这三者的应用场景不同。
HashSet
用于不需要保证元素插入和取出顺序的场景,LinkedHashSet
用于保证元素的插入和取出顺序满足 FIFO 的场景,TreeSet
用于支持对元素自定义排序规则的场景。
Queue
PriorityQueue
:Object[]
数组来实现小顶堆。DelayQueue
:PriorityQueue
。ArrayDeque
: 可扩容动态双向数组。
PriorityQueue
PriorityQueue
利用了二叉堆的数据结构来实现的,底层使用可变长的数组来存储数据PriorityQueue
通过堆元素的上浮和下沉,实现了在 O(logn) 的时间复杂度内插入元素和删除堆顶元素。PriorityQueue
是非线程安全的,且不支持存储NULL
和non-comparable
的对象。PriorityQueue
默认是小顶堆,但可以接收一个Comparator
作为构造参数,从而来自定义元素优先级的先后。
ArrayQueue vs LinkedList
ArrayDeque
和 LinkedList
都实现了 Deque
接口,两者都具有队列的功能,但两者有什么区别呢?
ArrayDeque
是基于可变长的数组和双指针来实现,而LinkedList
则通过链表来实现。ArrayDeque
不支持存储NULL
数据,但LinkedList
支持。ArrayDeque
是在 JDK1.6 才被引入的,而LinkedList
早在 JDK1.2 时就已经存在。ArrayDeque
插入时可能存在扩容过程, 不过均摊后的插入操作依然为 O(1)。虽然LinkedList
不需要扩容,但是每次插入数据时均需要申请新的堆空间,均摊性能相比更慢。
从性能的角度上,选用 ArrayDeque
来实现队列要比 LinkedList
更好。此外,ArrayDeque
也可以用于实现栈。
Map
HashMap
:JDK1.8 之前HashMap
由数组+链表组成的,数组是HashMap
的主体,链表则是主要为了解决哈希冲突而存在的(“拉链法”解决冲突)。JDK1.8 以后在解决哈希冲突时有了较大的变化,当链表长度大于阈值(默认为 8)(将链表转换成红黑树前会判断,如果当前数组的长度小于 64,那么会选择先进行数组扩容,而不是转换为红黑树)时,将链表转化为红黑树,以减少搜索时间。LinkedHashMap
:LinkedHashMap
继承自HashMap
,所以它的底层仍然是基于拉链式散列结构即由数组和链表或红黑树组成。另外,LinkedHashMap
在上面结构的基础上,增加了一条 双向链表,使得上面的结构可以保持键值对的插入顺序。同时通过对链表进行相应的操作,实现了访问顺序相关逻辑。Hashtable
:数组+链表组成的,数组是Hashtable
的主体,链表则是主要为了解决哈希冲突而存在的。TreeMap
:红黑树(自平衡的排序二叉树)。
HashMap
HashMap 主要用来存放键值对,它基于哈希表的 Map 接口实现,是常用的 Java 集合之一,是非线程安全的。
HashMap
可以存储 null 的 key 和 value,但 null 作为键只能有一个,null 作为值可以有多个
JDK1.8 之前 HashMap 由 数组+链表 组成的,数组是 HashMap 的主体,链表则是主要为了解决哈希冲突而存在的(“拉链法”解决冲突)。 JDK1.8 以后的 HashMap
在解决哈希冲突时有了较大的变化,当链表长度大于等于阈值(默认为 8)(将链表转换成红黑树前会判断,如果当前数组的长度小于 64,那么会选择先进行数组扩容,而不是转换为红黑树)时,将链表转化为红黑树,以减少搜索时间。
HashMap
默认的初始化大小为 16。之后每次扩充,容量变为原来的 2 倍。并且, HashMap
总是使用 2 的幂作为哈希表的大小。
ConcurrentHashMap
线程安全的 HashMap
Java 7 中 ConcurrentHashMap
的存储结构如上图,ConcurrnetHashMap
由很多个 Segment
组合,而每一个 Segment
是一个类似于 HashMap
的结构,所以每一个 HashMap
的内部可以进行扩容。但是 Segment
的个数一旦 初始化就不能改变,默认 Segment
的个数是 16 个,你也可以认为 ConcurrentHashMap
默认支持最多 16 个线程并发。
可以发现 Java8 的 ConcurrentHashMap 相对于 Java7 来说变化比较大,不再是之前的 Segment 数组 + HashEntry 数组 + 链表,而是 Node 数组 + 链表 / 红黑树。当冲突链表达到一定长度时,链表会转换成红黑树
ConcurrentHashMap
的初始化是通过 自旋和 CAS 操作完成的
Java7 中 ConcurrentHashMap
使用的分段锁,也就是每一个 Segment 上同时只有一个线程可以操作,每一个 Segment
都是一个类似 HashMap
数组的结构,它可以扩容,它的冲突会转化为链表。但是 Segment
的个数一但初始化就不能改变。
Java8 中的 ConcurrentHashMap
使用的 Synchronized
锁加 CAS 的机制。结构也由 Java7 中的 Segment
数组 + HashEntry
数组 + 链表 进化成了 Node 数组 + 链表 / 红黑树,Node 是类似于一个 HashEntry 的结构。它的冲突再达到一定大小时会转化成红黑树,在冲突小于一定数量时又退回链表。
有些同学可能对 Synchronized
的性能存在疑问,其实 Synchronized
锁自从引入锁升级策略后,性能不再是问题,有兴趣的同学可以自己了解下 Synchronized
的 锁升级。
LinkedHashMap
LinkedHashMap
是 Java 提供的一个集合类,它继承自 HashMap
,并在 HashMap
基础上维护一条双向链表,使得具备如下特性:
- 支持遍历时会按照插入顺序有序进行迭代。
- 支持按照元素访问顺序排序,适用于封装 LRU 缓存工具。
- 因为内部使用双向链表维护各个节点,所以遍历时的效率和元素个数成正比,相较于和容量成正比的 HashMap 来说,迭代效率会高很多。
如何选择集合
我们主要根据集合的特点来选择合适的集合。比如:
- 我们需要根据键值获取到元素值时就选用
Map
接口下的集合,需要排序时选择TreeMap
, 不需要排序时就选择HashMap
, 需要保证线程安全就选用ConcurrentHashMap
。 - 我们只需要存放元素值时,就选择实现
Collection
接口的集合,需要保证元素唯一时选择实现Set
接口的集合比如TreeSet
或HashSet
,不需要就选择实现List
接口的比如ArrayList
或LinkedList
,然后再根据实现这些接口的集合的特点来选用