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()
的使用技巧?
欢迎继续提问 😊