Java 数据管理类(集合框架)知识点总结
约 1466 个字 250 行代码 预计阅读时间 10 分钟
本章以“点歌机(Jukebox)”系统为背景,深入探讨如何使用 Java 集合框架存储、排序、去重以及管理复杂对象。
一、 Java 集合框架 (Collections Framework) 体系
Java 提供了一套完善的工具类来处理不同需求的数据组织,主要有以下三种:
- List (列表):顺序是核心。
- 允许重复元素。
- 注重元素的索引和插入顺序。
- 代表类:
ArrayList。
- Set (集):唯一性是核心。
- 不允许重复元素。
- 代表类:
HashSet(无序)、TreeSet(始终保持排序)。
- Map (映射):搜索专家。
- 使用“键-值对”(Key-Value)存储。
- 不继承自
Collection 接口,但属于集合框架。
- 代表类:HashMap。
二、 对象的排序逻辑 (Sorting)
在处理简单的 String 列表时,可以直接排序;但处理自定义对象(如 Song 类)时,需要明确比较规则。
样例代码见 ds 老师 deepseek的代码
1. 从 ArrayList 到 Collections.sort()
- 注意:
ArrayList 本身并没有 sort() 方法。
- 解决方案:使用
java.util.Collections.sort(List<T> list,Comparator<T>),它利用了多态性,可以对任何实现了 List 接口的集合进行排序。
- Collections.sort()的两种重载形式:
Collections.sort(List<T> list): 要求集合中的元素 T 必须实现了 Comparable 接口(即具备“自然排序”能力,如数字从小到大,字符串按字母表)。
Collections.sort(List<T> list, Comparator<? super T> c): 允许你传入一个自定义的比较器(Comparator)。这在元素没有实现接口,或者你需要特殊排序规则(如按年龄倒序)时非常有用。
- 案例代码
| List<String> songs = new ArrayList<>();
songs.add("Yesterday/The Beatles");
songs.add("Bohemian Rhapsody/Queen");
songs.add("Hotel California/Eagles");
songs.add("Imagine/John Lennon");
songs.add("Billie Jean/Michael Jackson");
// ArrayList本身没有sort()方法,但Collections类提供了sort()方法
// Collections.sort()可以对任何List进行排序。String类已经实现了Comparable接口,所以可以直接排序
// 排序规则:按字母顺序(字典序)升序排列
Collections.sort(songs);
|
2. Comparable 接口 (内部自然排序)
- 原理:让对象本身具备“可比较性”。
- 实现:类必须实现
Comparable<T> 接口并重写 compareTo(T o)。
- 案例代码:
| public class Song implements Comparable<Song> {
String title;
String artist;
public int compareTo(Song s) {
// 返回负数、零或正数,分别代表小于、等于、大于
return title.compareTo(s.getTitle());
}
// Getter, Setter 和 toString 省略...
}
|
- 局限:一个类只能有一个
compareTo 实现。如果想按歌手排序,就得修改源代码,这违反了开闭原则。
3. Comparator 接口 (外部定制排序)
- 场景:当需要多种排序方式(如按歌名、按歌手、按评分)时。
- 实现:创建一个独立的类实现
Comparator<T>。
- 案例代码:
| // 这是一个独立的类,专门负责按歌手排序
class ArtistComparator implements Comparator<Song> {
public int compare(Song one, Song two) {
return one.getArtist().compareTo(two.getArtist());
// one.getArtist.compareTo(two.getArtist),compareTo是String方法
}
}
// 使用方法
ArtistComparator artistCompare = new ArtistComparator();
Collections.sort(songList, artistCompare);
|
- 优化建议:避免在
compareTo 中通过加 flag 的方式写大量的 if-else 判断,应当优先使用 Comparator。
三、 泛型 (Generics) 的深度理解
泛型意味着更好的类型安全性,它是集合框架的基石。
- 核心价值:确保你加入集合的对象是预期的类型。
- 案例对比:
| // 老式写法:可以存入任何Object,取出时需要强转,不安全
List songList = new ArrayList();
// 泛型写法:编译器确保只能存入Song对象
List<Song> songList = new ArrayList<Song>();
|
四、 对象的唯一性与去重 (Set)
当系统出现重复数据(如点歌记录中有完全相同的行)时,需要使用 Set。
1. HashSet 的判定机制
一个对象要被 HashSet 识别为重复,必须通过两道关卡:
- hashCode():计算对象的散列值。
- equals():如果散列值相同,再进行逻辑比较。
| public boolean equals(Object aSong) {
Song s = (Song) aSong;
return getTitle().equals(s.getTitle());
}
public int hashCode() {
return title.hashCode(); // 必须重写,确保相同的title返回相同的hash
}
|
- 结论:如果你重写了
equals(),必须同时重写 hashCode()。
| import java.util.*;
// 1. 定义 Song 类,实现 Comparable 接口用于排序
class Song implements Comparable<Song> {
private String title;
private String artist;
Song(String t, String a) {
title = t;
artist = a;
}
// HashSet 检查重复的第一步:计算哈希码
@Override
public int hashCode() {
// 因为 title 是 String,String 类已经重写了 hashCode
// 只要 title 相同,返回的哈希码就相同
return title.hashCode();
}
// HashSet 检查重复的第二步:如果哈希码相同,则调用 equals 确认
@Override
public boolean equals(Object aSong) {
if (this == aSong) return true;
if (!(aSong instanceof Song)) return false;
Song s = (Song) aSong;
return getTitle().equals(s.getTitle());
}
// 用于 Collections.sort() 排序
@Override
public int compareTo(Song s) {
return title.compareTo(s.getTitle());
}
public String getTitle() { return title; }
public String getArtist() { return artist; }
// 重写 toString,方便打印输出
@Override
public String toString() {
return title + " (" + artist + ")";
}
}
// 2. 测试类
public class JukeboxTest {
public static void main(String[] args) {
new JukeboxTest().go();
}
public void go() {
List<Song> songList = new ArrayList<>();
// 添加数据,注意 "Pink Moon" 出现了两次
songList.add(new Song("Pink Moon", "Nick Drake"));
songList.add(new Song("Havah Nagila", "The Klezmatics"));
songList.add(new Song("Pink Moon", "Nick Drake"));
songList.add(new Song("Circles", "Mac Miller"));
System.out.println("1. 原始列表 (ArrayList):");
System.out.println(songList);
System.out.println("列表长度: " + songList.size());
System.out.println("\n2. 排序后的列表:");
Collections.sort(songList);
System.out.println(songList);
// 使用 HashSet 进行去重
System.out.println("\n3. 去重后的集合 (HashSet):");
HashSet<Song> songSet = new HashSet<>(songList);
System.out.println(songSet);
System.out.println("集合长度: " + songSet.size());
}
}
|
2. TreeSet:自动排序的集
在 Java 集合框架中,TreeSet 是 Set 接口的一个实现类。它有两个核心特点:
- 不允许重复:集合中不能有相同的元素。
- 自动排序:它会按照元素的“自然顺序”进行升序排列。
为了让 TreeSet 正常工作,存入的对象必须实现 Comparable 接口,或者在创建 TreeSet 时提供一个 Comparator。
在下面的例子中,如果只是创建了 Book 对象并放入 TreeSet。由于 Book 只是一个普通的类,Java 不知道你是想按“书名”排序、按“作者”排序还是按“价格”排序。
| import java.util.*;
// 1. 定义 Book 类,必须实现 Comparable 接口,TreeSet 才知道怎么排
class Book implements Comparable<Book> {
String title;
public Book(String t) {
title = t;
}
// TreeSet 在添加元素时会自动调用这个方法进行比较
@Override
public int compareTo(Book b) {
return title.compareTo(b.title); // 使用 String 自带的比较方法按字母排序
}
// 为了让 System.out.println(tree) 打印出好看的内容,需要重写 toString
@Override
public String toString() {
return title;
}
}
//默认的TreeSet,工作起来就会像sort()一样,使用对象的compareTo()方法来排序,也可以传入Comparator
//缺点就是如果并不是要一直保持排序,就会浪费
public class TestTree {
public static void main(String[] args) {
new TestTree().go();
}
public void go() {
Book b1 = new Book("How Cats Work");
Book b2 = new Book("Remix your Body");
Book b3 = new Book("Finding Emo");
// 创建 TreeSet
TreeSet<Book> tree = new TreeSet<>();
tree.add(b1);
tree.add(b2);
tree.add(b3);
// 打印结果:你会发现它们是按字母顺序排列的
System.out.println("排序后的书单:");
System.out.println(tree);
}
}
|
| 特性 |
HashSet |
TreeSet |
| 顺序 |
无序(随机) |
有序(按排序规则) |
| 性能 |
极快(\(O(1)\)) |
稍慢(\(O(\log n)\)) |
| 底层实现 |
哈希表(HashMap) |
红黑树(TreeMap) |
Map
HashMap 的基础
HashMap 就像一本字典,它存储的是“键值对”(Key-Value pairs)。你通过一个 Key(如姓名)来存取对应的 Value(如分数)。
思路与注意事项
- 键值对存储:使用
put(key, value) 存入,使用 get(key) 取出。
- 泛型约束:
HashMap<String, Integer> 规定了 Key 必须是字符串,Value 必须是整数。
- 无序性:
HashMap 不保证元素的存储顺序。
| import java.util.*;
public class TestMap {
public static void main(String[] args) {
// 创建一个 HashMap,Key 是 String,Value 是 Integer
HashMap<String, Integer> scores = new HashMap<>();
scores.put("Kathy", 42);
scores.put("Bert", 343);
scores.put("Skyler", 420);
System.out.println("完整的 Map 内容: " + scores);
// 获取特定 Key 的 Value
System.out.println("Bert 的分数是: " + scores.get("Bert"));
}
}
|
五、 集合与数组的差异 -泛型、多态与通配符
Java 对数组和泛型集合的处理方式完全不同:
| 特性 |
数组 (Arrays) |
集合 (ArrayList) |
| 多态行为 |
协变 (Covariant) |
不便性 (Invariant) |
| 安全性检查 |
运行时 (Runtime) |
编译时 (Compile-time) |
代码演示陷阱:
| // 数组的情况:编译通过,但运行时会抛出 ArrayStoreException
Animal[] animals = {new Dog(), new Dog()};
animals[0] = new Cat(); // 运行时报错,因为数组知道自己实际是Dog型
// 集合的情况:编译直接报错!
ArrayList<Animal> list = new ArrayList<Dog>(); // 编译错误!防止你后续往Dog列表里加Cat
|
这是 Java 泛型中最容易让人困惑的地方。图片展示了从“数组”到“泛型集合”的演进过程。
1. 数组的多态
在 Java 中,数组是协变的。这意味着如果你有一个 Animal[] 类型的参数,你可以向它传递一个 Dog[]。
- 原理:
Dog 是一个 Animal(IS-A 关系),所以 Dog 数组也被视为一种 Animal 数组。
2. 泛型的陷阱
泛型集合(如 ArrayList)与数组不同,它们是不协变的。
- 问题:虽然
Dog 是 Animal,但 ArrayList<Dog> 不是 ArrayList<Animal>。
- 原因:如果 Java 允许这样做,你可能会在
takeAnimals(ArrayList<Animal> animals) 方法里往原本只该装狗的 List 里塞进一只猫(animals.add(new Cat())),这会引发运行时崩溃。
3. 通配符解决方案
为了让方法能接受任何动物子类的 List,我们需要使用通配符 ? extends Animal。
ArrayList<? extends Animal>:这表示“一个存储某种 Animal 子类型的 ArrayList”。
- 代价:使用了这种语法后,你不能向该集合添加任何元素(除了
null),因为编译器无法确定这个 List 到底具体是什么类型。
| import java.util.*;
// 定义动物类层次结构 (image_455b8a)
abstract class Animal {
void eat() {
System.out.println("animal eating");
}
}
class Dog extends Animal {
void bark() { System.out.println("Woof!"); }
}
class Cat extends Animal {
void meow() { System.out.println("Meow!"); }
}
public class TestGenerics {
public static void main(String[] args) {
new TestGenerics().go();
}
public void go() {
// 场景 1:数组的多态
Animal[] animalsArray = {new Dog(), new Cat(), new Dog()};
Dog[] dogsArray = {new Dog(), new Dog()};
System.out.println("--- 执行数组多态 ---");
takeAnimalsArray(animalsArray);
takeAnimalsArray(dogsArray); // 数组允许这样做
// 场景 2:泛型集合
ArrayList<Animal> animalsList = new ArrayList<>();
animalsList.add(new Dog());
animalsList.add(new Cat());
ArrayList<Dog> dogsList = new ArrayList<>();
dogsList.add(new Dog());
System.out.println("\n--- 执行泛型通配符 ---");
takeAnimalsList(animalsList);
takeAnimalsList(dogsList); // 关键:有了 <? extends Animal> 才能传递 dogsList
}
// 处理数组的方法
public void takeAnimalsArray(Animal[] animals) {
for (Animal a : animals) {
a.eat();
}
}
// 处理集合的方法:使用通配符保证灵活性 (image_455bab)
public void takeAnimalsList(ArrayList<? extends Animal> animals) {
for (Animal a : animals) {
a.eat();
}
// 注意:在这里 animals.add(new Cat()) 会报错!(类型安全保护)
}
}
|
六、 总结:场景化选型方案
-
列表读取:保持读取顺序 \(\rightarrow\) ArrayList。
-
外部排序:需要灵活切换排序规则 \(\rightarrow\) Collections.sort() + Comparator。
-
防止重复:快速过滤重复歌曲 \(\rightarrow\) HashSet(需重写 hashCode/equals)。
-
持续有序:系统需要时刻展示有序歌单 \(\rightarrow\) TreeSet。
-
快速检索:通过 Key 找 Value \(\rightarrow\) HashMap。
| HashMap<String, Integer> scores = new HashMap<String, Integer>();
scores.put("SongA", 100);
int score = scores.get("SongA"); // 快速获取
|