ArrayList类
ArrayList
是 Java 集合框架中的一个类,属于 java.util
包,是一种 基于动态数组实现的可变长度集合。它实现了 List
接口,提供了一个可调整大小的数组,能够存储任意类型的对象(包括自定义类和基本类型的包装类)。
特点
- 动态数组:
ArrayList
的大小是可变的,默认容量为 10。当元素数量超过当前容量时,ArrayList
会自动扩容,通常以 1.5 倍的速度增长。
- 有序:
- 元素按照插入的顺序存储,可以通过索引访问,顺序不会改变(除非显式调整)。
- 允许重复元素:
ArrayList
允许存储重复的元素。
- 支持随机访问:
- 因为底层是数组,
ArrayList
支持快速随机访问,时间复杂度为 $ O(1)$ 。
- 因为底层是数组,
- 线程不安全:
ArrayList
是非同步的,因此在多线程环境下需要手动同步(可以使用Collections.synchronizedList()
或CopyOnWriteArrayList
替代)。
常见方法
构造方法
public ArrayList()
:创建一个默认初始容量为 10 的空ArrayList
public ArrayList(int initialCapacity)
:创建一个具有指定初始容量的空ArrayList
public ArrayList(Collection<? extends E> c)
:创建一个包含指定集合中所有元素的ArrayList
,按照集合的迭代器顺序。
添加元素
boolean add(E e)
:将指定的元素添加到列表的末尾void add(int index, E element)
:将指定的元素插入到列表的指定位置boolean addAll(Collection<? extends E> c)
:将指定集合中的所有元素添加到当前列表的末尾boolean addAll(int index, Collection<? extends E> c)
:将指定集合中的所有元素插入到当前列表的指定位置。
删除元素
void clear()
:删除全部元素E remove(int index)
:删除列表中指定位置的元素boolean remove(Object o)
:删除列表中第一次出现的指定元素。如果列表中没有该元素,则不做任何操作。boolean removeAll(Collection<?> c)
:删除当前列表中所有与指定集合中的元素相同的元素void removeRange(int from,int to)
:删除索引[from,to)之间的元素
判断元素
boolean contains(Object o)
:判断列表中是否包含指定的元素boolean containsAll(Collection<?> c)
:判断列表是否包含指定集合中的所有元素
修改元素
E set(int index, E element)
:用指定的元素替换列表中指定位置的元素
访问元素
E get(int index)
:返回指定位置的元素int indexOf(Object o)
:返回指定元素在列表中的第一次出现位置的索引。如果列表中没有该元素,则返回-1
int lastIndexOf(Object o)
:返回指定元素在列表中的最后一次出现位置的索引。如果列表中没有该元素,则返回-1
获取大小
int size()
:返回列表中元素的数量boolean isEmpty()
:判断列表是否为空void ensureCapacity(int minCapacity)
:确保列表能够容纳至少指定数量的元素,不会导致扩容。
扩容机制
源码分析
-
往
ArrayList
中添加元素时会有ensureCapacityInternal
的判断1 2 3 4 5
public boolean add(E e) { ensureCapacityInternal(size + 1); // Increments modCount!! elementData[size++] = e; return true; }
-
ensureCapacityInternal
内部会调用ensureExplicitCapacity
方法,若minCapacity - elementData.length > 0
即容量不够了,则会调用grow
方法:1 2 3 4 5 6 7 8 9 10 11
/** * 检查并确保集合容量足够,如果需要则增加集合容量。 * * @param minCapacity 所需最小容量 */ private void ensureExplicitCapacity(int minCapacity) { // 检查是否超出了数组范围,确保不会溢出 if (minCapacity - elementData.length > 0) // 如果需要增加容量,则调用 grow 方法 grow(minCapacity); }
-
grow
扩容逻辑1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
/** * 扩容 ArrayList 的方法,确保能够容纳指定容量的元素 * @param minCapacity 指定容量的最小值 */ private void grow(int minCapacity) { // 检查是否会导致溢出,oldCapacity 为当前数组长度 int oldCapacity = elementData.length; int newCapacity = oldCapacity + (oldCapacity >> 1); // 扩容至原来的1.5倍 if (newCapacity - minCapacity < 0) // 如果还是小于指定容量的最小值 newCapacity = minCapacity; // 直接扩容至指定容量的最小值 if (newCapacity - MAX_ARRAY_SIZE > 0) // 如果超出了数组的最大长度 newCapacity = hugeCapacity(minCapacity); // 扩容至数组的最大长度 // 将当前数组复制到一个新数组中,长度为 newCapacity elementData = Arrays.copyOf(elementData, newCapacity); }
总结
当 ArrayList
中的元素数量超过其当前容量时,会触发扩容机制。
-
默认情况下,
ArrayList
的初始容量为 10。 -
当发生扩容时,
ArrayList
会创建一个新的数组,其容量为原数组的 1.5 倍(即oldCapacity + (oldCapacity >> 1)
),若还是小于指定容量的最小值会直接扩容到指定容量的最小值 -
然后将原数组中的元素复制到新数组中。复制过程是通过
Arrays.copyOf()
方法实现的。 -
更新引用:将
ArrayList
内部指向原数组的引用指向新数组。 -
完成扩容:扩容完成后,可以继续添加新元素。
线程安全问题
为什么是线程不安全的
ArrayList
不是线程安全的。ArrayList
会暴露三个问题;
- 部分值为
null
(我们并没有add null进去) - 索引越界异常
- 线程1走到扩容那里发现当前
size
是 n,数组容量是n+1不用扩容,cpu让出执行权 - 线程2也发现不用扩容,这时候数组的容量就是n+1
- 而线程1 set完之后
size++
,这时候线程2再进来size
就是 n+1,数组的大小只有n+1,而你要设置下标索引为n+1的就会越界(数组的下标索引size
从0开始);
- 线程1走到扩容那里发现当前
size
与add
的数量不符- 因为
size++
本身不是原子操作,可以分为三步:- 获取 size 的值
- 将 size 的值加1
- 将新的 size 值覆盖掉原来的
- 线程1和线程2拿到一样的 size 值加完了同时覆盖,就会导致一次没有加上,所以肯定不会与
add
的数量保持一致的
- 因为
解决方法
-
使用Collections类的
synchronizedList
方法将ArrayList
包装成线程安全的List
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
import java.util.ArrayList; import java.util.Collections; import java.util.List; public class SynchronizedListExample { public static void main(String[] args) { List<Integer> list = Collections.synchronizedList(new ArrayList<>()); // 同步块中操作列表,保证线程安全 synchronized (list) { list.add(1); list.add(2); System.out.println(list); } } }
-
使用线程安全的替代类
CopyOnWriteArrayList
增删查改的机制
查询
|
|
时间复杂度为$ O(1)$,因为 ArrayList
内部使用数组来存储元素,所以可以直接根据索引来访问元素。
插入
添加一个元素(调用 add()
方法时)的时间复杂度最好情况为 $O(1)$,最坏情况为 $O(n)$。
- 如果在列表末尾添加元素,时间复杂度为 $O(1)$。
- 如果要在列表的中间或开头插入元素,则需要将插入位置之后的元素全部向后移动一位,时间复杂度为 $O(n)$。
|
|
修改
修改一个元素(调用 set()
方法时)可以直接根据索引来访问元素,时间复杂度为 $O(1)$。
|
|
删除
删除一个元素(调用 remove(Object)
方法时)的时间复杂度最好情况 $O(1)$,最坏情况 $O(n)$。
- 如果要删除列表末尾的元素,时间复杂度为 $O(1)$。
- 如果要删除列表中间或开头的元素,则需要将删除位置之后的元素全部向前移动一位,时间复杂度为 $O(n)$。
|
|
LinkedList类
LinkedList
是 Java 集合框架中的一个类,位于 java.util
包下,它同时实现了 List
和 Deque
接口,是一种基于双向链表的数据结构。
特点
- 双向链表:
- 每个节点包含:
- 一个存储数据的字段。
- 两个指针,分别指向前一个节点和后一个节点。
- 每个节点包含:
- 动态容量:
LinkedList
的大小可以动态变化,无需像数组那样预定义容量。
- 线程不安全:
LinkedList
是非同步的,因此在多线程环境下需要手动同步
- 插入和删除效率高:
- 插入操作:在链表任意位置插入元素的时间复杂度为 $O(1)$,只需调整指针即可(如果已定位到插入点)。
- 删除操作:删除元素也只需调整相关指针,时间复杂度为 $O(1)$。
- 有序:
- 元素按照插入的顺序存储,可以通过索引访问,顺序不会改变(除非显式调整)。
- 允许重复元素:
LinkedList
允许存储重复的元素。
常见方法
构造方法
public LinkedList()
:创建一个空的LinkedList
实例public LinkedList(Collection<? extends E> c)
:使用指定集合中的所有元素初始化LinkedList
添加元素
boolean add(E e)
:将指定的元素添加到链表的末尾void add(int index, E element)
:将指定的元素插入到链表的指定位置void addFirst(E e)
:在链表头部添加元素void addLast(E e)
:在链表尾部添加元素
访问元素
E get(int index)
:返回指定位置的元素E getFirst()
:返回链表头部的第一个元素E getLast()
:返回链表尾部的最后一个元素int indexOf(Object o)
:返回指定元素在列表中的首次出现位置,如果列表中不包含该元素,返回 -1 。int lastIndexOf(Object o)
:返回指定元素在列表中最后一次出现的位置,如果列表中不包含该元素,返回 -1 。
删除元素
E remove(int index)
:删除指定位置的元素,并返回被删除的元素boolean remove(Object o)
:删除链表中第一个匹配的元素E removeFirst()
:删除并返回链表头部的第一个元素E removeLast()
:删除并返回链表尾部的最后一个元素void clear()
:删除全部元素
判断元素
boolean contains(Object o)
:判断列表中是否包含指定的元素boolean containsAll(Collection<?> c)
:判断列表是否包含指定集合中的所有元素
获取大小
int size()
:返回列表中元素的数量boolean isEmpty()
:判断列表是否为空
修改元素
E set(int index, E element)
:用指定的元素替换列表中指定位置的元素
源码解析
链表节点类
|
|
组成结构
- 节点上的元素
- 下一个节点
- 上一个节点
插入节点
add
方法内部其实调用的是 linkLast
方法
|
|
linkLast
方法就是在链表的尾部添加元素(尾插法)
|
|
linkFirst
方法就是在链表的头部添加元素,把新的节点设为 first,并将新的 first 的 next 更新为之前的 first。(头插法)
|
|
删除节点
remove(int)
内部调用的是 unlink
方法
|
|
unlink
方法就是更新当前节点的 next
和 prev
,然后把当前节点上的元素设为 null
。
|
|
修改节点
|
|
node()
方法用于定位要替换的节点
|
|
node
方法会对下标进行一个初步判断,如果靠近前半截,就从下标 0 开始遍历;如果靠近后半截,就从末尾开始遍历。
Stack类
Stack
是 Java 集合框架中的一个类,位于 java.util
包中,作为 Vector
的子类间接实现了List接口,用于实现 栈(Stack) 数据结构
特点
Stack
类继承自Vector
类,因此它是一种 同步的(线程安全的) 集合。
常用方法
入栈操作
public E push(E item)
:将元素压入栈顶
出栈操作
public synchronized E pop()
- 移除并返回栈顶的元素。
- 如果栈为空,则抛出
EmptyStackException
。
查看栈顶元素
public synchronized E peek()
- 返回栈顶的元素,但不移除。
- 如果栈为空,则抛出
EmptyStackException
。
检查栈空
public boolean empty()
:判断栈是否为空
搜索元素
public synchronized int search(Object o)
- 返回元素在栈中的位置(以 1 为基准)。
- 如果元素不存在,则返回
-1
。
CopyOnWriteArrayList类
CopyOnWriteArrayList
是 Java 的一个线程安全的动态数组实现,属于 java.util.concurrent
包。
它通过写时复制机制,即在每次修改(写入)操作时,复制原始数组的内容来保证线程安全。
由于写操作涉及复制整个数组,所以它的写操作开销较大,但读取操作则完全无锁。这使得 CopyOnWriteArrayList
适合于读多写少的场景。
特点
- 写时复制是一种保证数据一致性和线程安全的技术。核心思想是在进行写操作时,不直接修改原来的数据结构,而是先复制一份副本,在副本上进行修改,然后将修改后的副本替换原来的数据结构。
- 保证数据一致性和线程安全
常见方法
构造方法
public CopyOnWriteArrayList()
:创建一个空的CopyOnWriteArrayList
public CopyOnWriteArrayList(Collection<? extends E> c)
:使用指定的集合初始化CopyOnWriteArrayList
public CopyOnWriteArrayList(E[] toCopyIn)
:使用指定的数组初始化CopyOnWriteArrayList
。
添加元素
boolean add(E e)
:将指定的元素添加到链表的末尾void add(int index, E element)
:将指定的元素插入到链表的指定位置void addFirst(E e)
:在链表头部添加元素void addLast(E e)
:在链表尾部添加元素
访问元素
E get(int index)
:返回指定位置的元素E getFirst()
:返回链表头部的第一个元素E getLast()
:返回链表尾部的最后一个元素int indexOf(Object o)
:返回指定元素在列表中的首次出现位置,如果列表中不包含该元素,返回 -1。int lastIndexOf(Object o)
:返回指定元素在列表中最后一次出现的位置,如果列表中不包含该元素,返回 -1。
删除元素
E remove(int index)
:删除指定位置的元素,并返回被删除的元素boolean remove(Object o)
:删除链表中第一个匹配的元素E removeFirst()
:删除并返回链表头部的第一个元素E removeLast()
:删除并返回链表尾部的最后一个元素void clear()
:删除全部元素
判断元素
boolean contains(Object o)
:判断列表中是否包含指定的元素boolean containsAll(Collection<?> c)
:判断列表是否包含指定集合中的所有元素
获取大小
int size()
:返回列表中元素的数量boolean isEmpty()
:判断列表是否为空
修改元素
E set(int index, E element)
:用指定的元素替换列表中指定位置的元素
源码分析
CopyOnWriteArrayList
底层也是通过一个数组保存数据,使用 volatile
关键字修饰数组,保证当前线程对数组对象重新赋值后,其他线程可以及时感知到。
|
|
写操作
|
|
- 读取当前数组:首先读取当前的数组,这个数组是
CopyOnWriteArrayList
当前持有的数组。 - 复制数组:创建一个当前数组的副本(新的数组),这个副本会拷贝当前数组中的所有元素。
- 在副本上进行修改:在副本数组上进行写操作(如添加、删除元素)。
- 用新数组替换旧数组:将修改后的副本数组设置为
CopyOnWriteArrayList
持有的数组,旧数组将不再使用。
读操作
|
|
所有读操作都可以无锁地直接读取 CopyOnWriteArrayList
当前持有的数组,因为这个数组在读操作期间不会被修改。
CopyOnWriteArrayList 和 Collections.synchronizedList 的区别(面试题)
回答要点
CopyOnWriteArrayList
是一个线程安全的 List 实现,特性就是写时复制。
每次对 List
的修改操作(如 add, set, remove)都会复制创建一个新的底层数组。读操作不需要加锁,写操作需要加锁。
优点:
- 读操作无锁:每次写操作都会创建并复制新数组,所以读写之间不冲突,因此读操作不需要加锁,能够提供非常高效的并发读性能。
缺点:
- 写操作开销大:每次写操作都会创建并复制新数组,且要将数据复制到新的数组中,在写操作频繁的场景下性能会较低。
- 内存消耗大:每次写操作都会创建并复制新数组,在数据量大的情况下,同一时刻会存在两倍 List 大小的内存占用,开销较大。
Collections.synchronizedList:
是一个包装方法,可以将任何 List
转换为线程安全的版本,它会对每个访问方法(如 get, set, add, remove)进行同步(加 synchronized
锁),从而保证线程安全。
优点:
- 方便:简单一个方法就可以将
List
变为线程安全版本,非常方便。
缺点:
- 并发低:读写操作都需要加锁,高并发场景下性能不高。
Collections.synchronizedList
适用于简单将 List 转为线程安全版本临时使用的场景。特定的场景还需使用并发度高的 JUC 类。