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

8.2 用 Generator<T> 认过泛型接口的脸;这节正式展开:T 在接口里的两个岗位(生产者/消费者)、实现的两种姿势怎么选、泛型接口当类型用(多态主场),最后掀开 JDK 的底牌——Comparable、Iterator 全是泛型接口,你天天在用,只是今天才看清身份证。

1. 先认门:带占位符的合同

8.1 说过:泛型类是"装东西的盒子";泛型接口则是"带占位符的合同"——合同里承诺"我提供的服务跟 T 打交道",但 T 是谁,留给实现的人拍板。

public interface Generator<T> {   // 合同:我能生产 T
    T next();                     // 每次要一个 T 类型的东西
}

三条基本规矩(都是 4.3 接口规矩 + 8.1 占位符规矩的组合,没有新语法):

规矩 原因
接口不能 new 接口没有构造方法——new Generator<String>() 编译报错,必须有实现类
方法体留空,只留签名 和普通接口一样,泛型只改变"类型怎么写",不改变"接口怎么用"
8.2 的两颗雷原样生效 实现类的 static 用不了 T;接口里也不能 new T()——T 的活儿全靠外面传

2. T 的两个岗位:生产者与消费者

T 在接口方法签名里能站两个岗位,看它出现在哪一边:

岗位 长相 谁定 T 白话
生产者 T next() 接口的实现类给你 T "我往外发货,货是 T 型的"
消费者 void fill(T item) 调用的人塞 T 进去 "我往里收货,只收 T 型的"
interface Tank<T> {
    T take();              // 生产者:从罐里拿一个 T
    void put(T item);      // 消费者:往罐里塞一个 T
}
💡 为什么分这两个岗位?

因为它决定安检往哪边拦:生产者岗位——你拿出来的东西保证是 T(不用强转);消费者岗位——你想塞个别的进去,编译器当场拦下。同一个 T,两个方向,两道安检。

3. 实现的两种姿势:决定权链讲透

8.2 见过写法,这里讲透决定权在谁手里:

姿势 写法 决定权 适合场景
当场拍板 class StrGen implements Generator<String> 实现类自己定死 这个实现类天生只服务一种类型
继续传下去 class UserGen<T> implements Generator<T> 推给使用者 实现类想保持通用(工具类都选这条)

三个细节,一个比一个阴:

  1. 姿势二的 <T> 可以改名:class UserGen<E> implements Generator<E> 完全合法——名字只是代号,两边对上号就行。惯例上写同名 T 是为了望文生义。
  2. 忘传类型 = raw types(8.2 头号雷区同款):class BadGen implements Generator 编译不报错,但 next() 出来是 Object,安检归零。
  3. 拍板拍错位:implements Generator<String> 后,实现类里必须 public String next()——写 Object next() 也不行(合同签的是 String,交货就不能缩水)。

4. 泛型接口当类型用:多态主场

接口最大的价值从来不是"被实现",而是当类型声明变量、参数、返回值——4.4 的"编译看左"直接适用:

// 左边写合同,右边换实现——实现类随便换,这行不用改
Generator<String> g = new StrGen();
String s = g.next();          // 拿出来直接是 String,免强转

// 泛型接口当方法参数:谁实现 Generator<String> 谁就能传进来
public static void printThree(Generator<String> gen) {
    System.out.println(gen.next() + " " + gen.next() + " " + gen.next());
}

// 当返回值:方法内部用哪个实现,调用方不关心(工厂味道)
public static Generator<Integer> scoreGen() {
    return new ScoreGen();
}
💡 收网:4.3 合同 + 4.4 多态 + 8.1 占位符

Generator<String> g = new StrGen() 这一行,左边是 4.3 的合同思想,右边是 4.4 的多态(可以换实现),尖括号是 8.1 的占位符——三课合体,一个不少。

5. JDK 真身收网:你早就在用泛型接口

以为泛型接口很学术?掀开 JDK 的底牌——这四个你全都打过交道:

泛型接口 方法 你在哪见过
Comparable<T> int compareTo(T o) 7.7 sort 能排 String/Integer,靠的就是它(内置比较合同)
Comparator<T> int compare(T a, T b) 4.7 Lambda 排序实战的参数类型
Iterable<T> Iterator<T> iterator() 增强 for 的资格证——Collection 实现了它,所以能被冒号遍历
Iterator<E> hasNext() / next() 7.2/7.4 迭代器的本体

头号收网:Comparable<T> 解 7.7 的钩子。 7.7 说过"自定义类直接 sort 会炸 ClassCastException,进阶留后"——解药就是这节:

class Person implements Comparable<Person> {     // 实现 JDK 的泛型接口
    String name; int score;
    public Person(String name, int score) { this.name = name; this.score = score; }

    @Override
    public int compareTo(Person o) {             // 自己跟别人比:按成绩
        return Integer.compare(this.score, o.score);   // 负=我小 0=相等 正=我大
    }
}

// 之后 Collections.sort / Arrays.sort 直接排,不再炸
Collections.sort(persons);

compareTo 的返回值口诀:我小返负、相等返零、我大返正——排序器只看正负号和零。Integer.compare(a, b) 是标准写法,别手写 this.score - o.score(大数相减会溢出)。

6. 动手时间(Demo03)

import java.util.*;

// 泛型接口:生产者合同
interface Generator<T> {
    T next();
}

// 姿势一:当场拍板——随机学号生成器
class IdGen implements Generator<String> {
    private Random r = new Random();
    @Override
    public String next() {
        return "学号" + (1000 + r.nextInt(9000));
    }
}

// 姿势二:继续传下去
class PairGen<T> implements Generator<T> {
    private T item;
    public void set(T item) { this.item = item; }
    @Override
    public T next() { return item; }   // 简化版:反复给同一个
}

// 头号收网:自定义类实现 JDK 的 Comparable
class Student implements Comparable<Student> {
    String name; int score;
    public Student(String name, int score) { this.name = name; this.score = score; }

    @Override
    public int compareTo(Student o) {
        return Integer.compare(this.score, o.score);
    }

    @Override
    public String toString() { return name + ":" + score; }
}

public class Demo03 {
    public static void main(String[] args) {
        // 泛型接口当类型用:左边合同,右边实现
        Generator<String> g = new IdGen();
        System.out.println(g.next() + " / " + g.next());

        // 多态:方法参数收实现类
        print(new IdGen());

        // 7.7 收网:自定义类排序不再炸
        List<Student> list = new ArrayList<>();
        list.add(new Student("波仔", 88));
        list.add(new Student("墨池", 95));
        list.add(new Student("AI", 76));
        Collections.sort(list);
        System.out.println("按成绩升序: " + list);
    }

    static void print(Generator<String> gen) {
        System.out.println("出货: " + gen.next());
    }
}

7. 30 秒实验

实验 1:Generator<String> g = new Generator<>();——编译报错 Cannot instantiate the type Generator<String>。接口不能 new,亲眼看。

实验 2:把 IdGen 的声明改成 class IdGen implements Generator(忘传类型)——编译能过但有 raw types 警告,main 里 g.next() 出来的东西调 toUpperCase() 要强转——安检归零现场。

实验 3:IdGen 里把 public String next() 改成 public Object next()——编译报错 The return type is incompatible with Generator<String>。合同签了 String,交货不能缩水。

实验 4:把 Student 的 implements Comparable<Student> 整行删掉——Collections.sort(list) 当场炸 ClassCastException。7.7 的钩子亲眼收网。

实验 5:compareTo 里改成 return 0;——排序"成功"但顺序纹丝不动(排序器认为全都相等)。体会返回值就是排序的全部依据。

8. 常见误区

  1. new Generator<String>()——接口没有构造方法,永远不能 new;要的是实现类的对象。
  2. 实现类忘传类型参数——raw types 不报错只警告,但安检全失效;8.2 的雷,接口这边再踩一遍。
  3. 以为实现类的 T 和接口的 T 是两个变量——姿势二里 class UserGen<E> implements Generator<E>,E 和接口那格对上号就是同一个占位符;名字可换,位置不能错。
  4. Comparable 写成裸接口——implements Comparable(不写 <Student>)时 compareTo 参数退化成 Object,方法体里什么都要强转——泛型白学了。
  5. compareTo 手写 this.score - o.score——负零正的思路对,但大数相减会溢出(Integer.MAX_VALUE - (-1) 直接翻车),标准写法 Integer.compare(a, b)。
  6. 以为泛型接口必须配泛型实现类——姿势一定死类型后,实现类就是普通类,new IdGen() 不用写尖括号。

9. 折叠自测题

❓ Q1:`T next()` 和 `void put(T item)` 里,T 分别是生产者还是消费者?

T next()——T 在返回值位,生产者:实现类往外发货;put(T item)——T 在参数位,消费者:调用方往里塞货。看 T 站在箭头的哪一头。

❓ Q2:姿势一和姿势二的核心区别是什么?

T 的决定权在谁手里。姿势一实现类当场拍板(implements Generator<String>),子类变普通类;姿势二实现类续传(class UserGen<T> implements Generator<T>),决定权推给使用者。工具类、容器类几乎都选姿势二。

❓ Q3:`class BadGen implements Generator`(忘传类型)编译器为什么不报错?

向后兼容老代码——Java 5 之前都是这么写的(raw types)。编译器只给警告不拦截,但占位符没交代,Generator 退化成 Object 合同,next() 出来全是 Object。不炸但悄悄变差,比报错更阴。

❓ Q4:为什么 `ArrayList` 能被增强 for 遍历?

Iterable<T> 的功劳:Collection 接口继承了 Iterable<E>,而 Iterable 的合同就是"提供 iterator()"——增强 for 的底层正是拿这个迭代器干活(7.4 的三步舞)。所以"能被冒号遍历"不是魔法,是实现了一份泛型接口合同。

❓ Q5:让 Person 按"成绩降序"排,compareTo 怎么写?

return Integer.compare(o.score, this.score);——参数顺序对调,负零正的含义反过来,降序达成。也可以排完再 Collections.reverse(list),但考试写"对调参数"更见功力。

10. 下一步学什么

相关笔记