Java List
Java List
讲一下 Java 里面 List 的几种实现?
Java 中的 List 接口定义了有序、可重复的线性集合。
最常用的实现类是 ArrayList。它基于动态数组实现,支持快速随机访问,插入删除效率一般,但由于不是线程安全的,在单线程场景下性能更优。
LinkedList 则采用双向链表结构,适合频繁插入和删除的操作,尤其在头尾节点操作中优势明显,同样不是线程安全的。
Vector 是一种早期的线程安全 List 实现,它的方法大多被 synchronized 修饰,适合并发访问,但由于加锁机制较重,性能不及 ArrayList。
Stack 则继承自 Vector,实现了后进先出(LIFO)的堆栈结构,用于实现调用栈、撤销操作等场景。
在现代开发中,推荐使用 ArrayList 作为通用选择,特殊需求场景再根据特性选用其他实现,尤其在线程安全方面可考虑使用并发集合替代 Vector 和 Stack。
ArrayList 和 Array(数组)的区别?
Array 是 Java 提供的最基本的数据结构,一旦创建长度固定,不能自动扩容,适用于数据规模确定且性能要求较高的场景。
而 ArrayList 属于集合框架中的实现类,底层基于动态数组,可自动扩容、支持插入、删除等操作,使用上更为灵活。ArrayList 只能存储对象类型,基本类型需要通过包装类(如 int -> Integer)来使用,而数组可直接存储基本类型。
在类型安全方面,ArrayList 支持泛型,能在编译期检查类型,而数组不支持泛型。此外,ArrayList 提供了如 add()
、remove()
、contains()
等 API,便于集合操作;而数组只支持基于下标的访问。虽然 ArrayList 更易用,但在极端性能敏感的场景下,仍可优先考虑数组结构。
ArrayList、Vector、LinkedList有什么区别 ?
ArrayList、Vector、LinkedList 作为 Java 中 List 接口的核心实现类,虽然都能存储有序可重复的元素,但底层结构差异导致它们在性能、安全性和适用场景上有显著区别。以下从结构、性能、代码示例等方面详细分析:
底层结构差异
- ArrayList:基于动态数组实现,元素存储在连续内存空间,通过索引快速定位(类似数组)。
- Vector:同样基于动态数组,结构与 ArrayList 一致,但自带线程同步机制。
- LinkedList:基于双向链表实现,元素通过前后指针连接,内存空间不连续,无真实索引。
性能对比表格
操作类型 | ArrayList | Vector | LinkedList |
---|---|---|---|
随机访问(get(index)) | 高效(O(1),直接索引定位) | 高效(O(1),同数组结构) | 低效(O(n),需遍历链表) |
新增/删除(中间位置) | 低效(O(n),需移动元素) | 低效(O(n),同 ArrayList) | 高效(O(1),仅改指针) |
新增/删除(首尾) | 尾部高效(O(1))、头部低效 | 同 ArrayList | 首尾均高效(O(1)) |
扩容机制 | 扩容为原容量的1.5倍 | 默认扩容为原容量的2倍 | 无需扩容(动态增长) |
线程安全 | 不安全 | 安全(synchronized 修饰) | 不安全 |
内存占用 | 连续空间,少量冗余 | 连续空间,冗余较大 | 非连续,额外指针开销 |
说一说 ArrayList 的扩容机制?
ArrayList 是基于动态数组实现的集合,它的底层是一个 Object 数组。初始容量默认是 10(或根据构造器设定),当我们向其中添加元素时,如果超出容量限制,就会触发扩容机制。
为了更好理解,我们可以看下如下代码:
// 底层存储元素的Object数组
transient Object[] elementData;
// 默认初始容量
private static final int DEFAULT_CAPACITY = 10;
// 添加元素时触发容量检查
public boolean add(E e) {
ensureCapacityInternal(size + 1); // 确保容量足够,size为当前元素数
elementData[size++] = e;
return true;
}
// 计算所需最小容量
private void ensureCapacityInternal(int minCapacity) {
if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA) {
minCapacity = Math.max(DEFAULT_CAPACITY, minCapacity);
}
ensureExplicitCapacity(minCapacity);
}
// 检查是否需要扩容
private void ensureExplicitCapacity(int minCapacity) {
modCount++;
if (minCapacity - elementData.length > 0)
grow(minCapacity); // 容量不足时调用grow扩容
}
// 核心扩容方法
private void grow(int minCapacity) {
int oldCapacity = elementData.length;
// 新容量为旧容量的1.5倍(位运算高效实现)
int newCapacity = oldCapacity + (oldCapacity >> 1);
// 若1.5倍仍不足,直接使用所需最小容量
if (newCapacity - minCapacity < 0)
newCapacity = minCapacity;
// 复制旧数组数据到新数组(耗时操作)
elementData = Arrays.copyOf(elementData, newCapacity);
}
可以看到,扩容由 ensureCapacityInternal()
方法触发,它会调用 grow()
方法,计算新容量为原容量的 1.5 倍(即 oldCapacity + (oldCapacity >> 1))。随后通过 Arrays.copyOf()
方法将旧数组的内容复制到新分配的更大数组中。由于每次扩容都涉及内存重新分配与数据复制,因此频繁扩容会带来性能开销。为提高效率,建议在明确元素数量时通过构造函数设置初始容量,避免不必要的扩容过程,提升性能表现。
17. 如何实现数组和 List 之间的转换?
Java 提供了便捷的工具类来实现数组与集合之间的互转。当我们想把数组转换成 List 时,可以使用 Arrays.asList(array)
,它会返回一个固定大小的 List,其背后仍然引用原始数组。
需要注意,该 List 不支持添加或删除元素,只能修改已有元素,否则会抛出 UnsupportedOperationException
。而当需要将 List 转换成数组时,可以使用 toArray()
方法。常见方式包括 list.toArray(new String[0])
,这能确保返回的数组类型安全、长度合适。例如:
String[] arr = { "Java", "Python" };
List<String> list = Arrays.asList(arr);
String[] newArr = list.toArray(new String[0]);
了解这两个转换方法的底层行为,有助于避免因误用导致的隐患,如数组同步修改问题、集合操作异常等。
String[] arr = { "Java", "Python" };
List<String> list = Arrays.asList(arr);
String[] newArr = list.toArray(new String[0]);
了解这两个转换方法的底层行为,有助于避免因误用导致的隐患,如数组同步修改问题、集合操作异常等。
ArrayList 是线程安全的吗?如何变成线程安全?
ArrayList 是非同步实现,在多线程环境下同时对其进行修改操作可能导致竞态条件、数据丢失或结构性破坏。要想让 ArrayList 在并发场景中安全使用,可以有几种常见方式。
第一,通过 Collections.synchronizedList(arrayList)
方法获得一个同步包装后的 List,在外部加锁确保每次操作只有一个线程能访问。此方法简单但并发性能较低;
第二,直接使用 CopyOnWriteArrayList
,它是 JDK 提供的线程安全替代实现,适合读多写少的场景,其内部通过写时复制机制(Copy-On-Write)来避免并发冲突,这种方式最为常用;
第三,也可以使用早期的 Vector 类,但它整体加锁,性能和扩展性较差,不推荐。在高并发系统中,更推荐使用并发集合类以获得更好的吞吐能力和线程可见性控制。
为什么 ArrayList 的 elementData 被 transient 修饰?
elementData 的定义明确使用了 transient:
// 存储ArrayList元素的数组缓冲区
transient Object[] elementData;
这是因为 ArrayList 重写了序列化和反序列化方法,在 writeObject() 中手动控制了序列化过程:
private void writeObject(java.io.ObjectOutputStream s) throws java.io.IOException {
// 记录修改次数,用于反序列化时校验
int expectedModCount = modCount;
// 先序列化非transient的字段(如size)
s.defaultWriteObject();
// 只序列化实际有元素的部分,而不是整个数组
s.writeInt(size);
// 逐个写入元素,从0到size-1,跳过空位置
for (int i=0; i<size; i++) {
s.writeObject(elementData[i]);
}
if (modCount != expectedModCount) {
throw new ConcurrentModificationException();
}
}
对应的反序列化方法 readObject() 则会重新创建数组并恢复元素:
private void readObject(java.io.ObjectInputStream s) throws java.io.IOException, ClassNotFoundException {
elementData = EMPTY_ELEMENTDATA;
// 读取非transient字段
s.defaultReadObject();
// 读取元素数量
s.readInt();
// 恢复元素到新数组
if (size > 0) {
int capacity = calculateCapacity(elementData, size);
SharedSecrets.getJavaOISAccess().checkArray(s, Object[].class, capacity);
elementData = Arrays.copyOf(elementData, capacity);
for (int i=0; i<size; i++) {
elementData[i] = s.readObject();
}
}
}
可见,transient 修饰 elementData 是为了阻止默认序列化把整个数组(包括未使用的空位置)都写出去,转而通过自定义逻辑只序列化实际元素,既节省空间又提高效率,这正是 transient 在这里的核心作用。
简单归纳一下,其实就是ArrayList 实现了 Serializable 接口,支持序列化与反序列化,但其底层 elementData 数组往往容量大于实际存储的元素个数。如果直接序列化整个数组,会导致大量无效数据被写入流中,既浪费空间也影响效率。
为此,JDK 使用 transient 关键字标记 elementData,避免它在默认序列化中被处理,而是通过自定义的 writeObject()
方法进行精细控制。这个方法只会序列化 size
指定的有效元素,而非整个数组容量,从而提高序列化效率并保证数据一致性。同时,readObject()
会在反序列化时****重建数组并恢复数据**。这种做法体现了对性能与设计细节的精巧平衡。
