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() | 将底层数组大小调整为当前实际元素数量 | 
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() | 删除并返回链表最后一个元素 | 
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() | 返回集合中元素数量 | 
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() | 按插入顺序遍历集合 | 
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的所有元素组成的子集 | 
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() | 返回键值对的集合 | 
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() | 按插入顺序遍历键值对 | 
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 视图 | 
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() | 返回所有值的枚举 | 
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 文档 | 
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()方法(基于排序) | 
下面我们分别讲解它们对元素的要求。
🔁 一、HashSet 和 LinkedHashSet 
✅ 要求: 
- 元素类必须重写: - hashCode()
- equals(Object obj)
 
这两个方法用于判断两个对象是否"相等"。
⚙️ 原理: 
- HashSet使用哈希表存储元素。
- 插入一个元素时,先调用 hashCode()找到桶的位置;
- 如果发生哈希冲突,再使用 equals()比较是否真的重复。
✅ 示例: 
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): 
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): 
Set<Student> set = new TreeSet<>((s1, s2) -> s1.name.compareTo(s2.name));✅ 示例(定义 Comparator 实现类): 
有时,比较逻辑比较复杂,或者我们希望在多个地方复用这个比较器,这时可以定义一个单独的类来实现 Comparator 接口。
假设我们有一个 Student 类,除了 name 还有 age 属性,我们希望按照年龄来排序 Student 对象。
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++ 
| 特征 | Java | C++ | 
|---|---|---|
| 是否需要重写方法/接口 | ✅ 需要(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) | Object类 | Object | ❌ 不是泛型 | 判断任意对象是否相等 | 
| int compareTo(T o) | Comparable<T>接口 | T | ✅ 是泛型 | 比较当前对象与指定类型对象的大小关系 | 
1. equals() 来自 Object 
public boolean equals(Object obj) {
    return (this == obj);
}这是所有类的父类方法,因此必须接收 Object 类型,才能支持任何对象之间的比较。
示例: 
Object a = "hello";
Object b = new Integer(123);
a.equals(b); // 合法,虽然返回 false为了保证兼容性,Java 不得不在 equals() 中使用 Object 参数。
2. compareTo() 来自 Comparable<T> 接口(泛型接口) 
public interface Comparable<T> {
    public int compareTo(T o);
}这是一个泛型接口,你可以指定要比较的对象类型。
你的代码: 
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) 
使用泛型后,编译器可以确保你传入的是正确的类型。
TreeSet<Student> students = new TreeSet<>();
students.add(new Student("Tom"));
students.add(new Student("Jerry"));
// 只能添加 Student 类型,不能添加 String 或其他类型
students.add("Hello"); // ❌ 编译错误!如果 compareTo() 用的是 Object,就无法做到这种编译期检查。
✅ 好处二:避免强制类型转换 
如果你用 Object 作为参数,你需要手动强转:
@Override
public int compareTo(Object obj) {
    Student other = (Student) obj;
    return this.name.compareTo(other.name);
}这样不仅不安全(可能抛出 ClassCastException),而且不够优雅。
而使用泛型后,可以直接使用 Student 类型,无需强转。
🔁 总结对比表 
| 特征 | equals(Object obj) | compareTo(T o) | 
|---|---|---|
| 定义位置 | Object类 | Comparable<T>接口 | 
| 参数类型 | Object | 泛型 T | 
| 是否需要强转 | ✅ 需要 | ❌ 不需要 | 
| 是否类型安全 | ❌ 不够安全 | ✅ 安全 | 
| 使用场景 | 判断相等 | 判断顺序/排序 | 
| 多态支持 | ✅ 支持任意对象 | ✅ 支持泛型匹配对象 | 
📝 总结一句话: 
equals()用Object是为了兼容所有对象,而compareTo()用泛型T是为了类型安全和避免强转。它们的设计体现了 Java 在通用性和安全性之间的权衡。
🧩 进阶建议(可选) 
如果你想进一步理解这些机制,我可以继续讲:
- Comparator<T>和- Comparable<T>的区别?
- Java 中的泛型擦除(Type Erasure)原理?
- 如何自定义排序规则并用于 TreeSet或Collections.sort()?
- Java 8+ 中 Comparator.comparing()的使用技巧?
欢迎继续提问 😊