泛型 = 编译期安检:把 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. 三条铁规矩
- 泛型只能装引用类型——
Box<int>编译报错,要写Box<Integer>(7.1 集合铁规矩的根子,自动装箱顶上)。 - 泛型不存在继承传递——
Box<Object>不是Box<String>的父类!虽然 String 是 Object 的子类,但两个盒子各管各的(4.1 继承在泛型这儿不通用,进阶的?通配符以后再讲)。 - 类型擦除——泛型只活在编译期,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. 常见误区
- 以为
Box<Object>能装Box<String>——泛型不传递继承;想"都能装"要用通配符?(进阶)。 Box<int>写基本类型——只能引用类型,基本类型用包装类(6.1)。- 以为泛型是运行时机制——类型擦除:编译完就没了,只在编译期站岗。
- 泛型方法把
<T>写在返回类型后面——static T pick(List<T>)里那个 T 编译器不认识;<T>必须在返回类型前。 - 以为钻石
<>是新语法糖随便省——右边new Box<>()可省,左边Box<String>的声明类型不能省,省了就退化成裸 Box。
9. 折叠自测题
把类型错误从运行时提前到编译期——不用强转 + 错误在敲代码时就暴露。运行时炸一次的代价 >> 编译红叉一次。
类型参数(占位符),代表"使用时才确定的类型"。能叫任何合法标识符,惯例是单个大写字母(T/E/K/V/R)——看到大写字母占位就知道是泛型。
泛型没有继承传递:String 是 Object 的子类,但 Box<String> 不是 Box<Object> 的子类——两个盒子是平级的独立类型。这是 4.1 继承规则在泛型上的例外。
泛型只能装引用类型(占位符擦除后统一按 Object 处理,基本类型不是对象)。想装基本类型就用包装类,set(42) 自动装箱。
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> 不行的解药——自动装箱