跳转至

Java 数据管理类(集合框架)知识点总结⚓︎

约 1466 个字 250 行代码 预计阅读时间 10 分钟

本章以“点歌机(Jukebox)”系统为背景,深入探讨如何使用 Java 集合框架存储、排序、去重以及管理复杂对象。

一、 Java 集合框架 (Collections Framework) 体系⚓︎

Java 提供了一套完善的工具类来处理不同需求的数据组织,主要有以下三种:

  1. List (列表):顺序是核心。 - 允许重复元素。 - 注重元素的索引和插入顺序。 - 代表类ArrayList
  2. Set (集):唯一性是核心。 - 不允许重复元素。 - 代表类HashSet(无序)、TreeSet(始终保持排序)。
  3. 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) 的深度理解⚓︎

泛型意味着更好的类型安全性,它是集合框架的基石。

  • 核心价值:确保你加入集合的对象是预期的类型。
  • 案例对比
1
2
3
4
5
// 老式写法:可以存入任何Object,取出时需要强转,不安全
List songList = new ArrayList(); 

// 泛型写法:编译器确保只能存入Song对象
List<Song> songList = new ArrayList<Song>(); 

四、 对象的唯一性与去重 (Set)⚓︎

当系统出现重复数据(如点歌记录中有完全相同的行)时,需要使用 Set

1. HashSet 的判定机制⚓︎

一个对象要被 HashSet 识别为重复,必须通过两道关卡:

  1. hashCode():计算对象的散列值。
  2. equals():如果散列值相同,再进行逻辑比较。
  • 关键代码实现
1
2
3
4
5
6
7
8
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 集合框架中,TreeSetSet 接口的一个实现类。它有两个核心特点:

  1. 不允许重复:集合中不能有相同的元素。
  2. 自动排序:它会按照元素的“自然顺序”进行升序排列。

为了让 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)

代码演示陷阱:

1
2
3
4
5
6
// 数组的情况:编译通过,但运行时会抛出 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)与数组不同,它们是不协变的。

  • 问题:虽然 DogAnimal,但 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()) 会报错!(类型安全保护)
    }
}

六、 总结:场景化选型方案⚓︎

  1. 列表读取:保持读取顺序 \(\rightarrow\) ArrayList

  2. 外部排序:需要灵活切换排序规则 \(\rightarrow\) Collections.sort() + Comparator

  3. 防止重复:快速过滤重复歌曲 \(\rightarrow\) HashSet(需重写 hashCode/equals)。

  4. 持续有序:系统需要时刻展示有序歌单 \(\rightarrow\) TreeSet

  5. 快速检索:通过 Key 找 Value \(\rightarrow\) HashMap

  • 代码演示:
1
2
3
HashMap<String, Integer> scores = new HashMap<String, Integer>();
scores.put("SongA", 100);
int score = scores.get("SongA"); // 快速获取