Skip to content

Java 集合框架笔记

Java 提供了一套强大的集合框架(Java Collections Framework),用于存储和操作一组对象。主要包括以下核心接口及其实现类:

  • List:有序、可重复
  • Set:无序、不可重复
  • Map:键值对映射

下面我们分别介绍它们的主要实现类及其常用方法和示例。

一、List 接口:有序、可重复集合

常用实现类:

  • ArrayList
  • LinkedList

ArrayList:基于动态数组的线性表

特点:

  • 支持快速随机访问(O(1))
  • 插入/删除效率较低(O(n),除非在末尾)
  • 线程不安全

常用方法与示例:

方法描述
boolean add(E e)添加元素到末尾
void add(int index, E element)在指定位置插入元素
E get(int index)获取指定索引处的元素
E set(int index, E element)替换指定索引处的元素并返回旧值
E remove(int index)删除指定索引处的元素并返回该元素
int size()返回当前元素数量
void ensureCapacity(int minCapacity)手动扩容内部数组
void trimToSize()将底层数组大小调整为当前实际元素数量
java
ArrayList<String> list = new ArrayList<>();
list.add("A");
list.add(0, "B"); // 插入到索引0
System.out.println(list.get(1)); // 输出 A
list.set(0, "C");
list.remove(1);
System.out.println(list.size()); // 输出 1

list.ensureCapacity(100); // 手动扩容
list.trimToSize(); // 节省内存

LinkedList:基于双向链表的线性表

特点:

  • 插入/删除效率高(O(1),已知节点位置)
  • 不支持快速随机访问(O(n))
  • 可作为栈、队列或双端队列使用
  • 线程不安全

常用方法与示例:

方法描述
void addFirst(E e)在链表头部插入元素
void addLast(E e)在链表尾部插入元素
E getFirst()获取链表第一个元素(不删除)
E getLast()获取链表最后一个元素(不删除)
E removeFirst()删除并返回链表第一个元素
E removeLast()删除并返回链表最后一个元素
java
LinkedList<Integer> list = new LinkedList<>();
list.addFirst(1);
list.addLast(2);
System.out.println(list.getFirst()); // 输出 1
System.out.println(list.removeLast()); // 输出 2

二、Set 接口:无序、不可重复集合

常用实现类:

  • HashSet
  • LinkedHashSet
  • TreeSet

HashSet:基于哈希表的无序集合

特点:

  • 元素无序
  • 插入、查找效率接近 O(1)
  • 允许 null 元素
  • 线程不安全

常用方法与示例:

方法描述
boolean add(E e)添加元素,若已存在则不添加
boolean remove(Object o)删除指定元素
boolean contains(Object o)判断是否包含某个元素
int size()返回集合中元素数量
java
HashSet<String> set = new HashSet<>();
set.add("Java");
set.add("Python");
System.out.println(set.contains("Java")); // true
set.remove("Python");
System.out.println(set.size()); // 输出 1

LinkedHashSet:保留插入顺序的 Set

特点:

  • 继承自 HashSet
  • 使用双向链表维护插入顺序
  • 插入性能略低于 HashSet

常用方法与示例:

方法描述
Iterator<E> iterator()按插入顺序遍历集合
java
LinkedHashSet<String> set = new LinkedHashSet<>();
set.add("A");
set.add("B");
for (String s : set) {
    System.out.print(s + " "); // 输出 A B
}

TreeSet:基于红黑树的有序集合

特点:

  • 元素默认按自然顺序排序(升序)
  • 插入、查找、删除时间复杂度为 O(log n)
  • 不允许 null 元素
  • 线程不安全

常用方法与示例:

方法描述
E first()返回集合中的第一个(最小)元素
E last()返回集合中的最后一个(最大)元素
SortedSet<E> headSet(E toElement)返回小于 toElement 的所有元素组成的子集
SortedSet<E> tailSet(E fromElement)返回大于等于 fromElement 的所有元素组成的子集
java
TreeSet<Integer> set = new TreeSet<>();
set.add(3);
set.add(1);
set.add(2);

System.out.println(set.first()); // 输出 1
System.out.println(set.last());  // 输出 3
System.out.println(set.headSet(2)); // 输出 [1]

三、Map 接口:键值对集合

常用实现类:

  • HashMap
  • LinkedHashMap
  • TreeMap
  • Hashtable

HashMap:基于哈希表的键值对集合

特点:

  • 键和值都可以为 null
  • 无序(插入顺序 ≠ 存储顺序)
  • 插入、查找效率接近 O(1)
  • 线程不安全

常用方法与示例:

方法描述
V put(K key, V value)添加或更新键值对
V get(Object key)根据键获取对应的值
V remove(Object key)根据键删除键值对并返回对应值
boolean containsKey(Object key)判断是否包含指定键
Set<K> keySet()返回所有键的集合
Collection<V> values()返回所有值的集合
Set<Map.Entry<K,V>> entrySet()返回键值对的集合
java
HashMap<String, Integer> map = new HashMap<>();
map.put("Java", 1);
map.put("Python", 2);
System.out.println(map.get("Java")); // 输出 1
System.out.println(map.containsKey("Python")); // true

LinkedHashMap:保留插入顺序的 Map

特点:

  • 继承自 HashMap
  • 使用双向链表维护插入顺序
  • 插入性能略低于 HashMap

常用方法与示例:

方法描述
Iterator<Map.Entry<K,V>> entrySet()按插入顺序遍历键值对
java
LinkedHashMap<String, Integer> map = new LinkedHashMap<>();
map.put("A", 1);
map.put("B", 2);
for (Map.Entry<String, Integer> entry : map.entrySet()) {
    System.out.println(entry.getKey() + ": " + entry.getValue());
}
// 输出:
// A: 1
// B: 2

TreeMap:基于红黑树的有序 Map

特点:

  • 按键排序(自然顺序或自定义顺序)
  • 插入、查找、删除时间复杂度为 O(log n)
  • 键不能为 null

常用方法与示例:

方法描述
Map.Entry<K,V> firstEntry()返回第一个(最小键)键值对
Map.Entry<K,V> lastEntry()返回最后一个(最大键)键值对
NavigableMap<K,V> descendingMap()返回逆序的 Map 视图
java
TreeMap<String, Integer> map = new TreeMap<>();
map.put("Java", 1);
map.put("C++", 2);
map.put("Python", 3);

System.out.println(map.firstEntry()); // 输出 C++=2
System.out.println(map.descendingMap()); // 输出 {Python=3, Java=1, C++=2}

Hashtable:早期线程安全版本的 Map

特点:

  • 键和值都不能为 null
  • 线程安全(synchronized)
  • 已被 ConcurrentHashMap 替代,但仍常见于老项目

常用方法与示例:

方法描述
Enumeration<K> keys()返回所有键的枚举
Enumeration<V> elements()返回所有值的枚举
java
import java.util.*;

Hashtable<String, String> table = new Hashtable<>();
table.put("name", "Tom");
System.out.println(table.get("name"));

Enumeration<String> keys = table.keys();
while (keys.hasMoreElements()) {
    String key = keys.nextElement();
    System.out.println(key + ": " + table.get(key));
}

Properties: 持久的属性集

特点:

  • 继承自 Hashtable,因此也是线程安全的。
  • 键和值通常都是字符串类型。
  • 主要用于读写配置文件(.properties 文件)。
  • 可以从输入/输出流加载和存储属性。

常用方法与示例:

方法描述
String getProperty(String key)获取指定键的属性值
String getProperty(String key, String defaultValue)获取指定键的属性值,若键不存在则返回默认值
Object setProperty(String key, String value)设置属性键值对(返回先前的值,如果没有则返回 null)
void load(InputStream inStream)从输入字节流加载属性列表
void store(OutputStream out, String comments)将属性列表写入输出字节流
void loadFromXML(InputStream in)从 XML 文档加载所有属性
void storeToXML(OutputStream os, String comment)将属性列表写入 XML 文档
java
import java.util.Properties;
import java.io.FileOutputStream;
import java.io.FileInputStream;
import java.io.IOException;

public class PropertiesExample {
    public static void main(String[] args) {
        Properties props = new Properties();

        // 设置属性
        props.setProperty("database.url", "localhost");
        props.setProperty("database.user", "admin");
        props.setProperty("database.password", "secret");

        // 获取属性
        System.out.println(props.getProperty("database.user")); // 输出 admin

        // 保存到文件
        try (FileOutputStream fos = new FileOutputStream("config.properties")) {
            props.store(fos, "数据库配置");
        } catch (IOException e) {
            e.printStackTrace();
        }

        // 从文件加载
        Properties loadedProps = new Properties();
        try (FileInputStream fis = new FileInputStream("config.properties")) {
            loadedProps.load(fis);
            System.out.println(loadedProps.getProperty("database.url")); // 输出 localhost
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}

四、总结表格(一句话记忆)

类名底层结构是否有序是否线程安全是否允许 null 键/值
ArrayList动态数组
LinkedList双向链表
HashSet哈希表
LinkedHashSet哈希表 + 链表✅(插入顺序)
TreeSet红黑树✅(排序)
HashMap哈希表
LinkedHashMap哈希表 + 链表✅(插入顺序)
TreeMap红黑树✅(按键排序)
Hashtable哈希表
Properties哈希表
深入解析:Java 的 Set 是如何判断重复元素的?

🧠 深入解析:Java 的 Set 是如何判断重复元素的?

Java 中常见的 Set 实现有:

Set 类型判断重复的方式
HashSet使用 hashCode()equals() 方法
LinkedHashSet同上,只是保留插入顺序
TreeSet使用 compareTo() 方法(基于排序)

下面我们分别讲解它们对元素的要求。

🔁 一、HashSetLinkedHashSet

✅ 要求:

  • 元素类必须重写:
    • hashCode()
    • equals(Object obj)

这两个方法用于判断两个对象是否"相等"。

⚙️ 原理:

  • HashSet 使用哈希表存储元素。
  • 插入一个元素时,先调用 hashCode() 找到桶的位置;
  • 如果发生哈希冲突,再使用 equals() 比较是否真的重复。

✅ 示例:

java
class Person {
    String name;

    public Person(String name) {
        this.name = name;
    }

    // 重写 equals()
    @Override
    public boolean equals(Object obj) {
        if (this == obj) return true;
        if (!(obj instanceof Person)) return false;
        Person other = (Person) obj;
        return this.name.equals(other.name);
    }

    // 重写 hashCode()
    @Override
    public int hashCode() {
        return name.hashCode();
    }
}

public class Main {
    public static void main(String[] args) {
        Set<Person> set = new HashSet<>();
        Person p1 = new Person("Tom");
        Person p2 = new Person("Tom");

        set.add(p1);
        set.add(p2);

        System.out.println(set.size()); // 输出 1,说明是同一个元素
    }
}

📌 如果你不重写 hashCode()equals(),即使内容相同,也会被视为不同对象。

📚 二、TreeSet

✅ 要求:

  • 元素类必须实现 Comparable<T> 接口,并重写 compareTo() 方法;
  • 或者,在创建 TreeSet 时传入一个自定义的 Comparator<T>

⚙️ 原理:

  • TreeSet 是基于红黑树实现的有序集合;
  • 它通过比较对象的大小来决定元素位置和去重;
  • 如果 compareTo() 返回 0,就认为两个对象相等。

✅ 示例(实现 Comparable):

java
class Student implements Comparable<Student> {
    String name;

    public Student(String name) {
        this.name = name;
    }

    @Override
    public int compareTo(Student other) {
        return this.name.compareTo(other.name); // 根据 name 排序
    }
}

public class Main {
    public static void main(String[] args) {
        Set<Student> set = new TreeSet<>();
        Student s1 = new Student("Alice");
        Student s2 = new Student("Alice");

        set.add(s1);
        set.add(s2);

        System.out.println(set.size()); // 输出 1,说明是同一个元素
    }
}

✅ 示例(使用 Comparator):

java
Set<Student> set = new TreeSet<>((s1, s2) -> s1.name.compareTo(s2.name));

✅ 示例(定义 Comparator 实现类):

有时,比较逻辑比较复杂,或者我们希望在多个地方复用这个比较器,这时可以定义一个单独的类来实现 Comparator 接口。

假设我们有一个 Student 类,除了 name 还有 age 属性,我们希望按照年龄来排序 Student 对象。

java
class Student { // 假设 Student 类已定义,包含 name 和 age
    String name;
    int age;

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

    @Override
    public String toString() {
        return "Student{name='" + name + "', age=" + age + "}";
    }
    // 注意:这里我们不需要 Student 实现 Comparable 接口
    // 也不需要重写 equals 和 hashCode (除非 HashSet/LinkedHashSet 也需要用)
}

// 自定义比较器类,按年龄升序排序
class StudentAgeComparator implements Comparator<Student> {
    @Override
    public int compare(Student s1, Student s2) {
        // 返回负整数、零或正整数,分别表示 s1 小于、等于或大于 s2
        return s1.age - s2.age;
        // 如果希望降序,可以是 s2.age - s1.age
    }
}

public class Main {
    public static void main(String[] args) {
        // 创建 TreeSet 时传入自定义比较器实例
        Set<Student> studentSetByAge = new TreeSet<>(new StudentAgeComparator());

        studentSetByAge.add(new Student("Alice", 20));
        studentSetByAge.add(new Student("Bob", 18));
        studentSetByAge.add(new Student("Charlie", 22));

        // 元素将按年龄排序
        for (Student s : studentSetByAge) {
            System.out.println(s);
        }
        // 输出:
        // Student{name='Bob', age=18}
        // Student{name='Alice', age=20}
        // Student{name='Charlie', age=22}
    }
}

使用单独的 Comparator 类的好处:

  • 关注点分离:将比较逻辑从元素类中分离出来。
  • 灵活性:可以为同一个类提供多种不同的排序方式。
  • 可复用性:比较器可以在需要相同排序逻辑的多个地方共享。

🆚 对比 C++

特征JavaC++
是否需要重写方法/接口✅ 需要(hashCode/equals 或 Comparable)✅ 需要(如 operator<、operator==)
默认去重依据hashCode() + equals()operator==()
默认排序依据❌(HashSet) / compareTo()(TreeSet)operator<()
是否支持函数对象自定义比较器✅(Comparator)✅(仿函数)

🧩 总结一句话:

Java 中的 Set 不像 C++ 那样需要重载运算符,但为了正确判断元素是否重复或进行排序,你需要为自定义类重写相应的方法:

  • HashSet:重写 hashCode()equals()
  • TreeSet:实现 Comparable 或提供 Comparator
为什么 compareTo(Student other) 的参数不是 Object,而 equals(Object obj) 是?

这个问题非常有深度,它触及到了 Java 中两个重要的面向对象特性:

  • 接口设计
  • 类型安全(Type Safety)

我们来一步一步地解释清楚。

✅ 简单回答:

因为 compareTo() 是定义在 Comparable<T> 接口里的方法,它的泛型版本直接指定了比较的对象类型,所以不需要用 Object。而 equals() 是定义在 Object 类中的方法,必须能接受任何类型的对象作为参数。

🧠 深入讲解:Java 中的两个方法设计差异

方法所属参数类型是否泛型设计目的
boolean equals(Object obj)ObjectObject❌ 不是泛型判断任意对象是否相等
int compareTo(T o)Comparable<T> 接口T✅ 是泛型比较当前对象与指定类型对象的大小关系

1. equals() 来自 Object

java
public boolean equals(Object obj) {
    return (this == obj);
}

这是所有类的父类方法,因此必须接收 Object 类型,才能支持任何对象之间的比较。

示例:

java
Object a = "hello";
Object b = new Integer(123);

a.equals(b); // 合法,虽然返回 false

为了保证兼容性,Java 不得不在 equals() 中使用 Object 参数。

2. compareTo() 来自 Comparable<T> 接口(泛型接口)

java
public interface Comparable<T> {
    public int compareTo(T o);
}

这是一个泛型接口,你可以指定要比较的对象类型。

你的代码:

java
class Student implements Comparable<Student> {
    String name;

    @Override
    public int compareTo(Student other) {
        return this.name.compareTo(other.name);
    }
}

这里 Student 实现了 Comparable<Student>,说明它只能和另一个 Student 对象进行比较。

这样做有什么好处呢?

✅ 好处一:类型安全(Type Safety)

使用泛型后,编译器可以确保你传入的是正确的类型。

java
TreeSet<Student> students = new TreeSet<>();
students.add(new Student("Tom"));
students.add(new Student("Jerry"));

// 只能添加 Student 类型,不能添加 String 或其他类型
students.add("Hello"); // ❌ 编译错误!

如果 compareTo() 用的是 Object,就无法做到这种编译期检查。

✅ 好处二:避免强制类型转换

如果你用 Object 作为参数,你需要手动强转:

java
@Override
public int compareTo(Object obj) {
    Student other = (Student) obj;
    return this.name.compareTo(other.name);
}

这样不仅不安全(可能抛出 ClassCastException),而且不够优雅。

而使用泛型后,可以直接使用 Student 类型,无需强转。

🔁 总结对比表

特征equals(Object obj)compareTo(T o)
定义位置ObjectComparable<T> 接口
参数类型Object泛型 T
是否需要强转✅ 需要❌ 不需要
是否类型安全❌ 不够安全✅ 安全
使用场景判断相等判断顺序/排序
多态支持✅ 支持任意对象✅ 支持泛型匹配对象

📝 总结一句话:

equals()Object 是为了兼容所有对象,而 compareTo() 用泛型 T 是为了类型安全和避免强转。它们的设计体现了 Java 在通用性和安全性之间的权衡。

🧩 进阶建议(可选)

如果你想进一步理解这些机制,我可以继续讲:

  • Comparator<T>Comparable<T> 的区别?
  • Java 中的泛型擦除(Type Erasure)原理?
  • 如何自定义排序规则并用于 TreeSetCollections.sort()
  • Java 8+ 中 Comparator.comparing() 的使用技巧?

欢迎继续提问 😊