一、前言
Java 集合框架(Java Collections Framework)是每个Java开发者必须熟知的基础部分,而List
接口是其中最常见的数据结构之一。在实际开发中,List
接口的不同实现类被广泛应用于各种场景。本篇文章通过对JDK 1.8版本的List
接口源码分析,结合其子类如ArrayList
、LinkedList
等的实现,探讨其内部工作原理、自动扩容机制以及线程安全问题,帮助开发者更好地理解和优化程序。
二、List
接口简介
2.1 List
接口的定义
List
是Java集合框架中一个重要的接口,继承自Collection
接口,用于存储有序且可重复的元素。List
允许我们通过索引访问元素,并定义了常见的集合操作,如增加、删除、修改和遍历等。
List
的部分定义如下:
public interface List<E> extends Collection<E> {int size();boolean isEmpty();boolean contains(Object o);boolean add(E e);boolean remove(Object o);E get(int index);E set(int index, E element);void add(int index, E element);E remove(int index);int indexOf(Object o);int lastIndexOf(Object o);ListIterator<E> listIterator();List<E> subList(int fromIndex, int toIndex);
}
- 索引访问:
List
允许通过索引快速访问元素,如get(int index)
。 - 重复元素:
List
可以存储重复的元素,这与Set
接口形成了对比。 - 有序性:
List
中的元素按照插入顺序进行存储和访问。
三、ArrayList
源码解析及自动扩容机制
3.1 ArrayList
的基本概述
ArrayList
是List
接口的常用实现类之一,底层使用数组来存储元素。它的特点是通过动态调整数组的大小来应对不断变化的数据量,因此它的存储效率较高,适合频繁读取的场景。
3.2 ArrayList
的源码分析
在ArrayList
的源码中,最重要的成员变量是:
transient Object[] elementData; // 存储元素的数组
private int size; // 当前数组中元素的数量
elementData
是一个动态数组,用来存储ArrayList
中的元素。size
则表示当前数组中实际存储的元素个数。
3.3 自动扩容机制
ArrayList
的自动扩容机制是其最重要的特性之一。当我们往ArrayList
中添加新元素时,如果当前数组容量不足以容纳新增元素,它会自动扩展数组的容量。这个过程由ensureCapacity()
和grow()
方法负责。
private void ensureCapacityInternal(int minCapacity) {// 确保容量足够,调用 ensureExplicitCapacity 方法ensureExplicitCapacity(calculateCapacity(elementData, minCapacity));
}private void ensureExplicitCapacity(int minCapacity) {// 增加修改计数modCount++;// 溢出意识代码// 如果最小容量大于当前数组长度,则扩容if (minCapacity - elementData.length > 0) grow(minCapacity);
}private void grow(int minCapacity) {// 溢出意识代码int oldCapacity = elementData.length; // 记录旧容量int newCapacity = oldCapacity + (oldCapacity >> 1); // 新容量为旧容量的 1.5 倍// 如果新容量仍小于最小容量,则设置为最小容量if (newCapacity - minCapacity < 0)newCapacity = minCapacity;// 如果新容量超过最大数组大小,则调用 hugeCapacity 方法处理if (newCapacity - MAX_ARRAY_SIZE > 0)newCapacity = hugeCapacity(minCapacity);// 最小容量通常接近当前大小,因此这是一个优化:elementData = Arrays.copyOf(elementData, newCapacity); // 扩容
}
grow()
方法通过当前容量的1.5倍来扩展数组容量,这个机制有效地避免了每次添加元素时都要扩容的性能开销。- 如果需要的最小容量
minCapacity
比扩展后的容量大,则直接将容量设置为minCapacity
,以确保足够的空间。
3.4 自动扩容的优缺点
- 优点:在不确定数据量时,
ArrayList
提供了动态调整数组大小的能力,开发者无需手动管理内存分配。 - 缺点:每次扩容都需要进行数组复制,可能造成性能开销,尤其是在处理大量数据时。因此,在大规模数据操作中,最好预估所需容量,使用
ArrayList
的构造函数设置初始容量。
四、LinkedList
源码解析
4.1 LinkedList
的基本概述
LinkedList
是List
接口的另一常见实现类。不同于ArrayList
,LinkedList
底层采用的是双向链表结构。这使得LinkedList
在频繁的插入和删除操作中表现优异,尤其是当操作不涉及随机访问时。
4.2 LinkedList
的源码结构
LinkedList
通过内部类Node
来维护链表的节点。每个节点包含三个元素:
private static class Node<E> {E item; // 节点存储的元素Node<E> next; // 指向下一个节点Node<E> prev; // 指向上一个节点
}
LinkedList
的元素是通过链表的形式串联起来的,通过next
和prev
指针可以方便地在链表中进行元素的增删改操作。
4.3 LinkedList
的操作效率
- 插入和删除操作:在
LinkedList
中,插入和删除元素的效率较高,特别是在首尾插入和删除时。因为不需要像ArrayList
那样移动其他元素,只需要调整节点指针即可。 - 随机访问效率低:由于
LinkedList
的底层是链表结构,随机访问某个元素时需要遍历链表,时间复杂度为O(n),这使得LinkedList
不适合频繁的随机访问。
五、线程安全的考虑
5.1 ArrayList
与LinkedList
的线程安全问题
ArrayList
和LinkedList
都是非线程安全的。这意味着当多个线程同时操作它们时,可能会导致数据不一致或抛出异常。解决线程安全问题的常见方法包括:
- Collections.synchronizedList():Java提供了
Collections.synchronizedList()
方法,将List
包装成线程安全的版本。
List<String> syncList = Collections.synchronizedList(new ArrayList<>());
- CopyOnWriteArrayList:
CopyOnWriteArrayList
是线程安全的List
实现类,适合读多写少的场景。在写操作时,它会将整个数组复制一份,进行修改后再替换旧数组,因此适合并发环境下的读操作。
5.2 CopyOnWriteArrayList
源码解析
CopyOnWriteArrayList
是 List
接口的一个线程安全的实现类。它的特点是 “写时复制” 的机制,即每次修改操作(如 add()
、set()
、remove()
)时,不是直接在原有数据结构上进行修改,而是先复制一份原有的数据,然后对新复制的数据进行修改。读操作则不受影响,仍然可以安全地访问原数据。
这种实现机制的优势在于,在并发环境下,多个线程可以安全地读操作,而不需要同步锁来协调。同时,由于写操作是在副本上进行,写操作的线程之间也不会发生竞争。
5.3 CopyOnWriteArrayList
源码分析
首先,CopyOnWriteArrayList
的底层依然是基于数组的结构,其重要的成员变量如下:
public boolean add(E e) {final ReentrantLock lock = this.lock;lock.lock();try {Object[] elements = getArray();int len = elements.length;Object[] newElements = Arrays.copyOf(elements, len + 1);newElements[len] = e;setArray(newElements);return true;} finally {lock.unlock();}
}
- 在添加元素时,
CopyOnWriteArrayList
会先对现有数组进行复制,确保在写操作过程中,其他线程仍然可以安全地读取旧数组。 - 虽然这种机制确保了线程安全,但在写操作频繁的场景中,频繁的数组复制操作可能导致较大的性能开销,因此不适用于写操作较多的场景。
六. ArrayList
、LinkedList
和 CopyOnWriteArrayList
的对比
特性 | ArrayList | LinkedList | CopyOnWriteArrayList |
---|---|---|---|
数据结构 | 动态数组 | 双向链表 | 动态数组,写时复制 |
访问速度 | O(1) 随机访问 | O(n) 随机访问,O(1) 插入和删除 | O(1) 读取,O(n) 写操作 |
线程安全 | 非线程安全 | 非线程安全 | 线程安全,读写分离 |
适用场景 | 适合频繁读取、偶尔修改的场景 | 适合频繁插入、删除操作 | 适合读多写少的场景 |
性能开销 | 写操作时可能频繁扩容 | 插入和删除效率高,访问效率低 | 写操作内存开销大,读操作性能高 |
扩容机制 | 自动扩容,1.5倍 | 不需要扩容 | 写时复制,写入时创建新数组 |
七、List
接口下的常用子类及应用场景
7.1 ArrayList
- 应用场景:适合频繁读取和少量插入、删除操作的场景,尤其是需要随机访问的情况下。
- 优点:读取操作快,适合做缓存或者需要频繁遍历数据的场景。
- 缺点:插入、删除元素较慢,尤其是在中间位置操作时。
7.2 LinkedList
- 应用场景:适合频繁插入、删除操作而随机访问较少的场景,比如队列和双端队列的实现。
- 优点:插入、删除操作快,尤其是头部和尾
7.3 CopyOnWriteArrayList
CopyOnWriteArrayList
是 List
接口的一个特殊实现,它通过写时复制的机制实现了线程安全性,特别适合在多线程环境下,读操作远多于写操作的场景。相比之下,ArrayList
和 LinkedList
各有优势:ArrayList
适合频繁的读取操作,而 LinkedList
更适合频繁插入和删除元素。
在实际项目中,选择哪种 List
实现类取决于具体的应用场景和性能需求。如果你需要线程安全的 List
且写操作较少,CopyOnWriteArrayList
是一个理想的选择。