Skip to content

Java集合框架:ArrayList、LinkedList与Set集合完全指南

目录


一、核心知识点

1. ArrayList底层源码分析

1.1 构造方法

无参构造

java
ArrayList<String> list = new ArrayList<>();
  • 构造一个初始容量为10的空列表
  • 重要:不是一new就创建长度为10的数组,而是第一次add时才创建
  • 如果超出容量,自动扩容1.5倍

有参构造

java
ArrayList<String> list = new ArrayList<>(15);
  • 直接指定数组长度,避免频繁扩容

1.2 源码解析

无参构造创建对象

java
public ArrayList() {
    this.elementData = DEFAULTCAPACITY_EMPTY_ELEMENTDATA;
}

第一次add时创建长度为10的数组

java
public boolean add(E e) {
    modCount++;
    add(e, elementData, size);
    return true;
}

private Object[] grow(int minCapacity) {
    int oldCapacity = elementData.length;
    if (oldCapacity > 0 || elementData != DEFAULTCAPACITY_EMPTY_ELEMENTDATA) {
        int newCapacity = ArraysSupport.newLength(oldCapacity,
                minCapacity - oldCapacity,
                oldCapacity >> 1);
        return elementData = Arrays.copyOf(elementData, newCapacity);
    } else {
        return elementData = new Object[Math.max(DEFAULT_CAPACITY, minCapacity)];
    }
}

1.3 重要注意事项

删除元素时的陷阱

java
ArrayList<Integer> list = new ArrayList<>();
list.add(2);

// 错误:会调用根据索引删除的remove方法
// list.remove(2);

// 正确:需要装箱
list.remove(Integer.valueOf(2));

ArrayList使用场景


2. LinkedList集合

2.1 基本特点

特性说明
元素有序
有索引
元素可重复
线程安全
数据结构双向链表

2.2 特有方法

LinkedList提供了大量直接操作首尾元素的方法:

java
// 添加元素
public void addFirst(E e)    // 插入到开头
public void addLast(E e)     // 添加到结尾

// 获取元素
public E getFirst()          // 获取第一个元素
public E getLast()           // 获取最后一个元素

// 删除元素
public E removeFirst()       // 移除并返回第一个元素
public E removeLast()        // 移除并返回最后一个元素

// 栈操作
public E pop()               // 弹出栈顶元素
public void push(E e)        // 推入栈顶

2.3 使用示例

java
LinkedList<String> list = new LinkedList<>();
list.add("开心超人");
list.add("哆啦A梦");
list.add("柯南");
list.add("加菲猫");

list.addFirst("小猫");
list.addLast("小狗");

System.out.println(list.getFirst());    // 小猫
System.out.println(list.getLast());     // 小狗

list.removeFirst();
list.removeLast();

栈操作示例(先进后出)

java
LinkedList<String> stack = new LinkedList<>();
stack.push("开心超人");
stack.push("哆啦A梦");
stack.push("柯南");
stack.push("加菲猫");

System.out.println(stack.pop());  // 加菲猫
System.out.println(stack.pop());  // 柯南
System.out.println(stack.pop());  // 哆啦A梦
System.out.println(stack.pop());  // 开心超人

2.4 底层结构

Node节点对象

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

LinkedList节点结构

2.5 get方法优化

LinkedList的get方法采用二分思想优化查询:

java
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;
    }
}
  • 如果index < size/2,从前往后遍历
  • 如果index >= size/2,从后往前遍历

3. Collections集合工具类

3.1 特点

  • 构造方法私有
  • 方法均为静态
  • 使用类名直接调用

3.2 常用方法

方法说明
static <T> boolean addAll(Collection<? super T> c, T... elements)批量添加元素
static void shuffle(List<?> list)打乱集合顺序
static <T> void sort(List<T> list)按默认规则排序(ASCII码值)
static <T> void sort(List<T> list, Comparator<? super T> c)按指定规则排序

3.3 使用示例

批量添加和打乱

java
ArrayList<String> list = new ArrayList<>();
Collections.addAll(list, "小猫", "小狗", "小猪", "小牛", "小羊");
Collections.shuffle(list);

默认排序

java
ArrayList<String> list = new ArrayList<>();
list.add("b.疑是地上霜");
list.add("a.床前明月光");
list.add("c.举头望明月");
list.add("d.低头思故乡");
Collections.sort(list);

3.4 自定义排序

使用Comparator

java
ArrayList<Person> list = new ArrayList<>();
list.add(new Person("张三", 18));
list.add(new Person("李四", 16));
list.add(new Person("王五", 20));

// 升序:o1-o2
// 降序:o2-o1
Collections.sort(list, (o1, o2) -> o1.getAge() - o2.getAge());

使用Comparable

java
@Data
public class Student implements Comparable<Student> {
    private String name;
    private Integer score;

    @Override
    public int compareTo(Student o) {
        return this.getScore() - o.getScore();  // 升序
    }
}

// 使用
ArrayList<Student> list = new ArrayList<>();
list.add(new Student("张三", 100));
list.add(new Student("李四", 90));
list.add(new Student("王五", 95));
Collections.sort(list);

3.5 Arrays.asList注意事项

java
List<String> list = Arrays.asList("小猫", "小狗", "小猪");
// 注意:不要修改集合长度,底层是数组,被final定死
// list.add("小马");  // 会抛出异常

4. 泛型

4.1 为什么使用泛型

从使用层面:统一类型,防止类型转换异常

从定义层面:定义时不确定类型,使用时再规定,代码更灵活

不使用泛型的问题

java
ArrayList list = new ArrayList();
list.add("abc");
list.add(1);
list.add(true);

for (Object o : list) {
    String s = (String) o;  // 运行时抛出ClassCastException
    System.out.println(s.length());
}

泛型类型转换异常

4.2 泛型的定义

含有泛型的类

java
public class MyArrayList<E> {
    private Object[] arr = new Object[10];
    private int size = 0;

    public void add(E e) {
        arr[size] = e;
        size++;
    }

    public E get(int index) {
        return (E) arr[index];
    }
}

// 使用
MyArrayList<String> list = new MyArrayList<>();
list.add("abc");
String element = list.get(0);

含有泛型的方法

java
public class MyCollections {
    public static <E> void addAll(ArrayList<E> list, E... elements) {
        for (E e : elements) {
            list.add(e);
        }
    }
}

// 使用
ArrayList<String> list = new ArrayList<>();
MyCollections.addAll(list, "小猫", "小狗", "小猪");

含有泛型的接口

java
public interface MyList<E> {
    void add(E e);
}

// 实现方式1:实现时确定类型
public class MyScanner implements MyIterator<String> {
    @Override
    public String next() {
        return "键盘录入字符串";
    }
}

// 实现方式2:创建对象时确定类型
public class MyArrayList<E> implements MyList<E> {
    public void add(E e) {
        // 实现
    }
}

4.3 泛型的高级使用

泛型通配符 ?

java
public void method(ArrayList<?> list) {
    for (Object o : list) {
        System.out.println(o);
    }
}

// 可以接收任意类型的ArrayList
method(new ArrayList<String>());
method(new ArrayList<Integer>());

泛型上限

java
// ?只能接收Number及其子类
public static void get1(Collection<? extends Number> collection) {
    // ...
}

get1(new ArrayList<Integer>());  // ✓
get1(new ArrayList<Number>());   // ✓
// get1(new ArrayList<String>()); // ✗

泛型下限

java
// ?只能接收Number及其父类
public static void get2(Collection<? super Number> collection) {
    // ...
}

get2(new ArrayList<Number>());   // ✓
get2(new ArrayList<Object>());   // ✓
// get2(new ArrayList<Integer>()); // ✗

5. 红黑树

5.1 为什么需要红黑树

提高查询效率

红黑树结构

5.2 红黑树规则

  1. 每一个节点或是红色的,或者是黑色的
  2. 根节点必须是黑色
  3. 如果一个节点没有子节点或者父节点,则该节点相应的指针属性值为Nil,这些Nil视为叶节点,每个叶节点(Nil)是黑色的
  4. 如果某一个节点是红色,那么它的子节点必须是黑色(不能出现两个红色节点相连的情况)
  5. 对每一个节点,从该节点到其所有后代叶节点的简单路径上,均包含相同数目的黑色节点

红黑树示例

可视化工具https://www.cs.usfca.edu/~galles/visualization/RedBlack


6. Set集合

6.1 Set接口特点

  • 是一个接口
  • 实现类:HashSet、LinkedHashSet、TreeSet
  • Set集合中的功能没有对Collection接口中的功能进行扩充
  • 所有的set集合底层都是依靠map集合实现的

Set集合继承体系

6.2 HashSet

特点

  • 元素无序
  • 无索引
  • 元素不可重复
  • 线程不安全

数据结构

  • JDK 8之前:哈希表 = 数组 + 链表
  • JDK 8开始:哈希表 = 数组 + 链表 + 红黑树

使用示例

java
HashSet<String> set = new HashSet<>();
set.add("小猫");
set.add("小狗");
set.add("小猪");
set.add("小牛");

for (String s : set) {
    System.out.println(s);
}

6.3 LinkedHashSet

特点

  • 元素有序
  • 无索引
  • 元素不可重复
  • 线程不安全

数据结构

  • 哈希表 + 双向链表

使用示例

java
LinkedHashSet<String> set = new LinkedHashSet<>();
set.add("小猫");
set.add("小狗");
set.add("小猪");
set.add("小牛");

for (String s : set) {
    System.out.println(s);  // 按添加顺序输出
}

二、重点难点

1. ArrayList扩容机制 ⭐⭐⭐

核心要点

  • 无参构造:第一次add时才创建长度为10的数组
  • 扩容时机:当元素个数超过当前容量
  • 扩容大小:新容量 = 旧容量 * 1.5倍
  • 扩容方式:创建新数组,复制元素

面试重点

java
// 扩容核心代码
int newCapacity = oldCapacity + (oldCapacity >> 1);  // 相当于1.5倍

2. LinkedList双向链表结构 ⭐⭐⭐

核心要点

  • 每个节点包含三个部分:prev、item、next
  • 查询优化:二分查找,从中间分开
  • 插入删除效率高,查询效率低

3. 泛型擦除 ⭐⭐⭐

核心要点

  • 泛型只在编译期有效
  • 运行期会进行类型擦除
  • 泛型本质上就是Object类型

4. HashSet如何保证元素唯一 ⭐⭐⭐

核心要点

  • 先比较hashCode
  • 如果hashCode相同,再比较equals
  • 两者都相同才认为是重复元素

三、常见面试题

1. ArrayList和LinkedList的区别?⭐⭐⭐

答案

对比项ArrayListLinkedList
数据结构动态数组双向链表
查询效率高(有索引)低(需要遍历)
增删效率低(需要移动元素)高(只需修改指针)
内存占用连续内存分散内存
适用场景查询多,增删少增删多,查询少

2. ArrayList的扩容机制?⭐⭐⭐

答案

  1. 无参构造创建ArrayList时,初始化为空数组
  2. 第一次add时,创建长度为10的数组
  3. 当元素个数超过当前容量时,自动扩容
  4. 扩容大小为原容量的1.5倍
  5. 使用Arrays.copyOf复制元素到新数组

3. 如何在遍历ArrayList时删除元素?⭐⭐

答案

错误方式

java
for (int i = 0; i < list.size(); i++) {
    if (list.get(i).equals("删除元素")) {
        list.remove(i);  // 会漏删
    }
}

正确方式1:倒序遍历

java
for (int i = list.size() - 1; i >= 0; i--) {
    if (list.get(i).equals("删除元素")) {
        list.remove(i);
    }
}

正确方式2:使用迭代器

java
Iterator<String> iterator = list.iterator();
while (iterator.hasNext()) {
    if (iterator.next().equals("删除元素")) {
        iterator.remove();
    }
}

4. Comparable和Comparator的区别?⭐⭐⭐

答案

对比项ComparableComparator
所属包java.langjava.util
方法compareTo(T o)compare(T o1, T o2)
使用方式类实现接口单独创建比较器
排序规则this - oo1 - o2
适用场景自然排序定制排序

示例对比

java
// Comparable
public class Student implements Comparable<Student> {
    private int score;
    
    @Override
    public int compareTo(Student o) {
        return this.score - o.score;  // 升序
    }
}

// Comparator
Collections.sort(list, (o1, o2) -> o1.getScore() - o2.getScore());

5. HashSet如何保证元素唯一性?⭐⭐⭐

答案

  1. 添加元素时,先计算元素的hashCode值
  2. 如果hashCode值不存在,直接添加
  3. 如果hashCode值存在,调用equals方法比较
  4. 如果equals返回true,认为是重复元素,不添加
  5. 如果equals返回false,添加到链表中

重要结论

  • 必须同时重写hashCode和equals方法
  • 相同对象必须返回相同的hashCode
  • equals返回true的两个对象,hashCode必须相同

6. HashSet和LinkedHashSet的区别?⭐⭐

答案

对比项HashSetLinkedHashSet
元素顺序无序有序(插入顺序)
数据结构哈希表哈希表 + 双向链表
性能稍快稍慢(维护链表)
适用场景不需要顺序需要保持插入顺序

7. 什么是泛型擦除?⭐⭐⭐

答案

  • 泛型只在编译期有效,用于类型检查
  • 运行期会将泛型擦除,替换为Object或上限类型
  • 编译器会在必要的地方插入类型转换代码

示例

java
List<String> list = new ArrayList<>();
list.add("abc");

// 编译后
List list = new ArrayList();
list.add("abc");
String s = (String) list.get(0);  // 编译器插入的类型转换

8. 泛型通配符?、? extends T、? super T的区别?⭐⭐⭐

答案

通配符含义适用场景
?任意类型不关心具体类型
? extends TT及其子类读取数据(生产者)
? super TT及其父类写入数据(消费者)

PECS原则

  • Producer Extends, Consumer Super
  • 频繁往外读取内容,适合用上界Extends
  • 经常往里插入内容,适合用下界Super

四、实用代码片段

1. ArrayList常用操作

java
// 创建并初始化
ArrayList<String> list = new ArrayList<>();
Collections.addAll(list, "元素1", "元素2", "元素3");

// 遍历方式1:普通for
for (int i = 0; i < list.size(); i++) {
    System.out.println(list.get(i));
}

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

// 遍历方式3:迭代器
Iterator<String> iterator = list.iterator();
while (iterator.hasNext()) {
    System.out.println(iterator.next());
}

// 遍历方式4:forEach + lambda
list.forEach(System.out::println);

2. LinkedList作为栈和队列

java
// 作为栈使用(先进后出)
LinkedList<String> stack = new LinkedList<>();
stack.push("元素1");
stack.push("元素2");
System.out.println(stack.pop());  // 元素2

// 作为队列使用(先进先出)
LinkedList<String> queue = new LinkedList<>();
queue.addLast("元素1");
queue.addLast("元素2");
System.out.println(queue.removeFirst());  // 元素1

3. 集合排序

java
// 方式1:实现Comparable
public class Person implements Comparable<Person> {
    private int age;
    
    @Override
    public int compareTo(Person o) {
        return this.age - o.age;
    }
}

// 方式2:使用Comparator
Collections.sort(list, Comparator.comparingInt(Person::getAge));

// 方式3:多条件排序
Collections.sort(list, (o1, o2) -> {
    int result = o1.getAge() - o2.getAge();
    return result != 0 ? result : o1.getName().compareTo(o2.getName());
});

4. HashSet去重

java
// 自定义对象去重
public class Person {
    private String name;
    private int age;
    
    @Override
    public int hashCode() {
        return Objects.hash(name, age);
    }
    
    @Override
    public boolean equals(Object obj) {
        if (this == obj) return true;
        if (obj == null || getClass() != obj.getClass()) return false;
        Person person = (Person) obj;
        return age == person.age && Objects.equals(name, person.name);
    }
}

HashSet<Person> set = new HashSet<>();
set.add(new Person("张三", 18));
set.add(new Person("张三", 18));  // 不会重复添加

5. 泛型方法

java
// 泛型方法示例
public class Utils {
    // 交换数组中两个元素
    public static <T> void swap(T[] arr, int i, int j) {
        T temp = arr[i];
        arr[i] = arr[j];
        arr[j] = temp;
    }
    
    // 打印集合
    public static <E> void print(Collection<E> collection) {
        for (E e : collection) {
            System.out.println(e);
        }
    }
}

五、经典练习题

练习1:ArrayList基本操作 ⭐

题目:创建一个ArrayList,存储5个学生对象,按照年龄升序排序并输出。

参考答案

java
public class Student {
    private String name;
    private int age;
    
    // 构造方法、getter、setter、toString省略
}

public class Test {
    public static void main(String[] args) {
        ArrayList<Student> list = new ArrayList<>();
        list.add(new Student("张三", 18));
        list.add(new Student("李四", 16));
        list.add(new Student("王五", 20));
        list.add(new Student("赵六", 17));
        list.add(new Student("钱七", 19));
        
        Collections.sort(list, Comparator.comparingInt(Student::getAge));
        
        for (Student student : list) {
            System.out.println(student);
        }
    }
}

练习2:LinkedList栈实现 ⭐⭐

题目:使用LinkedList实现一个简单的栈,包含push、pop、peek方法。

参考答案

java
public class MyStack<E> {
    private LinkedList<E> list = new LinkedList<>();
    
    public void push(E e) {
        list.addFirst(e);
    }
    
    public E pop() {
        return list.removeFirst();
    }
    
    public E peek() {
        return list.getFirst();
    }
    
    public boolean isEmpty() {
        return list.isEmpty();
    }
    
    public int size() {
        return list.size();
    }
}

练习3:HashSet去重 ⭐⭐

题目:创建一个HashSet存储学生对象,要求姓名和年龄相同的学生视为同一个学生。

参考答案

java
public class Student {
    private String name;
    private int age;
    
    public Student(String name, int age) {
        this.name = name;
        this.age = age;
    }
    
    @Override
    public int hashCode() {
        return Objects.hash(name, age);
    }
    
    @Override
    public boolean equals(Object obj) {
        if (this == obj) return true;
        if (obj == null || getClass() != obj.getClass()) return false;
        Student student = (Student) obj;
        return age == student.age && Objects.equals(name, student.name);
    }
    
    @Override
    public String toString() {
        return "Student{name='" + name + "', age=" + age + "}";
    }
}

public class Test {
    public static void main(String[] args) {
        HashSet<Student> set = new HashSet<>();
        set.add(new Student("张三", 18));
        set.add(new Student("李四", 16));
        set.add(new Student("张三", 18));  // 不会添加
        
        System.out.println(set.size());  // 2
    }
}

练习4:泛型方法 ⭐⭐

题目:编写一个泛型方法,可以接收任意类型的数组,并反转数组元素。

参考答案

java
public class ArrayUtils {
    public static <T> void reverse(T[] arr) {
        int left = 0;
        int right = arr.length - 1;
        
        while (left < right) {
            T temp = arr[left];
            arr[left] = arr[right];
            arr[right] = temp;
            left++;
            right--;
        }
    }
    
    public static void main(String[] args) {
        Integer[] arr = {1, 2, 3, 4, 5};
        reverse(arr);
        System.out.println(Arrays.toString(arr));  // [5, 4, 3, 2, 1]
        
        String[] strArr = {"a", "b", "c", "d"};
        reverse(strArr);
        System.out.println(Arrays.toString(strArr));  // [d, c, b, a]
    }
}

练习5:集合综合应用 ⭐⭐⭐

题目:从键盘录入5个学生的姓名和成绩,存储到ArrayList中,按照成绩降序排序,输出前三名学生。

参考答案

java
public class Student implements Comparable<Student> {
    private String name;
    private int score;
    
    public Student(String name, int score) {
        this.name = name;
        this.score = score;
    }
    
    @Override
    public int compareTo(Student o) {
        return o.score - this.score;  // 降序
    }
    
    @Override
    public String toString() {
        return name + ": " + score + "分";
    }
    
    public static void main(String[] args) {
        Scanner scanner = new Scanner(System.in);
        ArrayList<Student> list = new ArrayList<>();
        
        for (int i = 0; i < 5; i++) {
            System.out.print("请输入第" + (i + 1) + "个学生的姓名:");
            String name = scanner.next();
            System.out.print("请输入第" + (i + 1) + "个学生的成绩:");
            int score = scanner.nextInt();
            list.add(new Student(name, score));
        }
        
        Collections.sort(list);
        
        System.out.println("前三名学生:");
        for (int i = 0; i < 3 && i < list.size(); i++) {
            System.out.println(list.get(i));
        }
    }
}

六、学习建议

1. 学习路线

ArrayList基础 → ArrayList源码 → LinkedList → Collections工具类

泛型基础 → 泛型高级特性 → 泛型通配符

红黑树(了解) → HashSet → LinkedHashSet → TreeSet(后续学习)

2. 重点掌握

必须掌握

  • ArrayList的扩容机制
  • LinkedList的双向链表结构
  • Collections工具类的常用方法
  • 泛型的基本使用
  • HashSet如何保证元素唯一

理解即可

  • 红黑树的基本规则
  • 泛型擦除原理
  • 泛型上下限

3. 常见错误

错误1:ArrayList删除元素时索引问题

java
// 错误
for (int i = 0; i < list.size(); i++) {
    list.remove(i);  // 会漏删
}

// 正确
for (int i = list.size() - 1; i >= 0; i--) {
    list.remove(i);
}

错误2:HashSet存储自定义对象未重写hashCode和equals

java
// 必须同时重写hashCode和equals方法
@Override
public int hashCode() {
    return Objects.hash(name, age);
}

@Override
public boolean equals(Object obj) {
    // 实现逻辑
}

错误3:泛型使用基本类型

java
// 错误
ArrayList<int> list = new ArrayList<>();

// 正确
ArrayList<Integer> list = new ArrayList<>();

4. 面试准备

高频考点

  1. ArrayList扩容机制
  2. ArrayList vs LinkedList
  3. HashSet如何保证元素唯一
  4. Comparable vs Comparator
  5. 泛型擦除
  6. 泛型通配符

代码手写

  1. 手写ArrayList核心方法
  2. 手写LinkedList节点类
  3. 手写自定义排序
  4. 手写HashSet去重逻辑

5. 实践建议

  1. 多看源码:理解ArrayList和LinkedList的实现原理
  2. 多写代码:完成所有练习题,加深理解
  3. 多画图:画出链表结构、红黑树结构
  4. 多对比:对比不同集合的特点和使用场景
  5. 多总结:整理笔记,形成知识体系

附录:知识点速查表

ArrayList vs LinkedList

操作ArrayListLinkedList
随机访问O(1)O(n)
头部插入O(n)O(1)
尾部插入O(1)O(1)
中间插入O(n)O(n)
删除O(n)O(1)

Set集合对比

集合元素顺序线程安全数据结构
HashSet无序不安全哈希表
LinkedHashSet有序不安全哈希表+链表
TreeSet排序不安全红黑树

泛型通配符

通配符说明示例
?任意类型ArrayList<?>
? extends TT及其子类ArrayList<? extends Number>
? super TT及其父类ArrayList<? super Integer>