【JAVA基础面经】List(Vector+ArrayList+LinkedList)

文章目录


一、List

List 是 Java 集合框架中最常用的接口之一,代表一个有序、可重复的元素集合。Java 提供了多种 List 实现类,其中最常见的是 ArrayList、LinkedList 和 Vector

List 接口继承自 Collection,提供了对元素的有序操作。常用方法包括:

方法 说明
add(E e) 将元素添加到列表末尾
get(int index) 根据索引获取列表中的元素
remove(int index) 删除指定位置的元素
size() 返回列表中的元素个数
contains(Object o) 判断列表中是否包含指定元素

List 的主要实现类包括 ArrayList、LinkedList、Vector、Stack 以及 JUC 包下的 CopyOnWriteArrayList

二、Vector

Vector 与 ArrayList 一样基于动态数组实现,但它通过 synchronized 关键字保证了线程安全。不过,正是由于这把"全局锁",它在高并发场景下的性能表现远不如现代 JUC 并发容器。

Vector 的底层是一个 Object[] 数组(名为 elementData),它继承了 AbstractList,实现了 List、RandomAccess、Cloneable、Serializable 接口。

java 复制代码
public class Vector<E> extends AbstractList<E>
    implements List<E>, RandomAccess, Cloneable, java.io.Serializable {
    
    protected Object[] elementData;  // 存储元素的数组
    protected int elementCount;      // 实际元素个数
    protected int capacityIncrement; // 扩容增量(可自定义)
}

1.线程安全机制

Vector 通过在所有公开方法上添加 synchronized 关键字来保证线程安全,锁对象为当前 Vector 实例本身(即 this)

java 复制代码
public synchronized boolean add(E e) { ... }
public synchronized E get(int index) { ... }
public synchronized E set(int index, E element) { ... }
public synchronized boolean remove(Object o) { ... }
public synchronized int size() { ... }

这种设计确保了单次方法调用的原子性,但也意味着任意时刻只能有一个线程执行操作,读写全部互斥,并发吞吐量极低。

2.扩容机制

Vector 的扩容策略更加灵活:

  • 默认扩容:容量翻倍,即新容量 = 旧容量 × 2。

  • 自定义增量:通过构造函数传入 capacityIncrement 参数,扩容时新容量 = 旧容量 + capacityIncrement(若该值 ≤ 0,则仍采用翻倍策略)。

java 复制代码
private void grow(int minCapacity) {
    int oldCapacity = elementData.length;
    // 若 capacityIncrement > 0,则按增量扩容;否则翻倍
    int newCapacity = oldCapacity + 
        ((capacityIncrement > 0) ? capacityIncrement : oldCapacity);
    if (newCapacity - minCapacity < 0)
        newCapacity = minCapacity;
    if (newCapacity - MAX_ARRAY_SIZE > 0)
        newCapacity = hugeCapacity(minCapacity);
    elementData = Arrays.copyOf(elementData, newCapacity);
}

3.Vector的淘汰

尽管 Vector 是线程安全的,但它属于方法级的粗暴同步:

  • 锁粒度太粗(锁保护范围太大):读操作也加锁,多线程并发读取时仍需排队,无法发挥多核优势。(读操作本可以并发执行,却被强制串行化。)

  • 复合操作不安全:单个方法原子,但 check-then-act仍会出错,需手动加锁。如下例所示,

java 复制代码
Vector<Integer> vec = new Vector<>();

// 两个线程同时执行:如果 vec 中不包含 1,就添加 1
Thread t1 = new Thread(() -> {
    if (!vec.contains(1)) {      // 步骤1:检查
        vec.add(1);              // 步骤3:行动
    }
});

Thread t2 = new Thread(() -> {
    if (!vec.contains(1)) {      // 步骤2:检查
        vec.add(1);              // 步骤4:行动
    }
});

t1.start();
t2.start();
t1.join();
t2.join();

System.out.println(vec); // 可能输出 [1, 1] ------ 1 被添加了两次!

此时需要外部手动加锁,或使用并发容器提供的原子复合操作(例如 ConcurrentHashMap 提供的 putIfAbsent 等方法)

java 复制代码
synchronized (vec) {
    if (!vec.contains(1)) {
        vec.add(1);
    }
}
  • 现代替代品更优:
    • 读多写少 → CopyOnWriteArrayList(读无锁)。
    • 读写均衡 → Collections.synchronizedList(new ArrayList<>())。
    • 高并发随机写 → 手动使用 ReentrantReadWriteLock。

三、ArrayList

ArrayList源码如下,

java 复制代码
public class ArrayList<E> extends AbstractList<E>
        implements List<E>, RandomAccess, Cloneable, java.io.Serializable
{
    // 默认初始容量
    private static final int DEFAULT_CAPACITY = 10;
    
    // 空数组实例(用于指定容量为0时的共享空数组)
    private static final Object[] EMPTY_ELEMENTDATA = {};
    
    // 默认容量的空数组实例(用于无参构造)
    private static final Object[] DEFAULTCAPACITY_EMPTY_ELEMENTDATA = {};
    
    // 真正存储元素的数组,transient 表示不参与序列化
    transient Object[] elementData;
    
    // 集合中实际元素个数
    private int size;
    // ...
}
  • ArrayList 是泛型集合,使用时需要指定类型参数(如 ArrayList<Integer>)
  • 实现 RandomAccess:底层数组存储,支持按下标快速随机访问(get(i) 通常 O(1))
  • 实现 Cloneable:支持 clone(),但属于浅拷贝(元素对象本身不深拷贝)
  • 实现 Serializable:支持序列化/反序列化(用于持久化/网络传输等场景)
  • 线程不安全:多线程场景可选 Vector(同步开销大)或 CopyOnWriteArrayList(读多写少更合适)。
  • 底层是连续数组,可动态扩容,属于动态顺序表;插入/删除中间元素需要搬移(通常 O(n))

1.构造方法

方法 解释
ArrayList() 无参构造
ArrayList(int initialCapacity) 指定顺序表初始容量
ArrayList(Collection<? extends E> c) 利用其他Collection构建 ArrayList

(1)无参构造:先让 elementData 指向一个 默认空数组占位 (DEFAULTCAPACITY_EMPTY_ELEMENTDATA),此时 size=0、elementData.length=0。第一次 add 才分配底层数组,直接分配默认容量 10。

java 复制代码
// 1. 无参构造:懒加载,初始是一个空数组
public ArrayList() {
		//DEFAULTCAPACITY_EMPTY_ELEMENTDATA是只是暂时还没分配的数组
    this.elementData = DEFAULTCAPACITY_EMPTY_ELEMENTDATA;
}

(2)指定初始容量:

java 复制代码
public ArrayList(int initialCapacity) {          // 指定初始容量的构造方法
    if (initialCapacity > 0) {                   // 1) 如果容量 > 0
        this.elementData = new Object[initialCapacity]; // 直接分配一个指定长度的 Object 数组作为底层存储
    } else if (initialCapacity == 0) {           // 2) 如果容量 == 0
        this.elementData = EMPTY_ELEMENTDATA;    // 不分配新数组,引用一个共享的空数组(明确表示"容量就是0")
    } else {                                     // 3) 如果容量 < 0
        throw new IllegalArgumentException(
            "Illegal Capacity: " + initialCapacity
        );                                       // 抛异常:容量不允许为负数
    }
}

(3)传入一个已有的集合

java 复制代码
// 3. 传入一个已有的集合:用集合里的元素来初始化 ArrayList
public ArrayList(Collection<? extends E> c) {
		// 把集合转成数组,作为底层存储(注意:返回类型是 Object[],但运行时实际类型不一定真的是 Object[])
    elementData = c.toArray();             
    
    // 把 size 设为数组长度;如果不为 0,说明集合里有元素
    if ((size = elementData.length) != 0) {
        
        // c.toArray() 可能返回"不是 Object[] 的数组"
        // 例如某些集合实现可能返回 T[](运行时类型是 String[]、Integer[] 等)
        // 但 ArrayList 内部希望 elementData 的运行时类型就是 Object[]
        if (elementData.getClass() != Object[].class)
            elementData = Arrays.copyOf(elementData, size, Object[].class); // 拷贝成真正的 Object[],避免后续存取/扩容时的类型问题
    } else {
        // 如果集合为空:复用共享空数组,不额外分配
        elementData = EMPTY_ELEMENTDATA;
    }
}

2.创建 ArrayList 对象

java 复制代码
// 1) 无参构造:默认容量策略
ArrayList<Integer> list1 = new ArrayList<>();
List<Integer> list1 = new ArrayList<>();

//2) 指定初始容量构造
ArrayList<Integer> list2 = new ArrayList<>(10);
List<Integer> list2 = new ArrayList<>(10);

// 3) 传入已有集合构造(用集合元素初始化)
List<Integer> src = Arrays.asList(1, 2, 3);
ArrayList<Integer> list3 = new ArrayList<>(src);
List<Integer> list3 = new ArrayList<>(src);
  • ArrayList list = new ArrayList<>(); 既能用 List 里定义的方法,也能用 ArrayList 自己额外提供的方法(例如 ensureCapacity()、trimToSize() 等)
  • List list1 = new ArrayList<>();是一个向上转型的操作,只能调用 List 接口暴露的方法(add/get/remove/size/...),不能直接调用 ArrayList 的特有方法

3.ArrayList 的常用方法

方法 解释
boolean add(E e) 尾插е
void add (int index, E element) 将e插入到index位置
boolean addAll(Collection<? extends E> c) 尾插c中的元素
E remove(int index) 删除index位置元素
boolean remove(Object o) 删除遇到的第一个o
E get(int index) 获取下标 index位置元素
E set(int index, E element) 将下标index位置元素设置为 element
void clear() 清空
boolean contains(Object o) 判断o是否在线性表中
int indexOf(Object o) 返回第一个o所在下标
int lastIndexOf(Object o) 返回最后一个o的下标
List<E> subList(int fromIndex,int tolndex) 截取部分list
java 复制代码
public static void main(String[] args) {
    List<String> list = new ArrayList<>();
    list.add("JavaSE");
    list.add("JavaWeb");
    list.add("JavaEE");
    list.add("JVM");
    list.add("测试课程");
    System.out.println(list);//[JavaSE, JavaWeb, JavaEE, JVM, 测试课程]

    // 获取list中有效元素个数
    System.out.println(list.size());//5

    // 获取和设置index位置上的元素,注意index必须介于[0, size)间
    System.out.println(list.get(1));//JavaWeb
    list.set(1, "JavaWEB");
    System.out.println(list.get(1));//JavaWEB

    // 在list的index位置插入指定元素,index及后续的元素统一往后搬移一个位置
    list.add(1, "Java数据结构");
    System.out.println(list);//[JavaSE, Java数据结构, JavaWEB, JavaEE, JVM, 测试课程]
    // 删除指定元素,找到了就删除,该元素之后的元素统一往前搬移一个位置
    list.remove("JVM");
    System.out.println(list);//[JavaSE, Java数据结构, JavaWEB, JavaEE, 测试课程]
    // 删除list中index位置上的元素,注意index不要超过list中有效元素个数,否则会抛出下标越界异常
    list.remove(list.size()-1);
    System.out.println(list);//[JavaSE, Java数据结构, JavaWEB, JavaEE]

    // 检测list中是否包含指定元素,包含返回true,否则返回false
    if(list.contains("测试课程")){
        list.add("测试课程");//未输出
    }

    // 查找指定元素第一次出现的位置:indexOf从前往后找,lastIndexOf从后往前找
    list.add("JavaSE");//此时ArrayList为[JavaSE, Java数据结构, JavaWEB, JavaEE,JavaSE]
    System.out.println(list.indexOf("JavaSE"));//0
    System.out.println(list.lastIndexOf("JavaSE"));//4

    // 使用list中[0, 4)之间的元素构成一个新的SubList返回,但是和ArrayList共用一个elementData数组
    List<String> ret = list.subList(0, 4);
    System.out.println(ret);//[JavaSE, Java数据结构, JavaWEB, JavaEE]

    ret.set(0, "NewJavaSE");
    System.out.println(ret);//[NewJavaSE, Java数据结构, JavaWEB, JavaEE]
    System.out.println(list);//[NewJavaSE, Java数据结构, JavaWEB, JavaEE, JavaSE]

    list.clear();
    System.out.println(list.size());//0
}

注意:对于subList来说,不是创建了一个ArrayList的副本,而是直接持有了原 ArrayList 的 elementData 引用

由于 SubList 是原列表的一个视图,在通过 ret 迭代或操作期间,禁止对原列表 list 进行结构性修改(增删元素),否则会抛出 ConcurrentModificationException。

java 复制代码
List<String> list = new ArrayList<>(Arrays.asList("A", "B", "C", "D"));
List<String> sub = list.subList(0, 2);

list.add("E"); // 原列表结构性修改(增加元素)

System.out.println(sub); // 抛出 ConcurrentModificationException

4.ArrayList 的遍历

(1)普通 for + 下标

java 复制代码
List<Integer> list = new ArrayList<>(List.of(1, 2, 3, 4, 5));

for (int i = 0; i < list.size(); i++) {
	System.out.print(list.get(i) + " ");
}

注意:遍历时不建议直接结构性修改(容易漏/越界)

这里的结构性修改指list.add(e)、list.remove(index)改变列表大小的操作

例如在下面的循环里 remove(i),后面的元素会左移,很容易漏掉元素或 IndexOutOfBoundsException:

java 复制代码
// 不推荐:可能漏删
for (int i = 0; i < list.size(); i++) {
    if (list.get(i) % 2 == 0) {
        list.remove(i); // 删除后元素左移,i++ 会跳过下一个元素
    }
}

可以使用倒序来避免这个问题(更推荐使用 Iterator / removeIf)

java 复制代码
for (int i = list.size() - 1; i >= 0; i--) {
    if (list.get(i) % 2 == 0) {
        list.remove(i);
    }
}

(2)foreach

java 复制代码
for (Integer x : list) {
    System.out.println(x);
}

注意:底层是 Iterator,如果在循环体里 list.add/remove(...) 等结构性修改,会触发 CME(ConcurrentModificationException),

java 复制代码
for (Integer x : list) {
    if (x % 2 == 0) {
        list.remove(x); // 运行时很可能抛 ConcurrentModificationException
    }
}

(3)Iterator

Iterator 是集合遍历的标准安全方式,允许在遍历过程中通过 iterator.remove() 安全删除元素,而不会触发 CME。

java 复制代码
Iterator<Integer> it = list.iterator();
while (it.hasNext()) {
    Integer x = it.next();
    if (x % 2 == 0) {
        it.remove(); // 正确:删除当前迭代到的元素
    }
}
System.out.println(list); // [1, 3, 5]

iterator.remove() 删除的是上一次 next() 返回的元素,因此必须先调用 next() 再调用 remove();该方式仅支持删除,不支持结构性修改。

(4)ListIterator

ListIterator 是 Iterator 的增强版,专为 List 设计,提供了更丰富的操作能力,其支持以下方法,

方法 说明
hasNext() / next() 正向遍历
hasPrevious() / previous() 反向遍历
nextIndex() / previousIndex() 获取当前元素的前后索引
add(E e) 在当前迭代位置插入元素
set(E e) 替换上一次返回的元素
remove() 删除上一次返回的元素

a.正向与反向遍历

java 复制代码
List<String> list = new ArrayList<>(List.of("A", "B", "C", "D"));
ListIterator<String> listIt = list.listIterator();

// 正向遍历
System.out.print("正向:");
while (listIt.hasNext()) {
    System.out.print(listIt.next() + " ");
}
System.out.println();

// 反向遍历(必须先移动迭代器到末尾)
System.out.print("反向:");
while (listIt.hasPrevious()) {
    System.out.print(listIt.previous() + " ");
}
System.out.println();

b.遍历中安全地增删改

java 复制代码
List<String> list = new ArrayList<>(List.of("Java", "Python", "C++"));
ListIterator<String> it = list.listIterator();

while (it.hasNext()) {
    String s = it.next();
    if ("Python".equals(s)) {
        it.set("Go");      //  修改当前元素
    }
    if ("C++".equals(s)) {
        it.remove();       //  删除当前元素
    }
    if ("Java".equals(s)) {
        it.add("Kotlin");  //  在当前元素之后插入新元素
    }
}

System.out.println(list); // [Java, Kotlin, Go]

5.ArrayList 的扩容机制

ArrayList 扩容主要分为两种情况:

  • 首次扩容:当使用 new ArrayList() 创建对象时,JDK 8+ 采用了延迟初始化(懒加载) 策略,此时底层的 elementData 是一个空数组。只有当我们第一次调用 add 方法时,才会触发扩容,分配一个长度为默认值 10 的新数组。
  • 后续扩容:当连续添加元素,直到 size + 1(即所需的最小容量)大于当前数组 elementData.length 时,扩容才会被再次触发。

(2)扩容过程

创建新的数组容量(原来的1.5倍),并进行数组的复制

扩容的核心逻辑封装在 grow 方法中,下面逐行解析关键代码(基于 JDK 8+):

  • 计算新容量 (newCapacity):首先计算一个默认的新容量,其值为 oldCapacity + (oldCapacity >> 1)。这里的 >> 1 是位运算,等价于除以 2,因此整个表达式的结果就是旧容量的 1.5 倍。

  • 容量修正 (minCapacity):如果计算出的 newCapacity 仍然小于当前添加元素所需的最小容量 minCapacity(例如通过 addAll 批量添加时),则直接将 minCapacity 作为新的容量值,确保能够容纳所有元素。

  • 处理超大容量 (hugeCapacity):如果新容量超过了 MAX_ARRAY_SIZE(值为 Integer.MAX_VALUE - 8),会调用 hugeCapacity 方法做最终处理,确保容量不会越界。这是 JVM 为存储对象头保留的 8 个字节空间。

  • 执行复制 (Arrays.copyOf):最后调用 Arrays.copyOf,这个方法内部会创建一个指定长度的新数组 ,并使用高效的 System.arraycopy(native 方法)将旧数组元素复制过去。这意味着扩容本质上是一个 O(n) 的操作,代价相对较高。

扩容操作涉及到数组的复制和内存的重新分配,在频繁添加大量元素时扩容可能会影响性能。可以在初始化ArrayList时预分配足够大的容量,避免带来性能损耗。

(3)扩容为什么是 1.5 倍?

1.5 倍是 Java 设计者在时间(扩容频率) 和空间(内存利用率) 之间寻找到的最佳平衡点:

  • 若倍数太小(如 1.1 倍):优点是可减少内存浪费;但缺点也很明显,扩容会变得非常频繁,导致性能下降。例如从 10 扩容到 100,1.5倍只需要约 4 次扩容,而 1.1 倍则需要约 24 次。
  • 若倍数太大(如 2 倍):优点是扩容次数少,性能损耗小;但缺点是可能造成巨大的内存浪费,尤其是在容量接近 2 的幂次方时

6.二维ArrayList

Java 里没有真正"二维 ArrayList"这种类型,通常用 嵌套泛型来表示二维结构:List<List<T>>。它本质是 外层列表存每一行(row)的引用,内层列表存该行的元素 。

下面代码中创建二维 ArrayList,外层 List表示多行;内层 List<Integer>表示一行的数据。

java 复制代码
public static void main(String[] args) {
    List<List<Integer>> list = new ArrayList<>();

    List<Integer> row1 = new ArrayList<>();
    row1.add(1);
    row1.add(2);
    list.add(row1);

    List<Integer> row2 = new ArrayList<>();
    row2.add(11);
    row2.add(21);
    list.add(row2);

    System.out.println(list); // [[1, 2], [11, 21]]

    int v = list.get(1).get(0); // 取第2行第1列 -> 11
    list.get(0).set(1, 200);// // 把第1行第2列改成 200
    System.out.println(list);//[[1, 200], [11, 21]]

    list.add(new ArrayList<>());       // 新增空行
    list.get(0).add(999);              // 给第1行追加一个元素(相当于新增一列)
    list.get(1).add(0, 777);   // 在第2行指定位置插入
    System.out.println(list);          //[[1, 200, 999], [777, 11, 21], []]
}

可以使用下标遍历或者for- each 遍历二维 ArrayList

java 复制代码
List<List<Integer>> jagged = new ArrayList<>();
jagged.add(new ArrayList<>(List.of(1, 2, 3)));
jagged.add(new ArrayList<>(List.of(4)));
//下标遍历
for (int i = 0; i < jagged .size(); i++) {
    for (int j = 0; j < jagged .get(i).size(); j++) {
        System.out.print(jagged .get(i).get(j) + " ");
    }
    System.out.println();
}
//for- each 遍历
for (List<Integer> row : list) {
    for (Integer x : row) {
        System.out.print(x + " ");
    }
    System.out.println();
}

四、LinkedList

LinkedList的源码如下

java 复制代码
public class LinkedList<E>
    extends AbstractSequentialList<E>
    implements List<E>, Deque<E>, Cloneable, java.io.Serializable
{ 
		// 链表长度(节点个数)
    transient int size = 0;

		// 指向第一个节点
    transient Node<E> first;

		// 指向最后一个节点
    transient Node<E> last;
    ......
}
  • AbstractSequentialList:提供顺序访问的骨架实现
  • List接口:提供列表的基本操作
  • Deque接口:支持双端队列操作(这是LinkedList的特色)
  • Cloneable:支持克隆
  • Serializable:支持序列化

三个成员变量都用transient修饰,意味着它们不会参与默认序列化

LinkedList的底层是一个双向链表,其节点定义如下,每个节点都持有对前驱和后继节点的引用。LinkedList的任意位置插入和删除元素时效率比较高,时间复杂度为O(1),比较适合任意位置插入的场景。

java 复制代码
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;
    }
}

1.构造方法及LinkedList的创建

方法 解释
LinkedList() 无参构造
public LinkedList(Collection<? extends E> c) 使用其他集合容器中元素构造List
java 复制代码
public static void main(String[] args) {
    // 构造一个空的LinkedList
    // 只能使用List接口定义的方法
    List<Integer> list1 = new LinkedList<>();
    //可以调用LinkedList类的所有公开方法
    LinkedList<Integer> LinkedList = new LinkedList<>();

    List<String> list2 = new java.util.ArrayList<>();
    list2.add("JavaSE");
    list2.add("JavaWeb");
    list2.add("JavaEE");
    // 使用ArrayList构造LinkedList
    List<String> list3 = new LinkedList<>(list2);
}

2.LinkedList常用方法

方法 解释
boolean add(E e) 尾插е
void add(int index, E element) 将e插入到 index位置
boolean addAll(Collection<? extends E> c) 尾插c中的元素
E remove(int index) 删除 index 位置元素
boolean remove(Object o) 删除遇到的第一个o
E get(int index) 获取下标index位置元素
E set(int index, E element) 将下标index位置元素设置为element
void clear() 清空
boolean contains(Object o) 判断o是否在线性表中
int indexOf(Object o) 返回第一个o所在下标
int lastIndexOf(Object o) 返回最后一个o的下标
List<E> subList(int fromIndex, int toIndex) 截取部分 list
java 复制代码
public static void main(String[] args) {
    LinkedList<Integer> list = new LinkedList<>();
    list.add(1); // add(elem): 表示尾插
    list.add(2);
    list.add(3);
    list.add(4);
    list.add(5);
    list.add(6);
    list.add(7);
    System.out.println(list.size());//7
    System.out.println(list);//[1, 2, 3, 4, 5, 6, 7]

    // 在起始位置插入0
    list.add(0, 0); // add(index, elem): 在index位置插入元素elem
    System.out.println(list);//[0, 1, 2, 3, 4, 5, 6, 7]

    list.remove(); // remove(): 删除第一个元素,内部调用的是removeFirst()
    list.removeFirst(); // removeFirst(): 删除第一个元素
    list.removeLast(); // removeLast(): 删除最后元素
    list.remove(1); // remove(index): 删除index位置的元素
    System.out.println(list);//[2, 4, 5, 6]

    // contains(elem): 检测elem元素是否存在,如果存在返回true,否则返回false
    if(!list.contains(1)){
        list.add(0, 1);
    }
    list.add(1);
    System.out.println(list);//[1, 2, 4, 5, 6, 1]
    System.out.println(list.indexOf(1)); // indexOf(elem): 从前往后找到第一个elem的位置,输出0
    System.out.println(list.lastIndexOf(1)); // lastIndexOf(elem): 从后往前找第一个1的位置,输出5
    int elem = list.get(0); // get(index): 获取指定位置元素
    list.set(0, 100); // set(index, elem): 将index位置的元素设置为elem
    System.out.println(list);//[100, 2, 4, 5, 6, 1]

    // subList(from, to): 用list中[from, to)之间的元素构造一个新的LinkedList返回
    List<Integer> copy = list.subList(0, 3);
    System.out.println(list);//[100, 2, 4, 5, 6, 1]
    System.out.println(copy);//[100, 2, 4]
    list.clear(); // 将list中元素清空
    System.out.println(list.size());//0
}

3.LinkedList的遍历

(1)普通 for 循环

java 复制代码
// 这种方式性能最差!
for (int i = 0; i < linkedList.size(); i++) {
    System.out.print(linkedList.get(i) + " ");
}

LinkedList 的 get(int index) 方法需要从头或尾开始逐个遍历节点才能定位到目标位置

java 复制代码
// LinkedList 源码中的 get 方法
public E get(int index) {
    checkElementIndex(index);
    return node(index).item;  // node(index) 需要遍历链表
}

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;
    }
}

此时,get(0) 需要遍历 0 次(直接返回头节点)。get(1) 需要遍历 1 次。get(n) 最坏需要遍历 n/2 次。因此使用普通 for 循环遍历整个 LinkedList,总时间复杂度为 O(n²)

(2)增强 for 循环 / Iterator

java 复制代码
// 底层使用迭代器,性能为 O(n)
for (String s : linkedList) {
    System.out.print(s + " ");
}

增强 for 循环在编译后会被转换为 Iterator 遍历,等价于以下代码:

java 复制代码
Iterator<String> it = linkedList.iterator();
while (it.hasNext()) {
    String s = it.next();
    System.out.print(s + " ");
}

LinkedList 的迭代器内部维护了一个指向当前节点的指针,每次调用 next() 时只需将指针移动到下一个节点,时间复杂度为 O(1)。遍历整个链表的总时间复杂度为 O(n)

(3)ListIterator

ListIterator 支持双向遍历,安全修改、插入、删除和获取索引

java 复制代码
//  支持双向遍历 + 遍历中增删改
ListIterator<String> listIt = linkedList.listIterator();

// 正向遍历
while (listIt.hasNext()) {
    String s = listIt.next();
    if ("Java".equals(s)) {
        listIt.set("Go");      // 修改当前元素
    }
    if ("Python".equals(s)) {
        listIt.add("Kotlin");  // 在当前元素后插入新元素
    }
}

// 反向遍历(必须先移动到末尾)
while (listIt.hasPrevious()) {
    System.out.print(listIt.previous() + " ");
}

五、ArrayList 和 LinkedList 的区别

对比维度 ArrayList LinkedList
底层数据结构 动态数组(Object[]) 双向链表(Node 节点)
实现接口 List, RandomAccess, Cloneable, Serializable List, Deque, Cloneable, Serializable
随机访问 get(index) O(1) ✅ O(n) ❌
尾部添加 add(e) 均摊 O(1) O(1)
头部添加 add(0, e) O(n)(需搬移元素) O(1) ✅
中间插入 add(index, e) O(n) O(n)(定位慢)+ O(1)(指针修改)
删除操作 O(n)(需搬移元素) O(1)(定位到目标节点后)
内存占用 仅存储元素引用,内存紧凑 每个节点额外存储 prev 和 next 指针
遍历性能 普通 for 最优(O(n)) 普通 for 是 O(n²) 灾难,必须用迭代器
扩容/扩容开销 1.5 倍扩容,需复制数组(O(n)) 无需扩容,按需创建节点
线程安全
适用场景 读多写少,随机访问频繁 头尾增删频繁,用作队列/栈

六、面试问题

1.ArrayList源码中为什么需要两个不同的空数组?

  • EMPTY_ELEMENTDATA:通过 new ArrayList(0) 或 new ArrayList(emptyCollection) 显式创建容量为 0 的 ArrayList。之后按照 1.5 倍规则扩容。
  • DEFAULTCAPACITY_EMPTY_ELEMENTDATA:通过 new ArrayList() 无参构造器创建 ArrayList,直接扩容到默认容量 10。

2.ArrayList的缺陷

  • ArrayList 底层是一段连续空间,当在ArrayList任意位置插入或者删除元素时,就需要将后序元素整体往前或者往后搬移,时间复杂度为O(n),效率比较低,不适合做任意位置插入和删除比较多的场景。

3.arraylist和vector 区别

  • 两者底层都是基于动态数组实现的 List,但 Vector 是线程安全的(方法级 synchronized),而 ArrayList 是非线程安全的。
对比维度 ArrayList Vector
线程安全 非线程安全 线程安全(所有方法 synchronized 修饰)
性能 高(无锁开销) 低(读写均需竞争同一把锁)
扩容默认倍数 1.5 倍 2 倍(newCapacity = oldCapacity * 2,或按 capacityIncrement 增长)
扩容增量自定义 不支持 支持构造参数 capacityIncrement
初始容量 10(JDK 8+ 懒加载,首次 add 分配) 10(构造时立即分配)
遍历推荐方式 普通 for 最快(RandomAccess) 普通 for 或迭代器(但均有锁竞争)
相关推荐
慕容卡卡2 小时前
你所不知道的RAG那些事
java·开发语言·人工智能·spring boot·spring cloud
ch.ju2 小时前
Java程序设计(第3版)第二章——if if else else if
java
立莹Sir2 小时前
JVM深度解析与实战指南:从源码到生产环境优化
开发语言·jvm·python
SimonKing2 小时前
144K Star的开源神器,OpenCode进阶使用全攻略
java·后端·程序员
程序边界2 小时前
NFS环境下数据库安装报错解析(上篇):一个诡异的“权限门“事件
开发语言·数据库·php
froginwe112 小时前
Ruby 正则表达式
开发语言
程途知微2 小时前
Java线程池运行机制与拒绝策略底层全解析
java·后端
CPUOS20102 小时前
嵌入式C语言高级编程之单一职责原则
c语言·开发语言·单一职责原则
尘埃落定wf2 小时前
2026 年 LangChain (记忆)Memory 怎么用?三个核心类 + 完整代码示例
开发语言·前端·python