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

泛型 = 编译期安检:把 ClassCastException 从"运行时炸"提前到"敲代码时就报错"。7.1 里你早就在用 List<String>——现在正式弄清 < > 里面到底是谁、自己怎么写一个。

1. 先看没泛型的翻车现场

泛型出现之前的"万能袋子"(7.1 讲过的旧时代写法):

List list = new ArrayList();     // 不贴标签,什么都收
list.add("波仔");
list.add(42);                    // Integer 也进来了

String s = (String) list.get(1); // 编译通过,运行时炸!
// ClassCastException —— 4.4 集齐的三大新手异常之一

问题根源:不贴标签的袋子什么都装,取出来一律当 Object 处理,用之前全靠你强转——转对了是运气,转错了运行时才炸。

2. 泛型的解法:贴标签

List<String> list = new ArrayList<>();   // 袋子上贴好标签:只装 String
list.add("波仔");
list.add(42);          // ← 编译器当场红叉:String 的袋子塞不进 Integer

String s = list.get(0); // 取出来就是 String,强转全省
💡 核心价值一句话

把错误从运行时提前到编译期。和 5.2 的精神一脉相承:编译器拦下的错最便宜——改一秒,好过程序跑到一半炸一次。

3. 自己写一个泛型类

<T> 里的 T 叫类型参数(占位符)——先占个坑,装什么类型由使用的人说了算:

public class Box<T> {              // T = 占位符
    private T value;               // 字段类型是 T

    public void set(T v) { this.value = v; }
    public T get()         { return value; }
}

使用侧:

Box<String> b1 = new Box<>();      // 钻石操作符 <>:右边类型编译器自己推
b1.set("波仔");
String s = b1.get();               // 出来就是 String

Box<Integer> b2 = new Box<>();     // 同一个类,换个类型照样用
b2.set(42);

一个类管所有类型——这就是"泛"字的含义:不针对某个具体类型写死,而是一份代码通吃。

命名惯例(大写一个字母)

占位符 含义 你在哪见过
T Type 类型 Box\<T>
E Element 元素 List\<E>、Set\<E>
K / V Key / Value Map\<K, V>
R Result 返回值 map 方法的返回类型

看到尖括号里的大写字母,就认出这是占位符——它不是真实的类名。

4. 泛型方法(会认 + 会写简单版)

public static <T> T pickFirst(List<T> list) {   // <T> 写在返回类型前
    return list.isEmpty() ? null : list.get(0);
}

// 调用:类型自动推断,不用写 <String>
String first = pickFirst(List.of("甲", "乙"));

规矩:<T> 要写在返回类型前面——这是告诉编译器"我这个方法是泛型的"。Collections.max、Arrays.asList 内部全是泛型方法。

5. 三条铁规矩

  1. 泛型只能装引用类型——Box<int> 编译报错,要写 Box<Integer>(7.1 集合铁规矩的根子,自动装箱顶上)。
  2. 泛型不存在继承传递——Box<Object> 不是 Box<String> 的父类!虽然 String 是 Object 的子类,但两个盒子各管各的(4.1 继承在泛型这儿不通用,进阶的 ? 通配符以后再讲)。
  3. 类型擦除——泛型只活在编译期,class 文件里 <String> 就消失了(会认即可:所以反射拿到的还是裸 Box,这也是"运行时炸 ClassCastException"和"编译期安检"能并存的原理)。

6. 动手时间(Demo01)

import java.util.*;

public class Demo01 {
    // 泛型方法:挑第一个
    public static <T> T pickFirst(List<T> list) {
        return list.isEmpty() ? null : list.get(0);
    }

    public static void main(String[] args) {
        // 翻车现场复刻(注释掉才编译得过)
        List raw = new ArrayList();
        raw.add("波仔");
        raw.add(42);
        // String s = (String) raw.get(1);   // 放开就运行时炸 CCE

        // 泛型版
        List<String> list = new ArrayList<>();
        list.add("波仔");
        // list.add(42);   // ← 放开就是编译红叉,好过运行时炸

        // 自己的泛型类
        Box<String> b1 = new Box<>();
        b1.set("墨池");
        Box<Integer> b2 = new Box<>();
        b2.set(2026);
        System.out.println(b1.get() + " " + b2.get());

        // 泛型方法
        System.out.println(pickFirst(list));
    }
}

7. 30 秒实验

实验 1:List<String> 上 add(42)——编译红叉 incompatible types。安检当场体验。

实验 2:注释掉泛型(裸 List),强转 get(1) 为 String——编译安静,运行炸 ClassCastException。两相对比,泛型的价值一目了然。

实验 3:Box<Object> box = new Box<String>();——编译报错!泛型没有继承传递,4.1 的直觉在这儿失灵。

实验 4:Box<int> ——编译报错 unexpected type;换 Box<Integer> 好使,set(42) 自动装箱。

实验 5:把 pickFirst 的 <T> 删掉——编译报错"cannot resolve symbol T"。占位符必须先声明才能用。

8. 常见误区

  1. 以为 Box<Object> 能装 Box<String>——泛型不传递继承;想"都能装"要用通配符 ?(进阶)。
  2. Box<int> 写基本类型——只能引用类型,基本类型用包装类(6.1)。
  3. 以为泛型是运行时机制——类型擦除:编译完就没了,只在编译期站岗。
  4. 泛型方法把 <T> 写在返回类型后面——static T pick(List<T>) 里那个 T 编译器不认识;<T> 必须在返回类型前。
  5. 以为钻石 <> 是新语法糖随便省——右边 new Box<>() 可省,左边 Box<String> 的声明类型不能省,省了就退化成裸 Box。

9. 折叠自测题

❓ Q1:泛型最大的价值是什么?

把类型错误从运行时提前到编译期——不用强转 + 错误在敲代码时就暴露。运行时炸一次的代价 >> 编译红叉一次。

❓ Q2:`class Box` 里的 T 是什么?能叫别的吗?

类型参数(占位符),代表"使用时才确定的类型"。能叫任何合法标识符,惯例是单个大写字母(T/E/K/V/R)——看到大写字母占位就知道是泛型。

❓ Q3:`Box b = new Box()` 为什么编译报错?

泛型没有继承传递:String 是 Object 的子类,但 Box<String> 不是 Box<Object> 的子类——两个盒子是平级的独立类型。这是 4.1 继承规则在泛型上的例外。

❓ Q4:为什么 `Box` 不行而 `Box` 行?

泛型只能装引用类型(占位符擦除后统一按 Object 处理,基本类型不是对象)。想装基本类型就用包装类,set(42) 自动装箱。

❓ Q5:写一个泛型方法 `swap`,交换 List 中 i、j 两个位置的元素。

public static <T> void swap(List<T> list, int i, int j) { T tmp = list.get(i); list.set(i, list.get(j)); list.set(j, tmp); } 要点:<T> 写在 void 前;中间变量也得用 T——三个位置的类型必须是同一个占位符。

10. 下一步学什么

  • ☑ 8.2 泛型类 ✅ —— 点此复习:多参数 Pair\<K, V>(Map 骨架认亲)、泛型接口、继承两姿势 + raw types 失安检的头号雷区。

相关笔记

  • 第七章 集合/7.1 集合概述:泛型的第一线使用现场——<String> 会认版
  • 第四章 面向对象(下)/4.4 多态:ClassCastException 的出生地(instanceof 向下转型)
  • 第五章 异常/5.2 运行时异常与编译时异常:编译器两种态度——泛型是"编译时拦截"的代言人
  • 第六章 Java API/6.1 包装类:Box\<int> 不行的解药——自动装箱