Java集合核心:ArrayList与LinkedList深度解析

ArrayList

概念

一、先理解核心概念

  1. 标记接口(Marker Interface) :Cloneable 是一个空接口(没有任何方法),它的作用仅仅是标记 一个类,表示该类允许被克隆。如果一个类实现了 Cloneable,但没有重写 clone () 方法,调用 clone () 会抛出 CloneNotSupportedException
  2. 浅拷贝(Shallow Copy) :拷贝对象时,只复制对象本身(包括对象的基本数据类型成员变量),但对象中的引用类型成员变量(比如 List、自定义类对象)仅复制引用地址,新对象和原对象的引用类型成员指向同一个内存地址

二、LinkedList 的 clone () 实现原理

LinkedList 重写了 clone () 方法,核心逻辑是:

  • 创建一个新的 LinkedList 实例;
  • 遍历原 LinkedList 的所有节点,将节点中的元素引用逐一复制到新 LinkedList 中;
  • 最终返回这个新的 LinkedList,但新链表中的元素(如果是引用类型)和原链表指向同一个对象。

三、代码示例:直观理解浅拷贝

下面通过代码演示 LinkedList.clone () 的浅拷贝效果:

java 复制代码
import java.util.LinkedList;

// 自定义引用类型(用于测试浅拷贝)
class User {
    private String name;
    private int age;

    public User(String name, int age) {
        this.name = name;
        this.age = age;
    }

    // getter/setter
    public String getName() { return name; }
    public void setName(String name) { this.name = name; }
    public int getAge() { return age; }
    public void setAge(int age) { this.age = age; }

    @Override
    public String toString() {
        return "User{name='" + name + "', age=" + age + "}";
    }
}

public class LinkedListCloneDemo {
    public static void main(String[] args) {
        // 1. 创建原 LinkedList 并添加引用类型元素
        LinkedList<User> originalList = new LinkedList<>();
        User user1 = new User("张三", 20);
        originalList.add(user1);

        // 2. 调用 clone() 进行浅拷贝
        LinkedList<User> clonedList = (LinkedList<User>) originalList.clone();

        // 3. 验证:新链表和原链表是不同的对象
        System.out.println("原链表地址:" + originalList);
        System.out.println("克隆链表地址:" + clonedList);
        System.out.println("是否为同一对象:" + (originalList == clonedList)); // false

        // 4. 验证:引用类型元素指向同一个对象(浅拷贝核心特征)
        User originalUser = originalList.get(0);
        User clonedUser = clonedList.get(0);
        System.out.println("原链表元素地址:" + originalUser);
        System.out.println("克隆链表元素地址:" + clonedUser);
        System.out.println("元素是否为同一对象:" + (originalUser == clonedUser)); // true

        // 5. 修改原元素,克隆链表的元素也会变(因为指向同一对象)
        originalUser.setName("李四");
        System.out.println("原链表元素:" + originalList.get(0)); // User{name='李四', age=20}
        System.out.println("克隆链表元素:" + clonedList.get(0)); // User{name='李四', age=20}
    }
}

四、关键细节解释

  1. clone () 的返回值 :LinkedList.clone () 返回的是 Object 类型,需要强制类型转换为 LinkedList

  2. 基本数据类型 vs 引用类型

    • 如果 LinkedList 中存的是 IntegerString(不可变类型)等,浅拷贝几乎和 "深拷贝" 效果一致(因为不可变类型无法修改内部值);
    • 如果存的是自定义类(如上面的 User)、HashMap 等可变引用类型,修改原对象会影响克隆链表的元素。
  3. 深拷贝的实现(补充) :如果需要真正的深拷贝(新链表的元素是全新对象),需要自己实现:

    java 复制代码
    // 手动实现 LinkedList 深拷贝示例
    public static LinkedList<User> deepCloneLinkedList(LinkedList<User> original) {
        LinkedList<User> deepClone = new LinkedList<>();
        for (User user : original) {
            // 为每个元素创建新对象(深拷贝核心)
            User newUser = new User(user.getName(), user.getAge());
            deepClone.add(newUser);
        }
        return deepClone;
    }

总结

  1. LinkedList 实现 Cloneable 接口的核心目的是允许通过 clone () 实现浅拷贝,生成新的 LinkedList 实例,但新实例和原实例共享引用类型元素;
  2. 浅拷贝的关键特征:新链表是独立对象,但链表中的引用类型元素仅复制地址,修改原元素会同步影响克隆链表;
  3. 若需深拷贝,需手动遍历链表并为每个引用类型元素创建新对象,而非直接使用 clone () 方法。

arraylist详细的讲解

一、ArrayList 核心定义与底层原理

ArrayList 是 Java 集合框架中 java.util 包下的动态数组实现 ,实现了 List 接口,弥补了 Java 普通数组长度固定的缺陷,核心特征如下:

1. 底层数据结构
  • 本质是可变长度的数组 ,底层维护一个 transient Object[] elementData 数组(transient 表示该数组不参与默认序列化);
  • 初始化时如果不指定容量,默认初始容量是 10(JDK 8 及以上),也可以通过构造器 new ArrayList(int initialCapacity) 指定初始容量。
2. 扩容机制(核心)

当添加元素导致数组容量不足时,ArrayList 会自动扩容,核心逻辑:

  1. 扩容触发条件:size + 1 > elementData.lengthsize 是当前元素个数);
  2. 扩容规则:新容量 = 原容量 × 1.5(JDK 8 中通过 oldCapacity + (oldCapacity >> 1) 实现,位运算更高效);
  3. 扩容过程:创建新的更大数组 → 复制原数组元素到新数组 → 替换原数组,这个过程是耗时操作,所以初始化时指定合适容量能提升性能。

二、ArrayList 关键特性

1. 访问效率高
  • 基于数组实现,支持随机访问 (通过索引 get(int index)),时间复杂度为 O(1)
  • 这是 ArrayList 对比 LinkedList 最核心的优势(LinkedList 随机访问是 O(n))。
2. 增删效率(分场景)
  • 尾部增删(add (E e)/remove (size-1)) :效率高,时间复杂度 O(1)(无扩容时);
  • 中间 / 头部增删(add (int index, E e)/remove (int index)) :需要移动后续元素,时间复杂度 O(n),效率低;
  • 原因:数组元素在内存中是连续存储的,中间插入 / 删除会导致后续元素整体移位。
3. 线程不安全
  • ArrayList 是非线程安全 的集合,多线程环境下同时读写会导致数据错乱(如 ConcurrentModificationException);
  • 解决方案:
    • 使用 Collections.synchronizedList(new ArrayList<>()) 包装成线程安全版本;
    • CopyOnWriteArrayList(并发包下,写操作时复制数组,读写分离,适合读多写少场景)。
4. 空值与重复元素
  • 允许存储 null 值(可以通过 add(null) 添加);
  • 允许存储重复元素(如 add("a"); add("a") 是合法的)。
5. Cloneable 与浅拷贝
  • ArrayList 实现了 Cloneable 接口,调用 clone() 方法会返回一个新的 ArrayList 实例,但同样是浅拷贝
    • 新 ArrayList 的数组是新对象,但数组中的元素(引用类型)仅复制引用地址;
    • 示例:如果 ArrayList 中存自定义类对象,修改原对象会影响克隆后的 ArrayList 元素。

三、ArrayList 常用操作示例

下面通过代码演示 ArrayList 的核心操作,帮助你理解实际使用方式:

java 复制代码
import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;

public class ArrayListDemo {
    public static void main(String[] args) {
        // 1. 创建 ArrayList(指定初始容量,避免频繁扩容)
        List<String> list = new ArrayList<>(10);

        // 2. 添加元素
        list.add("Java");       // 尾部添加
        list.add("Python");
        list.add(1, "C++");     // 索引1处插入(中间插入,会移动元素)
        list.add(null);         // 允许null

        // 3. 访问元素
        String first = list.get(0); // 随机访问,O(1)
        System.out.println("索引0的元素:" + first); // Java

        // 4. 修改元素
        list.set(3, "Go");      // 将索引3的null改为Go
        System.out.println("修改后列表:" + list); // [Java, C++, Python, Go]

        // 5. 删除元素
        list.remove(1);         // 删除索引1的元素(C++),后续元素移位
        list.remove("Go");      // 删除指定元素(值匹配)
        System.out.println("删除后列表:" + list); // [Java, Python]

        // 6. 遍历元素(三种常用方式)
        // 方式1:for循环(随机访问,适合ArrayList)
        System.out.println("for循环遍历:");
        for (int i = 0; i < list.size(); i++) {
            System.out.println(list.get(i));
        }

        // 方式2:增强for循环
        System.out.println("增强for循环遍历:");
        for (String s : list) {
            System.out.println(s);
        }

        // 方式3:迭代器(支持遍历中删除,避免ConcurrentModificationException)
        System.out.println("迭代器遍历并删除:");
        Iterator<String> iterator = list.iterator();
        while (iterator.hasNext()) {
            String s = iterator.next();
            if (s.equals("Python")) {
                iterator.remove(); // 迭代器删除,安全
            }
        }
        System.out.println("迭代器删除后:" + list); // [Java]

        // 7. 克隆(浅拷贝)
        ArrayList<String> cloneList = (ArrayList<String>) ((ArrayList<String>) list).clone();
        System.out.println("原列表地址:" + list);
        System.out.println("克隆列表地址:" + cloneList);
        System.out.println("是否同一对象:" + (list == cloneList)); // false
    }
}

四、ArrayList vs LinkedList(核心区别)

为了帮你更清晰理解 ArrayList 的定位,对比 LinkedList 的关键差异:

特性 ArrayList LinkedList
底层结构 动态数组 双向链表
随机访问(get (index)) O (1)(高效) O (n)(需遍历)
增删(中间 / 头部) O (n)(需移位) O (1)(只需修改节点引用)
增删(尾部) O (1)(无扩容时) O(1)
内存占用 连续内存,有扩容冗余 每个节点存数据 + 前后引用,内存开销大
适用场景 读多写少、随机访问 写多读少、频繁增删

五、ArrayList 性能优化建议

  1. 初始化指定容量 :如果知道元素个数,创建时指定 new ArrayList(预计大小),避免扩容的数组复制开销;
  2. 批量添加用 addAll () :相比多次调用 add()addAll() 减少扩容检查次数;
  3. 遍历优先用 for 循环 :ArrayList 随机访问高效,for 循环(get(index))比迭代器 / 增强 for 更高效;
  4. 避免频繁中间增删:如果需要频繁在中间增删,优先用 LinkedList。

实现的接口

1. 核心接口:List<E>(最基础)
  • 作用 :这是 ArrayList 最核心的接口,继承自 CollectionIterable
  • 带来的能力 :定义了有序集合的核心方法,比如 add()get(int index)remove(int index)size() 等,保证 ArrayList 具备 "通过索引访问元素、可重复、有序" 的基本特性。
2. 标记接口:RandomAccess
  • 作用:这是一个 "空接口"(仅作标记),没有任何方法。
  • 带来的能力 :标记该集合支持快速随机访问 (通过索引 get(i) 访问元素的时间复杂度为 O (1))。
    • 比如遍历 ArrayList 时,用普通 for 循环(for (int i=0; i<size; i++))比迭代器遍历效率更高;而没有实现该接口的 LinkedList 则相反。
3. 标记接口:Cloneable
  • 作用 :标记该类支持调用 clone() 方法进行浅拷贝。
  • 带来的能力 :可以通过 arrayList.clone() 创建一个新的 ArrayList(注意:元素是引用类型时,仅拷贝引用,不是深拷贝)。
4. 标记接口:java.io.Serializable
  • 作用:标记该类的对象可以被序列化(转换为字节流)和反序列化(从字节流恢复对象)。
  • 带来的能力ArrayList 对象可以通过网络传输、写入文件,或在分布式场景中使用(比如序列化到 Redis)。

补充:ArrayList 没有实现的关键接口

  • 没有实现 Deque 接口:因此没有 addFirst()addLast()pollFirst() 等双端队列方法(这也是和 LinkedList 的核心区别之一)。
  • 没有实现 Set 接口:因此允许元素重复,而 HashSet 等实现 Set 接口的集合不允许重复。

总结

  1. ArrayList 最核心的接口是 List<E>,决定了它 "有序、可索引访问、可重复" 的核心特性。
  2. RandomAccess 标记它支持快速随机访问,Cloneable 支持浅拷贝,Serializable 支持序列化。
  3. ArrayList 未实现 Deque 接口,因此没有 addFirst()/addLast() 等双端队列方法,需手动封装才能使用这些功能。

常用的方法:

类别 核心方法
新增 add()addAll()add(int index, E e)
删除 remove()removeAll()clear()removeIf()
修改 set(int index, E e)
查询 get()indexOf()lastIndexOf()contains()isEmpty()size()
遍历 iterator()listIterator()forEach()
其他 clone()toArray()sort()subList()ensureCapacity()

先初始化一个测试用的 ArrayList,后续所有方法都基于这个列表演示:

java 复制代码
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Iterator;
import java.util.List;

public class ArrayListMethodsDemo {
    public static void main(String[] args) {
        // 初始化 ArrayList
        ArrayList<String> list = new ArrayList<>();
        list.add("Java");
        list.add("Python");
        list.add("C++");
        System.out.println("初始列表:" + list); // [Java, Python, C++]
1. 新增方法(增)
方法 作用 时间复杂度 注意事项
add(E e) 尾部添加元素 O (1)(无扩容) 扩容时为 O (n)(数组复制)
add(int index, E e) 指定索引插入元素 O(n) 索引需在 0~size 之间,会触发后续元素移位
addAll(Collection<? extends E> c) 尾部批量添加集合元素 O (m)(m 为新增元素数) 扩容时额外加 O (n) 开销
addAll(int index, Collection<? extends E> c) 指定索引批量添加 O(n+m) 索引合法,且会触发元素移位
java 复制代码
        // 1.1 尾部添加
        list.add("Go");
        System.out.println("add(Go) 后:" + list); // [Java, Python, C++, Go]

        // 1.2 指定索引插入
        list.add(1, "JavaScript");
        System.out.println("add(1, JavaScript) 后:" + list); // [Java, JavaScript, Python, C++, Go]

        // 1.3 批量添加
        List<String> newList = Arrays.asList("PHP", "Ruby");
        list.addAll(newList);
        System.out.println("addAll(newList) 后:" + list); // [Java, JavaScript, Python, C++, Go, PHP, Ruby]

        // 1.4 指定索引批量添加
        list.addAll(0, Arrays.asList("C#", "Swift"));
        System.out.println("addAll(0, ...) 后:" + list); // [C#, Swift, Java, JavaScript, Python, C++, Go, PHP, Ruby]
2. 删除方法(删)
方法 作用 时间复杂度 注意事项
remove(int index) 删除指定索引元素 O(n) 索引需在 0~size-1 之间,后续元素移位
remove(Object o) 删除第一个匹配的元素 O(n) 元素为 null 时也能匹配(删除第一个 null)
removeAll(Collection<?> c) 删除所有与集合 c 交集的元素 O(n*m) 批量删除,需遍历比对
removeIf(Predicate<? super E> filter) 按条件删除元素 O(n) JDK 8+ 新增,遍历一次即可,效率高
clear() 清空所有元素 O(1) 仅重置 size 为 0,数组本身不销毁
java 复制代码
        // 2.1 删除指定索引
        list.remove(0); // 删除索引0的C#
        System.out.println("remove(0) 后:" + list); // [Swift, Java, JavaScript, Python, C++, Go, PHP, Ruby]

        // 2.2 删除指定元素
        list.remove("Python"); // 删除第一个Python
        System.out.println("remove(Python) 后:" + list); // [Swift, Java, JavaScript, C++, Go, PHP, Ruby]

        // 2.3 批量删除
        List<String> delList = Arrays.asList("PHP", "Ruby");
        list.removeAll(delList);
        System.out.println("removeAll(delList) 后:" + list); // [Swift, Java, JavaScript, C++, Go]

        // 2.4 按条件删除(删除包含"Java"的元素)
        list.removeIf(s -> s.contains("Java"));
        System.out.println("removeIf(包含Java) 后:" + list); // [Swift, C++, Go]

        // 2.5 清空列表
        // list.clear();
        // System.out.println("clear() 后:" + list); // []
3. 修改方法(改)
方法 作用 时间复杂度 注意事项
set(int index, E e) 修改指定索引的元素值 O(1) 索引需在 0~size-1 之间,直接替换值
java 复制代码
        // 3.1 修改指定索引元素
        list.set(1, "C"); // 把索引1的C++改为C
        System.out.println("set(1, C) 后:" + list); // [Swift, C, Go]
4. 查询方法(查)
方法 作用 时间复杂度 注意事项
get(int index) 获取指定索引元素 O(1) 索引需合法,ArrayList 核心优势方法
contains(Object o) 判断是否包含指定元素 O(n) 底层调用 indexOf (o) != -1
indexOf(Object o) 返回第一个匹配元素的索引,无则返回 - 1 O(n) 支持 null 元素(返回第一个 null 的索引)
lastIndexOf(Object o) 返回最后一个匹配元素的索引,无则返回 - 1 O(n) 从尾部开始遍历
isEmpty() 判断列表是否为空 O(1) 仅判断 size == 0
size() 获取元素个数 O(1) 直接返回 size 变量
java 复制代码
        // 4.1 获取指定索引元素
        String elem = list.get(2);
        System.out.println("get(2):" + elem); // Go

        // 4.2 判断是否包含元素
        boolean hasC = list.contains("C");
        System.out.println("是否包含C:" + hasC); // true

        // 4.3 查找元素首次出现的索引
        int index = list.indexOf("Swift");
        System.out.println("indexOf(Swift):" + index); // 0

        // 4.4 查找元素最后出现的索引
        list.add("C");
        int lastIndex = list.lastIndexOf("C");
        System.out.println("lastIndexOf(C):" + lastIndex); // 3

        // 4.5 判断是否为空
        boolean empty = list.isEmpty();
        System.out.println("是否为空:" + empty); // false

        // 4.6 获取元素个数
        int size = list.size();
        System.out.println("列表大小:" + size); // 4
5. 遍历方法(遍历)
方法 作用 效率 注意事项
普通 for 循环(get (index)) 按索引遍历 最高 ArrayList 专属高效遍历方式
增强 for 循环 迭代遍历 中等 底层是迭代器,简洁但略慢于普通 for
iterator() 获取迭代器 中等 支持遍历中安全删除(iterator.remove ())
listIterator() 获取列表迭代器 中等 支持正向 / 反向遍历、添加 / 修改元素
forEach() 函数式遍历(JDK 8+) 中等 简洁,支持 Lambda 表达式
java 复制代码
        // 5.1 普通for循环(推荐ArrayList使用)
        System.out.println("普通for循环遍历:");
        for (int i = 0; i < list.size(); i++) {
            System.out.print(list.get(i) + " "); // Swift C Go C
        }
        System.out.println();

        // 5.2 增强for循环
        System.out.println("增强for循环遍历:");
        for (String s : list) {
            System.out.print(s + " "); // Swift C Go C
        }
        System.out.println();

        // 5.3 迭代器遍历(支持安全删除)
        System.out.println("迭代器遍历并删除:");
        Iterator<String> iterator = list.iterator();
        while (iterator.hasNext()) {
            String s = iterator.next();
            if (s.equals("C")) {
                iterator.remove(); // 安全删除,不会抛ConcurrentModificationException
            }
        }
        System.out.println("迭代器删除后:" + list); // [Swift, Go]

        // 5.4 forEach函数式遍历
        System.out.println("forEach遍历:");
        list.forEach(s -> System.out.print(s + " ")); // Swift Go
        System.out.println();
6. 其他常用方法
方法 作用 关键说明
clone() 浅拷贝,返回新 ArrayList 需强制类型转换,引用类型元素仅拷贝地址
toArray() 转为 Object 数组 无泛型,需手动强转
toArray(T[] a) 转为指定类型数组 推荐使用,避免强转
sort(Comparator<? super E> c) 自定义排序 JDK 8+ 支持 Lambda 表达式
subList(int fromIndex, int toIndex) 截取子列表 子列表是原列表的视图,修改会同步影响原列表
ensureCapacity(int minCapacity) 手动指定最小容量 提前扩容,避免添加元素时频繁扩容
java 复制代码
        // 6.1 克隆(浅拷贝)
        ArrayList<String> cloneList = (ArrayList<String>) list.clone();
        System.out.println("克隆列表:" + cloneList); // [Swift, Go]
        System.out.println("原列表与克隆列表是否同一对象:" + (list == cloneList)); // false

        // 6.2 转为数组
        Object[] objArray = list.toArray();
        String[] strArray = list.toArray(new String[0]);
        System.out.println("指定类型数组:" + Arrays.toString(strArray)); // [Swift, Go]

        // 6.3 排序
        list.add("Apple");
        list.sort((s1, s2) -> s1.compareTo(s2)); // 按字符串自然排序
        System.out.println("排序后:" + list); // [Apple, Go, Swift]

        // 6.4 截取子列表(索引0到2,左闭右开)
        List<String> subList = list.subList(0, 2);
        System.out.println("子列表:" + subList); // [Apple, Go]
        subList.add("Banana"); // 修改子列表会影响原列表
        System.out.println("原列表(子列表修改后):" + list); // [Apple, Go, Banana, Swift]

        // 6.5 手动扩容
        list.ensureCapacity(10); // 提前扩容到容量10,避免后续添加时扩容
    }
}

Java 中 "增强 for 循环(foreach 语法)"List.forEach()方法 的区别

  • List接口(Java 8+)提供的方法 ,底层依赖Iterable接口的forEach(),本质是调用函数式接口(比如Consumer)来处理元素。

2. 使用场景与限制

  • 增强 for 循环
    • 可以用break/continue控制循环(因为是语句);
    • 遍历过程中不能修改集合结构 (比如add/remove,否则会抛ConcurrentModificationException)。
  • list.forEach()
    • 是方法调用,不能用break/continue (除非用return跳出当前元素的处理,但无法终止整个循环);
    • 遍历逻辑由集合自身实现(比如 ArrayList 是普通遍历,CopyOnWriteArrayList 是安全遍历),部分集合(如并发集合)支持遍历中修改结构。

3. 语法风格

  • 增强 for 循环是命令式风格:自己写循环体逻辑;
  • forEach()函数式风格 :通常配合 Lambda 表达式(比如list.forEach(s -> System.out.println(s)))。
对比维度 增强 for 循环(for (T t : list) List.forEach() 方法
本质 Java 语法糖,编译后转为 Iterator 遍历,属于循环语句 Java 8+ 新增的集合方法,依赖函数式接口(Consumer),属于方法调用
语法风格 命令式编程(手动写循环体逻辑) 函数式编程(配合 Lambda / 方法引用,更简洁)
循环控制 支持 break(终止循环)、continue(跳过当前) 不支持 break/continue;仅能用 return 跳过当前元素,无法终止整个循环
集合修改限制 遍历中修改集合结构(add/remove)会抛 ConcurrentModificationException 取决于集合实现:- 普通集合(ArrayList):仍会抛异常- 并发集合(CopyOnWriteArrayList):支持安全修改
异常处理 循环体内的异常可直接用 try-catch 包裹整个循环 Lambda 内的异常需单独捕获(或声明未检查异常)

linkedList

LinkedList 核心定义与底层原理

LinkedList 是 Java 集合框架中 java.util 包下的双向链表实现,实现了 ListDeque 等接口,弥补了 ArrayList 中间增删效率低的缺陷,核心特征如下:

  1. 底层数据结构本质是双向链表 (JDK 1.6 及之前为循环链表,之后改为双向链表),底层没有数组,而是由一个个 Node 节点串联而成:
    • 每个 Node 节点包含 3 部分:元素值(item)、前驱节点引用(prev)、后继节点引用(next);
    • 链表维护 first(头节点)和 last(尾节点)两个引用,头节点 prev 为 null,尾节点 next 为 null;
    • 元素在内存中非连续存储,通过节点间的引用关联。
  2. 无扩容机制(核心区别于 ArrayList)LinkedList 没有固定容量的概念,也无需扩容:
    • 新增元素时,只需创建新的 Node 节点,修改相邻节点的引用即可,无需复制数据;
    • 不存在 "扩容冗余内存",元素个数与节点个数完全一致。

LinkedList 关键特性

  1. 访问效率(随机访问低)基于双向链表实现,不支持快速随机访问(通过索引 get(int index)):
    • 访问指定索引元素时,会先判断索引靠近头 / 尾(优化遍历),再从近的一端开始遍历,时间复杂度 O(n)
    • 这是 LinkedList 对比 ArrayList 最核心的劣势(ArrayList 随机访问 O(1))。
  2. 增删效率(分场景)
    • 首尾增删(addFirst()/addLast()/removeFirst()/removeLast()):仅修改头 / 尾节点引用,时间复杂度 O(1),效率极高;
    • 中间增删(add(int index, E e)/remove(int index)):需先遍历到指定索引位置(O(n)),但找到位置后仅修改节点引用(O(1)),整体时间复杂度 O(n)(对比 ArrayList 中间增删的 "遍历 + 移位",实际效率更高);
    • 原因:链表元素非连续存储,增删无需移动其他元素,仅修改节点引用关系。
  3. 线程不安全LinkedList 是非线程安全的集合,多线程环境下同时读写会导致数据错乱(如 ConcurrentModificationException);解决方案:
    • 使用 Collections.synchronizedList(new LinkedList<>()) 包装成线程安全版本;
    • 并发场景优先用 ConcurrentLinkedDeque(并发包下,无锁设计,适合高并发)。
  4. 空值与重复元素
    • 允许存储 null 值(可以通过 add(null) 添加);
    • 允许存储重复元素(如 add("a"); add("a") 合法)。
  5. Cloneable 与浅拷贝LinkedList 实现了 Cloneable 接口,调用 clone() 方法会返回新的 LinkedList 实例,但为浅拷贝
    • 新 LinkedList 的节点是全新对象,但节点中的元素(引用类型)仅复制引用地址;
    • 示例:如果 LinkedList 中存自定义类对象,修改原对象会影响克隆后的 LinkedList 元素。

LinkedList 常用操作示例

下面通过代码演示 LinkedList 的核心操作,覆盖基础操作、队列 / 栈特性、克隆等关键场景:

java 复制代码
import java.util.Iterator;
import java.util.LinkedList;

public class LinkedListDemo {
    public static void main(String[] args) {
        // 1. 创建 LinkedList(无需指定容量,无扩容概念)
        LinkedList<String> list = new LinkedList<>();

        // 2. 添加元素(核心:首尾/中间添加)
        list.add("Java");          // 尾部添加(等价于 addLast())
        list.addFirst("Python");   // 头部添加(O(1))
        list.addLast("C++");       // 尾部添加(O(1))
        list.add(2, "Go");         // 索引2处插入(先遍历找位置,再修改引用)
        System.out.println("添加元素后:" + list); // [Python, Java, Go, C++]

        // 3. 访问元素
        String first = list.getFirst(); // 获取头节点(O(1))
        String last = list.getLast();   // 获取尾节点(O(1))
        String index2 = list.get(2);    // 获取索引2元素(遍历,O(n))
        System.out.println("头:" + first + ",尾:" + last + ",索引2:" + index2);

        // 4. 修改元素
        list.set(1, "JavaScript"); // 修改索引1元素(先遍历找位置,再替换值)
        System.out.println("修改后:" + list); // [Python, JavaScript, Go, C++]

        // 5. 删除元素
        list.removeFirst(); // 删除头节点(O(1))
        list.removeLast();  // 删除尾节点(O(1))
        list.remove(0);     // 删除索引0元素(遍历找位置,再修改引用)
        System.out.println("删除后:" + list); // [Go]

        // 6. 遍历元素(三种方式,优先迭代器/增强for)
        // 方式1:普通for循环(效率低,每次get(index)都要遍历)
        System.out.println("普通for循环遍历:");
        for (int i = 0; i < list.size(); i++) {
            System.out.println(list.get(i));
        }

        // 方式2:增强for循环(推荐,底层是迭代器,无需重复遍历)
        System.out.println("增强for循环遍历:");
        for (String s : list) {
            System.out.println(s);
        }

        // 方式3:迭代器(支持遍历中删除,安全)
        System.out.println("迭代器遍历并删除:");
        Iterator<String> iterator = list.iterator();
        while (iterator.hasNext()) {
            String s = iterator.next();
            if (s.equals("Go")) {
                iterator.remove(); // 迭代器删除,避免并发修改异常
            }
        }
        System.out.println("迭代器删除后:" + list); // []

        // 7. 队列/栈/双端队列特性(实现Deque接口,核心优势)
        LinkedList<String> deque = new LinkedList<>();
        // 队列操作(FIFO)
        deque.offer("A"); // 尾部添加
        deque.poll();     // 头部删除(队列为空返回null)
        // 栈操作(LIFO)
        deque.push("B");  // 头部添加(等价于 addFirst())
        deque.pop();      // 头部删除(等价于 removeFirst(),栈为空抛异常)
        // 双端队列操作
        deque.offerFirst("C"); // 头部添加
        deque.offerLast("D");  // 尾部添加
        System.out.println("双端队列操作后:" + deque); // [C, D]

        // 8. 克隆(浅拷贝)
        LinkedList<String> cloneList = (LinkedList<String>) deque.clone();
        System.out.println("原队列地址:" + deque);
        System.out.println("克隆队列地址:" + cloneList);
        System.out.println("是否同一对象:" + (deque == cloneList)); // false
    }
}

LinkedList 实现的接口

核心接口 1:

List<E>作用:继承自 CollectionIterable,是 LinkedList 作为 "列表" 的基础。带来的能力:定义了有序集合的核心方法(add()/get(int index)/remove(int index)/size() 等),保证 LinkedList 具备 "有序、可重复、可通过索引访问" 的基本特性。

核心接口 2:

Deque<E>(双向队列)作用:继承自 Queue,是 LinkedList 区别于 ArrayList 的核心接口。带来的能力:赋予 LinkedList 队列(FIFO)、栈(LIFO)、双端队列的特性,提供 addFirst()/addLast()/offer()/push()/pop()/poll() 等方法。

标记接口:

Cloneable作用:标记该类支持调用 clone() 方法进行浅拷贝。带来的能力:可以通过 linkedList.clone() 创建新的 LinkedList 实例(元素为引用类型时仅拷贝引用)。

标记接口

java.io.Serializable作用:标记该类的对象可以被序列化 / 反序列化。带来的能力:LinkedList 对象可网络传输、写入文件(如序列化到 Redis)。

补充:LinkedList 没有实现的关键接口

  • 没有实现 RandomAccess 接口:标记该集合不支持快速随机访问,遍历优先用迭代器而非普通 for 循环;
  • 没有实现 Set 接口:允许元素重复,区别于 HashSet 等不重复集合。

LinkedList 性能优化建议

  1. 优先用首尾操作:充分利用 addFirst()/addLast()/removeFirst()/removeLast() 等 O (1) 方法,避免中间增删;
  2. 遍历选迭代器 / 增强 for:避免普通 for 循环(每次 get(index) 都会重新遍历链表);
  3. 批量添加用 addAll():减少节点创建和引用修改的次数,提升效率;
  4. 避免频繁查询:如果需要频繁随机访问,优先替换为 ArrayList;
  5. 并发场景用 ConcurrentLinkedDeque:替代 Collections.synchronizedList,高并发下性能更好。

LinkedList vs ArrayList(核心区别)

对比维度 ArrayList LinkedList
底层实现 动态数组(基于数组) 双向链表(每个节点存数据 + 前后指针)
随机访问(get/set) 效率高(O (1)),实现 RandomAccess 接口 效率低(O (n)),未实现 RandomAccess
增删操作(非头尾) 效率低(O (n)),需移动后续元素 效率高(O (1)),仅需修改指针
增删操作(头尾) 头部增删 O (n)(移动元素),尾部增删 O (1) 头尾增删 O (1)(直接操作指针)
内存占用 连续内存,有扩容冗余(默认扩容 1.5 倍) 非连续内存,每个节点额外存前后指针(内存开销更大)
实现的接口 List、RandomAccess、Cloneable、Serializable List、Deque、Cloneable、Serializable
功能支持 仅 List 基础功能(索引访问、增删) 兼具 List + Deque 功能(队列 / 栈 / 双端操作)
遍历效率 普通 for 循环(索引)效率更高 迭代器(Iterator)效率更高
空元素 / 重复元素 支持 支持
初始容量 有默认初始容量(10),可指定 无初始容量概念(链表按需创建节点)

关键区别展开解释

1. 底层实现(最核心区别)
  • ArrayList :基于动态数组实现,数组是连续的内存空间,JVM 会为它分配一块连续的内存,访问时通过索引直接定位,速度极快。但数组长度固定,满了之后会自动扩容(默认新容量 = 原容量 * 1.5),扩容时需要复制数组,有额外开销。
  • LinkedList :基于双向链表 实现,每个节点(Node)包含 prev(前驱指针)、next(后继指针)、item(数据)。链表节点分散在内存中,无需连续空间,也没有扩容概念,新增节点时只需创建新 Node 并修改指针即可。
2. 性能差异(开发中最常用的判断依据)

性能差异的核心是「数组的连续内存」vs「链表的离散节点」:

  • 随机访问(get (index)) :ArrayList 直接通过 数组首地址 + 索引 * 元素大小 定位元素,时间复杂度 O (1);LinkedList 必须从头 / 尾节点开始遍历,直到找到目标索引,时间复杂度 O (n)。
  • 增删操作
    • 非头尾位置:ArrayList 增删后需要移动后续所有元素(比如在索引 2 插入元素,索引 2 及之后的元素都要后移),O (n);LinkedList 只需找到目标节点,修改前后指针,O (1)(找节点的过程是 O (n),但移动 / 修改指针是 O (1))。
    • 头尾位置:ArrayList 尾部增删 O (1)(无扩容时),头部增删 O (n)(需移动所有元素);LinkedList 头尾增删都是 O (1)(直接操作头尾指针)。
3. 功能与接口差异
  • ArrayList 仅实现 List 接口,只有 List 的基础功能(索引访问、增删改查);
  • LinkedList 同时实现 ListDeque 接口,因此兼具:
    • List 功能:按索引访问、增删;
    • Deque 功能:队列(offer/poll)、栈(push/pop)、双端操作(addFirst/addLast)。
4. 内存占用差异
  • ArrayList:内存连续,扩容时会预留冗余空间(比如当前容量 10,扩容后 15,多出来的 5 个位置暂时空置),但每个元素仅存数据本身;
  • LinkedList:每个节点除了存数据,还要存前后指针(额外的内存开销),且节点分散在内存中,可能产生内存碎片。
相关推荐
篱笆院的狗2 小时前
Group by很慢,如何定位?如何优化?
java·数据库
@淡 定2 小时前
DDD领域事件详解:抽奖系统实战
开发语言·javascript·网络
lly2024062 小时前
DOM 简介
开发语言
期待のcode2 小时前
Java的反射
java·开发语言
j .2 小时前
Java 集合的核心概念笔记
开发语言·python
2201_757830872 小时前
AOP入门程序
java·开发语言
雨中飘荡的记忆2 小时前
MyBatis反射模块详解
java·mybatis
宸津-代码粉碎机2 小时前
Spring 6.0+Boot 3.0实战避坑全指南:5大类高频问题与解决方案(附代码示例)
java·数据仓库·hive·hadoop·python·技术文档编写
笃行客从不躺平2 小时前
ThreadLocal 复习一
java·开发语言