Java集合核心进阶:Collection与List底层全解析

3 阅读25分钟

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 核心注意事项(面试避坑)

  1. Collection接口中所有方法均为抽象方法,需由实现类(如ArrayList)重写实现,不能直接new Collection()实例化;

  2. 集合中存储的是对象的引用,而非对象本身(基本数据类型会自动装箱为包装类,如int→Integer);

  3. contains()、remove()方法的底层依赖元素的equals()方法,若自定义对象未重写equals(),会使用Object类的equals()(比较地址值),导致判断结果异常;

  4. 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. 核心避坑点(面试高频)
  1. 迭代器遍历期间,不能直接调用集合的add()、remove()方法,否则会抛出ConcurrentModificationException(并发修改异常),需使用迭代器自身的remove()方法;

  2. remove()方法必须在next()之后调用,否则会抛出IllegalStateException(非法状态异常)(因为没有指向任何元素);

  3. 迭代器是“一次性”的,遍历结束后(hasNext()返回false),无法重新遍历,需重新获取迭代器(coll.iterator());

  4. 迭代器只能遍历元素,不能添加元素(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. 核心避坑点
  1. 增强for循环不能修改集合的结构(添加、删除元素),否则会抛出ConcurrentModificationException;

  2. 增强for循环遍历集合时,变量是集合元素的“副本”,修改变量的值不会影响集合中原本的元素(如String类型,修改变量不会改变集合中的字符串);

  3. 增强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)核心分支:

  1. List集合:有序、可重复、有索引(如ArrayList、LinkedList、Vector);

  2. Set集合:无序、不可重复、无索引(如HashSet、LinkedHashSet、TreeSet);

  3. 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核心面试考点总结

  1. 底层数据结构:JDK 8+ 基于Object类型动态数组实现,transient修饰数组避免默认序列化,自定义序列化逻辑节省空间;

  2. 初始容量与扩容机制:空参构造初始容量为0,第一次add元素扩容为10;后续扩容为原容量的1.5倍(位运算实现,效率更高);指定初始容量可避免频繁扩容;

  3. 增删慢、查询快的原因:查询(get())直接通过数组索引访问,时间复杂度O(1);增删(add(index)、remove())需要移动数组元素,时间复杂度O(n),元素越多效率越低;

  4. 并发修改异常:modCount记录集合修改次数,迭代器遍历期间,若集合结构(add/remove)发生变化,modCount与迭代器的expectedModCount不相等,抛出ConcurrentModificationException;

  5. equals()方法的影响:contains()、remove(Object o)、indexOf()等方法依赖元素的equals(),自定义对象未重写equals()会导致判断异常;

  6. 内存泄漏:remove()方法会主动将最后一个元素置为null,帮助GC回收,避免内存泄漏;

  7. 与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核心面试考点总结

  1. 底层数据结构:基于双向链表实现,每个节点包含prev、item、next三个部分,无需连续内存空间;

  2. 增删快、查询慢的原因:增删(add、remove)仅需修改节点的prev和next引用,时间复杂度O(1)(查找节点除外);查询(get)需遍历链表查找节点,时间复杂度O(n),虽有首尾遍历优化,但效率仍低于ArrayList;

  3. 与ArrayList的核心区别(面试高频对比): 底层结构:ArrayList是动态数组,LinkedList是双向链表;

  4. 效率:ArrayList查询O(1)、增删O(n);LinkedList增删O(1)(查找节点除外)、查询O(n);

  5. 内存占用:ArrayList需预留数组扩容空间,内存利用率较低;LinkedList每个节点需存储前后引用,内存占用略高;

  6. 适用场景:ArrayList适用于查询频繁、增删少;LinkedList适用于增删频繁、查询少,或作为队列/栈使用。

  7. 队列/栈功能:实现Deque接口,可调用offer()、poll()(队列)、push()、pop()(栈)等方法,功能更灵活;

  8. 并发安全性:与ArrayList一致,线程不安全,修改时需加锁或使用Collections.synchronizedList();

  9. 节点查找优化:node()方法会根据索引位置,选择从头或从尾遍历,减少遍历次数,提升查询效率(面试常考)。

2.4 List集合面试高频总结

List集合是Java面试中集合模块的核心考点,重点掌握以下内容,轻松应对面试提问:

  1. List接口核心特征:有序、可重复、有索引,继承Collection接口,新增索引相关方法;

  2. ArrayList与LinkedList对比:底层结构、效率、内存占用、适用场景(重中之重);

  3. ArrayList扩容机制:初始容量、扩容比例、扩容触发条件、超大容量处理;

  4. LinkedList节点结构:双向链表节点的组成、增删节点的核心逻辑;

  5. 并发修改异常:产生原因(modCount与expectedModCount不相等)、避免方式;

  6. equals()方法的影响:contains()、remove(Object o)等方法的底层依赖,自定义对象重写equals()的必要性;

  7. List遍历方式:三种遍历方式的适用场景、优缺点,尤其是迭代器的使用注意事项。