☕ 第八章 泛型
📄 一句话概括

8.1 学会了用和写单个占位符的泛型类;这节进阶三步:多个类型参数(Pair\<K, V>)、泛型接口、泛型类的继承两姿势——学完你会发现 HashMap 的骨架你自己就能搭出来。

1. 多个类型参数:Pair\<K, V>

一个占位符不够用?逗号隔开,各管各的:

public class Pair<K, V> {          // K 管键,V 管值
    private K key;
    private V value;

    public Pair(K key, V value) {
        this.key = key;
        this.value = value;
    }

    public K getKey()   { return key; }
    public V getValue() { return value; }
}

使用:

Pair<String, Integer> p = new Pair<>("波仔", 95);
String name = p.getKey();     // K 出来是 String
Integer score = p.getValue(); // V 出来是 Integer
💡 认亲时刻

HashMap 就是 Pair 思想的放大版——每个键值对就是一间 K→V 的格子。7.6 学 Map 时看到的 Map<K, V> 尖括号里那两个字母,源头上就是这里的多参数占位符。

2. 泛型接口

接口也能带占位符,最典型的模样是"生产者":

public interface Generator<T> {
    T next();          // 每次要一个 T 类型的东西
}

实现它有两种姿势(和继承泛型类完全同款):

// 姿势一:当场拍板——定死 String
class StrGen implements Generator<String> {
    @Override
    public String next() { return "波仔" + new Random().nextInt(100); }
}

// 姿势二:继续传下去——保持泛型(类也要跟着声明 <T>)
class UserGen<T> implements Generator<T> {
    private Random r = new Random();
    @Override
    public T next() { return null; }   // 实际项目里返回真实数据
}

3. 泛型类的继承:两种姿势二选一

父类留着占位符 T,子类接手时必须表态:

姿势 写法 结果
当场拍板 class StringBox extends Box<String> 子类是普通类,用的时候不用写 <>
继续传下去 class SubBox<T> extends Box<T> 子类还是泛型类,T 继续由使用者决定
class StringBox extends Box<String> { }        // 定死:StringBox 只装 String
StringBox sb = new StringBox();                // 不用写 <>

class SubBox<T> extends Box<T> { }             // 续传:还是泛型
SubBox<Integer> sb2 = new SubBox<>();          // 使用时才定类型
⚠️ 头号雷区:子类把 `` 弄丢了

class SubBox extends Box —— 编译器不报错(顶多给个 raw types 警告),但 Box 从此退化成裸 Object 盒子:set(Integer) 也不拦、get() 出来全要强转——安检全失效,8.1 的价值归零。这是泛型章最阴的坑:不炸,但悄悄变差。

4. 两颗雷:static 与 new T()

雷区 代码 为什么
static 成员用 T private static T shared; ✗ static 在类加载时就存在,那时 T 还没定——类的每个实例可能 T 不同,static 只有一份,没法伺候
new T() value = new T(); ✗ 类型擦除后 T 只是占位符,编译后不知道 T 是谁、有没有无参构造——要对象从外面传进来(构造方法参数)

记住结论就行:T 的活儿全靠"外面传进来",泛型类自己造不出 T 的对象。

5. 动手时间(Demo02)

import java.util.*;

// 泛型接口
interface Generator<T> {
    T next();
}

// 多参数泛型类
class Pair<K, V> {
    private K key;
    private V value;
    public Pair(K key, V value) { this.key = key; this.value = value; }
    public K getKey() { return key; }
    public V getValue() { return value; }
}

// 姿势一:定死类型
class ScoreGen implements Generator<Integer> {
    private Random r = new Random();
    @Override
    public Integer next() { return r.nextInt(101); }
}

// 姿势二:继续泛型
class Box<T> {
    private T value;
    public void set(T v) { this.value = v; }
    public T get() { return value; }
}
class SubBox<T> extends Box<T> { }

public class Demo02 {
    public static void main(String[] args) {
        // Pair:学生 → 成绩
        Pair<String, Integer> p = new Pair<>("波仔", 95);
        System.out.println(p.getKey() + " → " + p.getValue());

        // 泛型接口姿势一:定死 Integer
        Generator<Integer> g = new ScoreGen();
        System.out.println("随机分: " + g.next());

        // 姿势二:续传
        SubBox<String> sb = new SubBox<>();
        sb.set("墨池");
        System.out.println(sb.get());
    }
}

6. 30 秒实验

实验 1:class SubBox extends Box(故意丢 <T>)——编译能过但有 raw types 警告;接着 sb.set(42) 也不报错——安检悄悄失效,亲眼见最阴的坑。

实验 2:在 Pair 里写 private static K count;——编译报错 non-static type variable K cannot be referenced from a static context。static 用不了占位符。

实验 3:在 Box 里写 value = new T();——编译报错。类型擦除的代价。

实验 4:Pair<String, Integer> 上调 p.setValue("高的")——编译报错,V 的座位已被 Integer 占了,别想塞 String。

实验 5:Pair p2 = new Pair("甲", 1);(裸用)——能跑,但 p2.getKey() 出来是 Object,什么都要强转——和 8.1 万能袋子一个下场。

7. 常见误区

  1. 以为泛型类的子类必须也是泛型类——两种姿势都合法:定死类型(变普通类)或续传(保持泛型)。
  2. 子类忘写 <T>——编译不报错但安检全失效(raw types),比报错更危险。
  3. 在 static 成员里用类的占位符——static 先于 T 存在,用不了;static 方法想泛型要自己单独声明 <T>(8.1 的泛型方法)。
  4. 想在泛型类里 new T()——不行,对象从外面传进来。
  5. 以为多参数必须同名规则——Pair<K, V>、Pair<A, B> 都行,惯例用 K/V 是为了望文生义;顺序也不能乱(K 填第一个参数位)。
  6. 把泛型接口当成只能被泛型类实现——普通类定死类型照样实现(姿势一),ArrayList<String> 实现的就是泛型接口 List<E> 的定死版。

8. 折叠自测题

❓ Q1:`class A` 的 K 和 V 能是同一个类型吗?

能。Pair<String, String> 完全合法——两个占位符只是两个"座位",填同一个类型没问题。泛型不要求互不相同。

❓ Q2:泛型类继承的两种姿势分别适合什么场景?

定死类型:这个子类天生只服务一种类型(如 StringBox 只装字符串)——用起来简洁;续传:子类要继续保持灵活,让使用者决定类型——工具类、容器类几乎都选这条(ArrayList\<E> 继承 AbstractList\<E>)。

❓ Q3:`class SubBox extends Box` 为什么编译器不报错却很危险?

这叫 raw types(原始类型):编译器放行(向后兼容老代码),只给警告。但占位符没交代,Box 退化成 Object 盒子——set 什么都能塞、get 全要强转,8.1 的安检价值归零,错误重新推迟到运行时。

❓ Q4:为什么 static 方法不能用类上声明的 T?

static 成员随类加载就存在,那时占位符 T 还没被任何具体类型替换;而且每个 Box<String>、Box<Integer> 共享同一个 static——它没法同时是 String 又是 Integer。static 方法想泛型,自己声明 <T>。

❓ Q5:仿照 Pair,写一个三元组 Triple\ 的骨架。
class Triple<A, B, C> {
    private A first; private B second; private C third;
    public Triple(A a, B b, C c) { first=a; second=b; third=c; }
    public A getFirst() { return first; }
    public B getSecond() { return second; }
    public C getThird() { return third; }
}

要点:占位符数量随便加,逗号隔开;getter 返回值各归各位。

9. 下一步学什么

相关笔记