Java集合核心进阶:Collection与List底层全解析
本文聚焦Java集合两大核心模块——Collection顶层接口与List集合进阶,深度拆解Collection接口通用方法、迭代器体系(Iterator/增强for/ListIterator),以及List核心实现类(ArrayList/LinkedList)的底层原理、源码细节。
一、集合顶层核心:Collection接口与迭代器体系
集合是Java中用于存储、操作一组对象的容器,区别于数组(长度固定、类型单一),集合支持动态扩容、类型适配(泛型)、丰富操作(增删改查),是开发中使用频率最高的工具之一。集合体系的顶层是java.util.Collection接口,所有单列集合(List、Set、Queue等)均继承自该接口。
1.1 Collection顶层接口核心特征
1.1.1 核心定位
Collection是所有单列集合的父接口,定义了单列集合的通用操作方法,不直接实例化,而是通过实现类(如ArrayList、LinkedList、HashSet)来使用其功能。
1.1.2 常用通用方法(重点)
| 方法名 | 作用 | 返回值 | 适用场景 |
|---|---|---|---|
| boolean add(E e) | 向集合中添加一个元素 | true(添加成功)/false(添加失败,如Set集合添加重复元素) | 所有单列集合,添加单个元素 |
| boolean addAll(Collection<? extends E> c) | 将另一个集合的所有元素添加到当前集合 | true(至少添加一个元素)/false(未添加任何元素) | 批量添加元素,合并两个集合 |
| void clear() | 清空集合中的所有元素 | 无返回值(void) | 需要重置集合、释放资源场景 |
| boolean contains(Object o) | 判断集合中是否包含指定元素 | true(包含)/false(不包含) | 元素存在性校验(依赖equals()方法) |
| boolean containsAll(Collection<?> c) | 判断当前集合是否包含另一个集合的所有元素 | true(全部包含)/false(至少一个不包含) | 集合包含关系校验 |
| boolean isEmpty() | 判断集合是否为空(元素个数为0) | true(空集合)/false(非空集合) | 集合操作前的空判断,避免空指针异常 |
| Iterator iterator() | 获取集合的迭代器,用于遍历集合元素 | Iterator 迭代器对象 | 集合遍历(传统迭代方式,支持删除操作) |
| boolean remove(Object o) | 删除集合中指定的元素(仅删除第一个匹配元素) | true(删除成功)/false(删除失败,元素不存在) | 删除单个指定元素 |
| boolean removeAll(Collection<?> c) | 删除当前集合中包含的另一个集合的所有元素(差集操作) | true(至少删除一个元素)/false(未删除任何元素) | 批量删除元素,取两个集合的差集 |
| boolean retainAll(Collection<?> c) | 保留当前集合与另一个集合的共有元素(交集操作),删除其他元素 | true(集合发生变化)/false(集合无变化) | 取两个集合的交集 |
| int size() | 获取集合中元素的个数 | int类型,元素个数(0表示空集合) | 统计集合元素数量、循环遍历边界控制 |
| Object[] toArray() | 将集合转换为Object类型的数组 | Object[] 数组(元素顺序与集合一致) | 集合与数组的转换,适配数组相关操作 |
| T[] toArray(T[] a) | 将集合转换为指定类型的数组 | T[] 数组(指定类型,避免强制转换) | 类型安全的集合转数组操作(推荐使用) |
1.1.3 核心注意事项(面试避坑)
-
Collection接口中所有方法均为抽象方法,需由实现类(如ArrayList)重写实现,不能直接new Collection()实例化;
-
集合中存储的是对象的引用,而非对象本身(基本数据类型会自动装箱为包装类,如int→Integer);
-
contains()、remove()方法的底层依赖元素的equals()方法,若自定义对象未重写equals(),会使用Object类的equals()(比较地址值),导致判断结果异常;
-
addAll()、removeAll()、retainAll()方法的参数是Collection类型,需传入集合对象,不能直接传入单个元素。
1.2 迭代器体系:Iterator与增强for循环
遍历集合是开发中最常用的操作,Java提供了三种核心遍历方式:Iterator迭代器、增强for循环(for-each)、普通for循环(仅List集合可用),其中Iterator是Collection接口定义的标准遍历方式,所有单列集合均支持。
1.2.1 Iterator迭代器(核心)
1. 核心原理
Iterator(迭代器)是一个接口,定义了遍历集合的标准方法,由Collection的实现类(如ArrayList)提供具体实现。迭代器的核心思想是“统一遍历接口”,无论集合底层数据结构如何(数组、链表),都能通过相同的方式遍历。
2. 常用方法
| 方法名 | 作用 | 返回值 |
|---|---|---|
| boolean hasNext() | 判断集合中是否还有下一个元素(未遍历的元素) | true(有下一个元素)/false(无下一个元素) |
| E next() | 获取集合中的下一个元素,并将迭代器指针向后移动一位 | E类型(集合中元素的类型) |
| void remove() | 删除迭代器当前指向的元素(必须在next()之后调用) | 无返回值(void) |
3. Java代码实战
import java.util.ArrayList;
import java.util.Collection;
import java.util.Iterator;
public class IteratorDemo {
public static void main(String[] args) {
// 1. 创建Collection实现类(ArrayList)
Collection<String> coll = new ArrayList<>();
// 2. 添加元素
coll.add("Java");
coll.add("集合");
coll.add("进阶");
coll.add("面试");
// 3. 获取迭代器
Iterator<String> iterator = coll.iterator();
// 4. 遍历集合(核心写法)
while (iterator.hasNext()) {
// 获取下一个元素
String element = iterator.next();
System.out.println(element);
// 需求:删除元素“进阶”(迭代器遍历期间,只能用迭代器的remove(),不能用coll.remove())
if ("进阶".equals(element)) {
iterator.remove(); // 正确:迭代器删除,避免并发修改异常
// coll.remove(element); // 错误:遍历期间调用集合的remove(),会抛出ConcurrentModificationException
}
}
System.out.println("删除后的集合:" + coll); // 输出:[Java, 集合, 面试]
}
}
4. 核心避坑点(面试高频)
-
迭代器遍历期间,不能直接调用集合的add()、remove()方法,否则会抛出ConcurrentModificationException(并发修改异常),需使用迭代器自身的remove()方法;
-
remove()方法必须在next()之后调用,否则会抛出IllegalStateException(非法状态异常)(因为没有指向任何元素);
-
迭代器是“一次性”的,遍历结束后(hasNext()返回false),无法重新遍历,需重新获取迭代器(coll.iterator());
-
迭代器只能遍历元素,不能添加元素(Iterator接口无add()方法),若需遍历期间添加元素,需使用ListIterator(List集合专属迭代器)。
1.2.2 增强for循环(for-each,推荐)
1. 核心原理
增强for循环(JDK 5+新增)是Iterator迭代器的“语法糖”,底层还是通过Iterator实现,简化了遍历代码,无需手动获取迭代器、判断hasNext()、调用next()。
适用场景:仅用于遍历集合/数组,无需在遍历期间删除元素(删除会抛出并发修改异常)。
2. 语法格式
// 遍历集合
for (元素类型 变量名 : 集合对象) {
// 操作变量(变量代表集合中的每一个元素)
}
// 遍历数组(增强for也支持数组)
for (元素类型 变量名 : 数组对象) {
// 操作变量
}
3. Java代码实战
import java.util.ArrayList;
import java.util.Collection;
public class ForEachDemo {
public static void main(String[] args) {
Collection<String> coll = new ArrayList<>();
coll.add("Java");
coll.add("集合");
coll.add("进阶");
coll.add("面试");
// 增强for遍历集合(最简洁的遍历方式)
for (String element : coll) {
System.out.println(element);
// 注意:此处不能调用coll.remove(element),会抛出并发修改异常
}
// 增强for遍历数组
String[] arr = {"Java", "算法", "IO", "多线程"};
for (String s : arr) {
System.out.println(s);
}
}
}
4. 核心避坑点
-
增强for循环不能修改集合的结构(添加、删除元素),否则会抛出ConcurrentModificationException;
-
增强for循环遍历集合时,变量是集合元素的“副本”,修改变量的值不会影响集合中原本的元素(如String类型,修改变量不会改变集合中的字符串);
-
增强for循环不支持获取元素索引(List集合若需索引,需使用普通for循环)。
1.2.3 三种遍历方式对比(面试重点)
| 遍历方式 | 适用集合 | 优点 | 缺点 | 是否支持删除元素 |
|---|---|---|---|---|
| Iterator迭代器 | 所有单列集合(Collection的所有实现类) | 标准遍历方式,支持删除元素,灵活度高 | 代码相对繁琐,需手动操作迭代器 | 支持(使用iterator.remove()) |
| 增强for循环 | 所有单列集合、数组 | 代码简洁,无需手动操作迭代器 | 不支持删除元素,不支持获取索引 | 不支持(会抛出并发修改异常) |
| 普通for循环 | 仅List集合(有索引) | 支持获取索引,支持修改元素(通过索引) | 代码繁琐,仅适用于List集合,不适用于Set集合 | 支持(通过索引删除,如list.remove(index)) |
1.3 ListIterator:List集合专属迭代器(进阶)
ListIterator是Iterator的子接口,仅List集合(ArrayList、LinkedList等)支持,在Iterator的基础上,增加了“向前遍历”“添加元素”“修改元素”的功能,更灵活。
1.3.1 核心新增方法
| 方法名 | 作用 |
|---|---|
| boolean hasPrevious() | 判断集合中是否有上一个元素(向前遍历) |
| E previous() | 获取上一个元素,并将迭代器指针向前移动一位 |
| void add(E e) | 在迭代器当前指向的位置添加一个元素 |
| void set(E e) | 修改迭代器当前指向的元素(需在next()/previous()之后调用) |
1.3.2 Java代码实战
import java.util.ArrayList;
import java.util.List;
import java.util.ListIterator;
public class ListIteratorDemo {
public static void main(String[] args) {
List<String> list = new ArrayList<>();
list.add("Java");
list.add("集合");
list.add("进阶");
// 获取ListIterator迭代器(List集合专属)
ListIterator<String> listIterator = list.listIterator();
// 1. 向后遍历(与Iterator一致)
System.out.println("向后遍历:");
while (listIterator.hasNext()) {
String element = listIterator.next();
System.out.println(element);
// 修改当前元素
if ("集合".equals(element)) {
listIterator.set("List集合"); // 将“集合”改为“List集合”
}
}
// 2. 向前遍历(ListIterator独有)
System.out.println("向前遍历:");
while (listIterator.hasPrevious()) {
String element = listIterator.previous();
System.out.println(element);
// 添加元素
if ("Java".equals(element)) {
listIterator.add("基础"); // 在“Java”之前添加“基础”
}
}
System.out.println("最终集合:" + list); // 输出:[基础, Java, List集合, 进阶]
}
}
1.4 集合体系总结(顶层架构)
Java集合体系分为两大分支:单列集合(Collection)和双列集合(Map),本文重点讲解单列集合核心内容。
单列集合(Collection)核心分支:
-
List集合:有序、可重复、有索引(如ArrayList、LinkedList、Vector);
-
Set集合:无序、不可重复、无索引(如HashSet、LinkedHashSet、TreeSet);
-
Queue集合:队列(先进先出FIFO),多用于多线程、任务调度(如LinkedList、PriorityQueue)。
核心记忆点:Collection是所有单列集合的父接口,Iterator是所有单列集合的标准遍历方式,ListIterator是List集合专属迭代器,增强for是迭代器的语法糖。
二、List集合进阶:底层原理与源码解读(面试重点)
List集合是Collection接口的重要子接口,核心特征:有序(元素存入顺序与取出顺序一致)、可重复(允许存入相同元素)、有索引(可通过索引操作元素),是开发中使用最频繁的集合类型,核心实现类:ArrayList、LinkedList、Vector(已过时)。
2.1 List接口核心方法(补充Collection接口)
List接口继承自Collection接口,除了继承Collection的通用方法,还新增了与“索引”相关的方法(核心区别于Set集合)。
| 方法名 | 作用 | 返回值 | 注意事项 |
|---|---|---|---|
| void add(int index, E element) | 在指定索引位置添加元素,原索引及后续元素向后移动 | 无返回值 | 索引越界会抛出IndexOutOfBoundsException |
| boolean addAll(int index, Collection<? extends E> c) | 在指定索引位置添加另一个集合的所有元素 | true(添加成功)/false(添加失败) | 索引越界会抛出异常,原索引及后续元素向后移动 |
| E get(int index) | 根据索引获取集合中的元素 | E类型(集合元素类型) | 索引越界会抛出异常,最常用的方法之一 |
| int indexOf(Object o) | 查找指定元素在集合中第一次出现的索引 | int(找到返回索引,未找到返回-1) | 依赖元素的equals()方法 |
| int lastIndexOf(Object o) | 查找指定元素在集合中最后一次出现的索引 | int(找到返回索引,未找到返回-1) | 依赖元素的equals()方法 |
| E remove(int index) | 根据索引删除元素,原索引后续元素向前移动 | E类型(被删除的元素) | 索引越界会抛出异常 |
| E set(int index, E element) | 根据索引修改元素的值,替换原元素 | E类型(被替换的原元素) | 索引越界会抛出异常,不改变集合长度 |
| List subList(int fromIndex, int toIndex) | 获取从fromIndex(包含)到toIndex(不包含)的子集合 | List 子集合 | 子集合与原集合共用同一个底层数组,修改子集合会影响原集合 |
2.1.1 Java代码实战(List专属方法)
import java.util.ArrayList;
import java.util.List;
public class ListMethodDemo {
public static void main(String[] args) {
List<String> list = new ArrayList<>();
// 1. 添加元素(Collection方法)
list.add("Java");
list.add("算法");
list.add("集合");
// 2. 按索引添加元素(List专属)
list.add(1, "进阶"); // 在索引1位置添加“进阶”
System.out.println("添加后:" + list); // 输出:[Java, 进阶, 算法, 集合]
// 3. 按索引获取元素(List专属)
String element = list.get(2);
System.out.println("索引2的元素:" + element); // 输出:算法
// 4. 查找元素索引(List专属)
int firstIndex = list.indexOf("集合");
int lastIndex = list.lastIndexOf("Java");
System.out.println("集合第一次出现的索引:" + firstIndex); // 输出:3
System.out.println("Java最后一次出现的索引:" + lastIndex); // 输出:0
// 5. 按索引修改元素(List专属)
String oldElement = list.set(3, "面试"); // 将索引3的元素改为“面试”
System.out.println("被替换的元素:" + oldElement); // 输出:集合
System.out.println("修改后:" + list); // 输出:[Java, 进阶, 算法, 面试]
// 6. 按索引删除元素(List专属)
String removedElement = list.remove(1); // 删除索引1的元素
System.out.println("被删除的元素:" + removedElement); // 输出:进阶
System.out.println("删除后:" + list); // 输出:[Java, 算法, 面试]
// 7. 获取子集合(List专属)
List<String> subList = list.subList(0, 2); // 获取索引0-1的子集合
System.out.println("子集合:" + subList); // 输出:[Java, 算法]
// 修改子集合,原集合也会变化
subList.add("IO");
System.out.println("修改子集合后,原集合:" + list); // 输出:[Java, 算法, IO, 面试]
}
}
2.2 ArrayList底层原理与源码解读(面试高频)
2.2.1 核心定位与底层数据结构
ArrayList是List接口的最常用实现类,核心定位:基于动态数组实现,查询快、增删慢,适用于“查询频繁、增删较少”的场景(如数据展示、查询列表)。
底层数据结构:Object类型的动态数组(private transient Object[] elementData;),数组的长度可以动态扩容(区别于普通数组的固定长度)。
关键说明:transient关键字修饰elementData,意味着该数组不会被序列化(ArrayList自定义了序列化逻辑,只序列化实际存储的元素,节省空间)。
2.2.2 核心源码解读(JDK 8+)
1. 成员变量(核心)
// 默认初始容量(空参构造时,初始容量为0,第一次添加元素时扩容为10)
private static final int DEFAULT_CAPACITY = 10;
// 空数组(空参构造时使用)
private static final Object[] EMPTY_ELEMENTDATA = {};
// 空数组(默认容量的空数组,第一次添加元素时扩容为DEFAULT_CAPACITY)
private static final Object[] DEFAULTCAPACITY_EMPTY_ELEMENTDATA = {};
// 存储元素的数组(transient修饰,不参与默认序列化)
transient Object[] elementData;
// 集合中元素的实际个数(区别于数组长度)
private int size;
// 最大容量(避免数组扩容时超出Integer.MAX_VALUE)
private static final int MAX_ARRAY_SIZE = Integer.MAX_VALUE - 8;
2. 构造方法(3种)
// 1. 空参构造(最常用):初始化为空数组,第一次添加元素时扩容为10
public ArrayList() {
this.elementData = DEFAULTCAPACITY_EMPTY_ELEMENTDATA;
}
// 2. 指定初始容量的构造方法:适用于已知元素数量的场景,避免频繁扩容
public ArrayList(int initialCapacity) {
if (initialCapacity > 0) {
this.elementData = new Object[initialCapacity];
} else if (initialCapacity == 0) {
this.elementData = EMPTY_ELEMENTDATA;
} else {
throw new IllegalArgumentException("Illegal Capacity: " + initialCapacity);
}
}
// 3. 传入Collection集合的构造方法:将其他集合转换为ArrayList
public ArrayList(Collection<? extends E> c) {
elementData = c.toArray();
if ((size = elementData.length) != 0) {
// 若转换后的数组不是Object[]类型,转为Object[]
if (elementData.getClass() != Object[].class) {
elementData = Arrays.copyOf(elementData, size, Object[].class);
}
} else {
// 若集合为空,使用空数组
this.elementData = EMPTY_ELEMENTDATA;
}
}
3. 核心方法:add()(添加元素,触发扩容)
// 添加元素到集合末尾
public boolean add(E e) {
// 确保数组容量足够(核心:扩容判断)
ensureCapacityInternal(size + 1); // size是当前元素个数,size+1是添加后的个数
// 将元素添加到数组的size位置,然后size自增
elementData[size++] = e;
return true;
}
// 确保内部容量(核心扩容逻辑入口)
private void ensureCapacityInternal(int minCapacity) {
ensureExplicitCapacity(calculateCapacity(elementData, minCapacity));
}
// 计算需要的容量
private static int calculateCapacity(Object[] elementData, int minCapacity) {
// 若数组是默认空数组(空参构造初始化),返回默认容量10和minCapacity的最大值
if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA) {
return Math.max(DEFAULT_CAPACITY, minCapacity);
}
// 否则返回minCapacity(size+1)
return minCapacity;
}
// 确保显式容量(判断是否需要扩容)
private void ensureExplicitCapacity(int minCapacity) {
modCount++; // 记录集合修改次数(用于并发修改校验)
// 若需要的容量 > 数组当前长度,触发扩容
if (minCapacity - elementData.length > 0) {
grow(minCapacity); // 核心扩容方法
}
}
// 扩容方法(核心)
private void grow(int minCapacity) {
// 旧数组长度
int oldCapacity = elementData.length;
// 新数组长度 = 旧长度 + 旧长度/2(扩容1.5倍,位运算:oldCapacity >> 1 等价于 oldCapacity/2)
int newCapacity = oldCapacity + (oldCapacity >> 1);
// 若新容量 < 需要的容量,直接使用需要的容量(避免扩容不足)
if (newCapacity - minCapacity < 0) {
newCapacity = minCapacity;
}
// 若新容量超过最大容量,使用Integer.MAX_VALUE
if (newCapacity - MAX_ARRAY_SIZE > 0) {
newCapacity = hugeCapacity(minCapacity);
}
// 复制旧数组元素到新数组(扩容核心:数组拷贝)
elementData = Arrays.copyOf(elementData, newCapacity);
}
// 处理超大容量场景(补充grow()方法的收尾逻辑)
private static int hugeCapacity(int minCapacity) {
// 若minCapacity为负数,说明溢出(非法参数)
if (minCapacity < 0) {
throw new OutOfMemoryError();
}
// 若需要的容量超过最大容量,返回Integer.MAX_VALUE,否则返回MAX_ARRAY_SIZE
return (minCapacity > MAX_ARRAY_SIZE) ? Integer.MAX_VALUE : MAX_ARRAY_SIZE;
}
// 补充:指定索引添加元素的add()方法(面试高频,与末尾添加的区别)
public void add(int index, E element) {
// 1. 校验索引合法性(核心面试考点:索引越界异常的触发条件)
rangeCheckForAdd(index);
// 2. 确保容量足够,触发扩容(与末尾add()共用扩容逻辑)
ensureCapacityInternal(size + 1);
// 3. 数组拷贝:将index及后续元素向后移动一位,腾出index位置
// 核心:System.arraycopy(源数组, 源起始索引, 目标数组, 目标起始索引, 拷贝长度)
System.arraycopy(elementData, index, elementData, index + 1, size - index);
// 4. 在index位置存入新元素
elementData[index] = element;
// 5. 元素个数自增
size++;
}
// 索引校验方法(add(int index, E element)专属)
private void rangeCheckForAdd(int index) {
// 索引小于0或大于size(注意:不是大于数组长度),抛出索引越界异常
if (index > size || index < 0) {
throw new IndexOutOfBoundsException(outOfBoundsMsg(index));
}
}
// 索引越界异常提示信息(辅助方法,了解即可)
private String outOfBoundsMsg(int index) {
return "Index: " + index + ", Size: " + size;
}
4. 核心方法:get()(查询元素,体现查询快的优势)
// 根据索引查询元素(核心:直接通过数组索引访问,时间复杂度O(1))
public E get(int index) {
// 校验索引合法性(与add指定索引的校验逻辑一致)
rangeCheck(index);
// 直接返回数组对应索引的元素(数组随机访问特性,查询效率极高)
return elementData(index);
}
// 索引校验方法(get()、set()、remove()共用)
private void rangeCheck(int index) {
// 索引大于等于size(注意:不是大于数组长度),抛出异常
if (index >= size) {
throw new IndexOutOfBoundsException(outOfBoundsMsg(index));
}
}
// 辅助方法:将Object类型数组元素转为泛型E(避免强制转换,提升代码安全性)
@SuppressWarnings("unchecked")
E elementData(int index) {
return (E) elementData[index];
}
5. 核心方法:remove()(删除元素,体现增删慢的劣势)
// 1. 根据索引删除元素(面试高频)
public E remove(int index) {
// 校验索引合法性
rangeCheck(index);
modCount++; // 记录集合修改次数,用于并发修改校验
E oldValue = elementData(index); // 获取被删除的元素(用于返回)
// 计算需要拷贝的元素个数:size - index - 1(index后续的元素个数)
int numMoved = size - index - 1;
if (numMoved > 0) {
// 数组拷贝:将index+1及后续元素向前移动一位,覆盖被删除的元素
System.arraycopy(elementData, index + 1, elementData, index, numMoved);
}
// 清空最后一个元素的引用(帮助GC回收,避免内存泄漏)
elementData[--size] = null;
return oldValue; // 返回被删除的元素
}
// 2. 根据元素删除(底层依赖equals()方法,面试避坑点)
public boolean remove(Object o) {
if (o == null) {
// 处理元素为null的情况(遍历数组,找到第一个null元素)
for (int index = 0; index < size; index++) {
if (elementData[index] == null) {
fastRemove(index); // 快速删除(无索引校验,内部调用)
return true;
}
}
} else {
// 处理元素不为null的情况(调用equals()方法匹配元素)
for (int index = 0; index< size; index++) {
if (o.equals(elementData[index])) {
fastRemove(index);
return true;
}
}
}
return false; // 未找到元素,删除失败
}
// 快速删除方法(内部使用,跳过索引校验,提升效率)
private void fastRemove(int index) {
modCount++;
int numMoved = size - index - 1;
if (numMoved > 0) {
System.arraycopy(elementData, index + 1, elementData, index, numMoved);
}
elementData[--size] = null; // 清空引用,避免内存泄漏
}
2.2.3 ArrayList核心面试考点总结
-
底层数据结构:JDK 8+ 基于Object类型动态数组实现,transient修饰数组避免默认序列化,自定义序列化逻辑节省空间;
-
初始容量与扩容机制:空参构造初始容量为0,第一次add元素扩容为10;后续扩容为原容量的1.5倍(位运算实现,效率更高);指定初始容量可避免频繁扩容;
-
增删慢、查询快的原因:查询(get())直接通过数组索引访问,时间复杂度O(1);增删(add(index)、remove())需要移动数组元素,时间复杂度O(n),元素越多效率越低;
-
并发修改异常:modCount记录集合修改次数,迭代器遍历期间,若集合结构(add/remove)发生变化,modCount与迭代器的expectedModCount不相等,抛出ConcurrentModificationException;
-
equals()方法的影响:contains()、remove(Object o)、indexOf()等方法依赖元素的equals(),自定义对象未重写equals()会导致判断异常;
-
内存泄漏:remove()方法会主动将最后一个元素置为null,帮助GC回收,避免内存泄漏;
-
与Vector的区别:Vector是线程安全的(方法加synchronized),效率低;ArrayList线程不安全,效率高,开发中多使用ArrayList,线程安全场景可使用Collections.synchronizedList()或CopyOnWriteArrayList。
2.3 LinkedList底层原理与源码解读(面试高频)
2.3.1 核心定位与底层数据结构
LinkedList是List接口的另一个核心实现类,核心定位:基于双向链表实现,查询慢、增删快,适用于“增删频繁、查询较少”的场景(如队列、栈、频繁插入删除的列表)。
底层数据结构:双向链表,每个节点(Node)包含三个部分:前驱节点引用(prev)、本身元素(item)、后继节点引用(next),节点之间通过引用关联,无需连续的内存空间。
关键说明:LinkedList同时实现了Deque接口,因此可以作为队列(FIFO)、栈(LIFO)使用,功能更灵活。
2.3.2 核心源码解读(JDK 8+)
1. 内部节点类(核心,双向链表的基础)
// 私有静态内部类,代表双向链表的一个节点
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. 成员变量(核心)
// 链表首节点(头节点),初始为null
transient Node<E> first;
// 链表尾节点(尾节点),初始为null
transient Node<E> last;
// 链表中元素的实际个数(与ArrayList的size含义一致)
private int size = 0;
// 集合修改次数(与ArrayList的modCount作用一致,用于并发修改校验)
transient int modCount = 0;
3. 构造方法(2种)
// 1. 空参构造(最常用):初始化头节点和尾节点为null,size为0
public LinkedList() {
}
// 2. 传入Collection集合的构造方法:将其他集合元素添加到链表中
public LinkedList(Collection<? extends E> c) {
this();
addAll(c); // 调用addAll()方法批量添加元素
}
4. 核心方法:add()(添加元素,体现增删快的优势)
// 1. 向链表末尾添加元素(最常用,时间复杂度O(1))
public boolean add(E e) {
linkLast(e); // 链接到链表末尾
return true;
}
// 核心:将元素链接到链表末尾(私有方法)
void linkLast(E e) {
// 保存当前尾节点(last)为临时变量l
final Node<E> l = last;
// 创建新节点:前驱为l(原尾节点),元素为e,后继为null
final Node<E> newNode = new Node<>(l, e, null);
// 将新节点设为新的尾节点
last = newNode;
// 若原尾节点为null(链表为空),则新节点同时作为头节点
if (l == null) {
first = newNode;
} else {
// 原尾节点的后继指向新节点,完成链接
l.next = newNode;
}
// 元素个数自增
size++;
// 修改次数自增
modCount++;
}
// 2. 向指定索引添加元素(面试高频,时间复杂度O(n),但比ArrayList的add(index)高效)
public void add(int index, E element) {
// 校验索引合法性(与ArrayList一致:index >=0 && index <= size)
checkPositionIndex(index);
// 若索引等于size,直接添加到末尾(调用linkLast())
if (index == size) {
linkLast(element);
} else {
// 找到index位置的节点(succ),将新节点链接到succ之前
linkBefore(element, node(index));
}
}
// 校验索引合法性(add(index)、get(index)等方法共用)
private void checkPositionIndex(int index) {
if (!isPositionIndex(index)) {
throw new IndexOutOfBoundsException(outOfBoundsMsg(index));
}
}
// 判断索引是否合法(0 <= index <= size)
private boolean isPositionIndex(int index) {
return index >= 0 && index <= size;
}
// 核心:根据索引查找节点(面试考点,体现查询慢的原因)
Node<E> node(int 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;
}
}
// 核心:将元素链接到指定节点(succ)之前
void linkBefore(E e, Node<E> succ) {
// 保存succ的前驱节点(pred)
final Node<E> pred = succ.prev;
// 创建新节点:前驱为pred,元素为e,后继为succ
final Node<E> newNode = new Node<>(pred, e, succ);
// 将succ的前驱指向新节点
succ.prev = newNode;
// 若pred为null(succ是头节点),则新节点作为头节点
if (pred == null) {
first = newNode;
} else {
// pred的后继指向新节点,完成链接
pred.next = newNode;
}
// 元素个数和修改次数自增
size++;
modCount++;
}
5. 核心方法:get()(查询元素,体现查询慢的劣势)
// 根据索引查询元素(核心:调用node()方法查找节点,时间复杂度O(n))
public E get(int index) {
// 校验索引合法性
checkElementIndex(index);
// 查找index位置的节点,返回节点的元素
return node(index).item;
}
// 校验元素索引合法性(get()、set()、remove()共用,与checkPositionIndex区别)
private void checkElementIndex(int index) {
if (!isElementIndex(index)) {
throw new IndexOutOfBoundsException(outOfBoundsMsg(index));
}
}
// 判断元素索引是否合法(0 <= index < size,区别于positionIndex的<=size)
private boolean isElementIndex(int index) {
return index >= 0 && index < size;
}
6. 核心方法:remove()(删除元素,体现增删快的优势)
// 1. 根据索引删除元素(时间复杂度O(n),主要耗时在查找节点)
public E remove(int index) {
// 校验索引合法性
checkElementIndex(index);
// 找到index位置的节点,调用unlink()方法删除
return unlink(node(index));
}
// 2. 根据元素删除(底层依赖equals()方法,与ArrayList逻辑类似)
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;
}
// 核心:删除指定节点(时间复杂度O(1),核心优势所在)
E unlink(Node<E> x) {
// 保存节点x的元素、前驱、后继
final E element = x.item;
final Node<E> next = x.next;
final Node<E> prev = x.prev;
// 处理前驱节点:若prev为null(x是头节点),则头节点设为next;否则prev的后继指向next
if (prev == null) {
first = next;
} else {
prev.next = next;
x.prev = null; // 清空x的前驱引用,帮助GC回收
}
// 处理后继节点:若next为null(x是尾节点),则尾节点设为prev;否则next的前驱指向prev
if (next == null) {
last = prev;
} else {
next.prev = prev;
x.next = null; // 清空x的后继引用,帮助GC回收
}
// 清空x的元素引用,帮助GC回收
x.item = null;
// 元素个数减少,修改次数自增
size--;
modCount++;
// 返回被删除的元素
return element;
}
2.3.3 LinkedList核心面试考点总结
-
底层数据结构:基于双向链表实现,每个节点包含prev、item、next三个部分,无需连续内存空间;
-
增删快、查询慢的原因:增删(add、remove)仅需修改节点的prev和next引用,时间复杂度O(1)(查找节点除外);查询(get)需遍历链表查找节点,时间复杂度O(n),虽有首尾遍历优化,但效率仍低于ArrayList;
-
与ArrayList的核心区别(面试高频对比): 底层结构:ArrayList是动态数组,LinkedList是双向链表;
-
效率:ArrayList查询O(1)、增删O(n);LinkedList增删O(1)(查找节点除外)、查询O(n);
-
内存占用:ArrayList需预留数组扩容空间,内存利用率较低;LinkedList每个节点需存储前后引用,内存占用略高;
-
适用场景:ArrayList适用于查询频繁、增删少;LinkedList适用于增删频繁、查询少,或作为队列/栈使用。
-
队列/栈功能:实现Deque接口,可调用offer()、poll()(队列)、push()、pop()(栈)等方法,功能更灵活;
-
并发安全性:与ArrayList一致,线程不安全,修改时需加锁或使用Collections.synchronizedList();
-
节点查找优化:node()方法会根据索引位置,选择从头或从尾遍历,减少遍历次数,提升查询效率(面试常考)。
2.4 List集合面试高频总结
List集合是Java面试中集合模块的核心考点,重点掌握以下内容,轻松应对面试提问:
-
List接口核心特征:有序、可重复、有索引,继承Collection接口,新增索引相关方法;
-
ArrayList与LinkedList对比:底层结构、效率、内存占用、适用场景(重中之重);
-
ArrayList扩容机制:初始容量、扩容比例、扩容触发条件、超大容量处理;
-
LinkedList节点结构:双向链表节点的组成、增删节点的核心逻辑;
-
并发修改异常:产生原因(modCount与expectedModCount不相等)、避免方式;
-
equals()方法的影响:contains()、remove(Object o)等方法的底层依赖,自定义对象重写equals()的必要性;
-
List遍历方式:三种遍历方式的适用场景、优缺点,尤其是迭代器的使用注意事项。