← Backend / Java

14_Java 泛型与集合框架

List、Set、Map 的选择,equals/hashCode 契约及泛型通配符。

Java 泛型与集合框架

学习目标:根据语义选择 List、Set、Map,理解泛型约束与对象相等规则。

1. 三类常用集合

类型 适合场景 常见实现
List 有顺序、可重复 ArrayList
Set 元素唯一 HashSet、LinkedHashSet
Map 键到值的查找 HashMap、LinkedHashMap
import java.util.HashMap;
import java.util.List;
import java.util.Map;

public class Main {
    public static void main(String[] args) {
        List<String> names = List.of("甲", "乙");
        Map<String, Integer> counts = new HashMap<>();
        for (String name : names) counts.merge(name, 1, Integer::sum);
        System.out.println(counts.getOrDefault("甲", 0));
    }
}

List.of 返回不可修改的集合;不能 add,也不接受 null。HashMap 不保证遍历顺序,若顺序有业务含义,明确选择 LinkedHashMap 或排序后的视图。

2. equals 与 hashCode

相等对象必须有相同哈希值。若自定义对象作为 HashSet 元素或 HashMap 键,需要一致地实现 equals 与 hashCode。放入后不要修改参与哈希计算的字段,否则可能再也找不到该键。不可变 record 常适合值对象。

3. 泛型与通配符

List<String> 不是 List<Object> 的子类型;否则有人可向字符串列表写入整数。通配符处理方向不同:

  • List<? extends Number>:适合从列表读取 Number;通常不能向其添加新数字。
  • List<? super Integer>:适合写入 Integer;读取时只能安全地看作 Object。

可用“生产者用 extends,消费者用 super”辅助记忆,但以实际方法签名为准。泛型主要在编译期保证类型安全;运行时的类型擦除限制了 new T() 和直接判断 instanceof List<String> 等写法。

易错点:不要把 `HashMap` 当作线程安全容器;并发读写要用适当同步策略或 `ConcurrentHashMap`,并理解复合操作的原子性。

自测

  1. 需要按插入顺序输出去重元素,应选什么?LinkedHashSet。
  2. 为什么重写 equals 时通常必须重写 hashCode?散列表依赖两者一致性。

4. 选集合还要看操作成本

ArrayList 适合按索引访问和尾部追加;中间插入/删除可能移动大量元素。HashSet 与 HashMap 适合按哈希查找,但迭代顺序未定义;LinkedHashSet、LinkedHashMap 保留插入顺序,TreeSet、TreeMap 按比较器排序。不要把平均复杂度当作绝对保证,元素数量、哈希质量和内存开销也重要。

import java.util.HashMap;
import java.util.Map;

class CountWords {
    static Map<String, Integer> count(Iterable<String> words) {
        Map<String, Integer> result = new HashMap<>();
        for (String word : words) result.merge(word, 1, Integer::sum);
        return result;
    }
}

修改作为 map 键的对象会破坏查找预期,因为插入时和查找时的哈希值可能不同。值对象最好不可变;若使用 record,确认其组成字段本身的相等语义符合业务需求。

5. 泛型的编译期能力与限制

泛型保证 List<String> 取出的是 String,但运行时类型擦除使 List<String> 与 List<Integer> 的原始类相同。不能直接 new T(),也不能写 instanceof List<String>。通配符适用于 API 边界:只读入数字用 List<? extends Number>;向容器写入整数用 List<? super Integer>。

练习:实现从 List<? extends Number> 求和的方法,再实现向 List<? super Integer> 写入 1 到 3 的方法,解释读取结果为何只能安全看成 Object。