Java集合(一) - Collection
Java集合系列文章包括:
本文主要介绍Java容器原理、特性及简单使用方式,主要包含Collection接口及其主要实现类。
1. 概览
Java 集合, 也叫作容器,主要是由两大接口派生而来:一个是 Collecton接口,主要用于存放单一元素;另一个是 Map 接口,主要用于存放键值对。对于Collection 接口,下面又有三个主要的子接口:List、Set 和 Queue。
2. Collection接口

3. 常用实现类
- ArrayList
- List接口的主要实现类,利用数组存储数据,元素是有序的,它能够添加null元素,使用数组下标查找数据O(1),适用于频繁查找的场景
- 插入元素时,当size == elementData.length的时候,就需要扩容。初始化的时候尽量指定容量,减少扩容次数
- 线程不安全
- LinkedList
- 使用双向链表存储数据,元素是有序的,并且元素可以为null
- 直接继承自
AbstractSequentialList,实现了Deque(双端队列)接口,因此可以将其作为双端队列的一种实现 - 查找耗时,插入删除比较快速,适合频繁增删的场景
- 遍历推荐使用
forEach或Iterator方式,for循环方式比较耗时(没有实现RandomAccess接口) - 线程不安全,如果想使LinkedList变成线程安全的,可以调用静态类
Collections类中的synchronizedList方法
- Vector
- List的古老实现类,与ArrayList很像,它利用数组存储数据,包含了需求不属于集合的传统方法
- 线程安全
- Stack
- 继承自
Vector,LIFO(last-in-first-out)先进后出的数据结构 - 推荐使用Deque的实现类(如ArrayDeque或LinkedList)而非继承Stack,Deque vs Stack
- 继承自
4. ArrayList
1. 数据结构
数组
/**
* 存储元素的数组,当size等于数组的长度,再次进行插入就需要扩容
*/
transient Object[] elementData;
/**
* 存储的元素的个数,一般情况下,size != elementData.length
*/
private int size;
2. 添加元素(add)
1. 扩容
插入元素时,当size == elementData.length的时候,就需要扩容。扩容实际上是通过System.arraycopy方法将旧数组内容复制到新数组,新数组长度的大小逻辑如下:
- 初始情况下,数组长度扩容为10(默认的)
- 其他情况下,新数组的长度是原数组的1.5倍(oldCapacity + (oldCapacity >> 1)),数组长度最大为Integer.MAX_VALUE
2. 两种插入方式
private void add(E e, Object[] elementData, int s) {
if (s == elementData.length) {
elementData = grow(); // 扩容
}
elementData[s] = e;
size = s + 1;
}
/**
* 元素插入到数组末尾
*/
public boolean add(E e) {
modCount++;
add(e, elementData, size);
return true;
}
/**
* 在指定位置插入元素
*/
public void add(int index, E element) {
rangeCheckForAdd(index);
modCount++;
final int s;
Object[] elementData;
if ((s = size) == (elementData = this.elementData).length) {
elementData = grow();
}
System.arraycopy(elementData, index, elementData, index + 1, s - index);
elementData[index] = element;
size = s + 1;
}
private void rangeCheckForAdd(int index) {
if (index > size || index < 0) {
throw new IndexOutOfBoundsException(outOfBoundsMsg(index));
}
}
- 元素添加到数组末尾。首先判断是否需要扩容,然后给elementData[size]赋值,时间复杂度为O(1)
- 元素添加到指定位置。首先判断指定位置是否越界了([0, size]),然后判断是否需要扩容,然后将[index, size - 1]的元素移动到[index + 1, size],这里是通过System.arraycopy直接复制指定区间的元素,速度比循环更快,最后给elementData[index]赋值
- ArrayList还提供了批量添加元素的方法(addAll)
因此,在中间位置插入元素效率比在末尾添加要低,所以我们在使用的时候,尽量不要在中间位置插入元素。
3. 访问元素(get)
public E get(int index) {
Objects.checkIndex(index, size);
return elementData(index);
}
E elementData(int index) {
return (E) elementData[index];
}
- 通过数组下标访问元素,时间复杂度为O(1)
- ArrayList还提供了判断集合是否包含元素(contains)和查询元素在集合中的下标(indexOf)的方法
4. 删除元素(remove)
/**
* 删除指定位置元素
*/
public E remove(int index) {
Objects.checkIndex(index, size);
final Object[] es = elementData;
@SuppressWarnings("unchecked") E oldValue = (E) es[index];
fastRemove(es, index);
return oldValue;
}
/**
* 删除指定元素
*/
public boolean remove(Object o) {
final Object[] es = elementData;
final int size = this.size;
int i = 0;
found: {
if (o == null) {
for (; i < size; i++) {
if (es[i] == null) {
break found;
}
}
} else {
for (; i < size; i++) {
if (o.equals(es[i])) {
break found;
}
}
}
return false;
}
fastRemove(es, i);
return true;
}
private void fastRemove(Object[] es, int i) {
modCount++;
final int newSize;
if ((newSize = size - 1) > i) {
System.arraycopy(es, i + 1, es, i, newSize - i);
}
es[size = newSize] = null;
}
- 删除指定位置元素:将指定元素后面的元素整个向前挪一个位置,再将最后一个元素置为null
- 删除指定元素:定位指定元素下标,之后的操作与删除指定位置元素一致
- ArrayList还提供了删除符合条件元素的方法(removeIf)
5. LinkedList
1. 数据结构
双向链表
//元素个数
transient int size = 0;
//双向链表的头指针
transient Node<E> first;
//双向链表的尾部指针
transient Node<E> last;
private static class Node<E> {
E item;
Node<E> next;
Node<E> prev;
Node(Node<E> prev, E element, Node<E> next) {
this.item = element;
this.next = next;
this.prev = prev;
}
}

2. 添加元素
// 添加元素
public boolean add(E e) {
linkLast(e);
return true;
}
// 在LinkedList末尾插入元素 并移动头尾指针
void linkLast(E e) {
final Node<E> l = last; // 尾指针赋值
final Node<E> newNode = new Node<>(l, e, null); // 生成新的节点
last = newNode; // 重新赋值尾节点
if (l == null) { // 尾节点为空
first = newNode; // 链表为空,这是插入的第一个元素
} else {
l.next = newNode; // 尾节点的后继为新生的节点
}
size++; // 元素数自增
modCount++; // 结构修改自增
}
3. 访问元素
//根据索引获取元素
public E get(int index) {
checkElementIndex(index);//检查索引是否越界
return node(index).item;//定位索引元素
}
//定位元素位置
Node<E> node(int index) {
// assert isElementIndex(index);
if (index < (size >> 1)) {
Node<E> x = first;
for (int i = 0; i < index; i++) {
x = x.next;
}
return x;
} else {
Node<E> x = last;
for (int i = size - 1; i > index; i--) {
x = x.prev;
}
return x;
}
}
- 使用二分查找的思想,从链表头部或尾部开始遍历查找
4. 删除元素
public E remove(int index) {
checkElementIndex(index);
return unlink(node(index));
}
public boolean remove(Object o) {
if (o == null) {
for (Node<E> x = first; x != null; x = x.next) {
if (x.item == null) {
unlink(x);
return true;
}
}
} else {
for (Node<E> x = first; x != null; x = x.next) {
if (o.equals(x.item)) {
unlink(x);
return true;
}
}
}
return false;
}
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 和第 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)) 时间复杂度近似为 O(n) ,因为需要先移动到指定位置再插入。 - 是否支持快速随机访问: LinkedList 不支持高效的随机元素访问,而 ArrayList 支持。快速随机访问就是通过元素的序号快速获取元素对象(对应于get(int index)方法)。
- 内存空间占用: ArrayList 的空间浪费主要体现在在 list 列表的结尾会预留一定的容量空间,而 LinkedList 的空间花费则体现在它的每一个元素都需要消耗比 ArrayList 更多的空间(因为要存放直接后继和直接前驱以及数据)。
6. Vector
Vector 类实现了一个动态数组。和ArrayList很相似,但是两者是不同的:
- 是否保证线程安全:Vector保证线程安全,ArrayList不保证线程安全
- Vector 包含了许多传统的方法,这些方法不属于集合框架
7. Queue
LinkedList是Queue的一个常用实现类,也是Deque(双端队列)的一个常用实现类。因此,可以将LinkedList作为栈来使用。
Queue和Deque的区别
Queue 是单端队列,只能从一端插入元素,另一端删除元素,实现上一般遵循先进先出(FIFO)规则。
Queue 扩展了Collection的接口,根据 因为容量问题而导致操作失败后处理方式的不同 可以分为两类方法: 一种在操作失败后会抛出异常,另一种则会返回特殊值。
| Queue 接口 | 抛出异常 | 返回特殊值 |
|---|---|---|
| 插入队尾 | add(E e) -> 容量限制时抛出IllegalStateException |
offer(E e) |
| 删除队首 | remove() -> 队列为空时抛出NoSuchElementException |
poll() |
| 查询队首元素 | element() -> 队列为空时抛出NoSuchElementException |
peek() |
Deque 是双端队列,在队列的两端均可以插入或删除元素。
Deque 扩展了 Queue 的接口, 增加了在队首和队尾进行插入和删除的方法,同样根据失败后处理方式的不同分为两类:
| Deque 接口 | 抛出异常 | 返回特殊值 |
|---|---|---|
| 插入队首 | addFirst(E e) | offerFirst(E e) |
| 插入队尾 | addLast(E e) | offerLast(E e) |
| 删除队首 | removeFirst() | pollFirst() |
| 删除队尾 | removeLast() | pollLast() |
| 查询队首元素 | getFirst() | peekFirst() |
| 查询队尾元素 | getLast() | peekLast() |
事实上,Deque 还提供有 push()和 pop()等其他方法,可用于模拟栈。 |
8. Stack
由于LinkedList底层由双端链表实现,因此,也可以将其作为栈来使用。
9. 常见问题
1. 有序性和不可重复性的含义是什么?
1、什么是有序性?说的是元素的插入先后,与元素在集合内存储的位置是否有前后对应关系。即有序、无序是指插入时,插入位置是否遵循先入在前后入在后的规则,若先插的位置在前,后插的位置在后,则可说此集合类是有序的,反之则无序。
实现了List接口的集合类全部有序,如ArrayList、LinkedList
实现了Map接口的集合类中,HashMap无序
实现了Set接口的集合类中,HashSet(基于HashMap实现)无序
2、什么是不可重复性?不可重复性是指添加的元素按照 equals()判断时,返回 false。需要同时重写 equals()方法和 hashCode()方法。

浙公网安备 33010602011771号