本节摘要:泛型给类与方法加上类型参数,让集合只收指定类型、取值免强转,错误从运行期提前到编译期。类型擦除是它的实现方式:编译后类型参数被抹掉,因此运行时拿不到泛型信息,并带来三条禁令——泛型类型不能直接 new、不能 instanceOf 判断、同泛型不同参数不算不同类。通配符解决"参数化类型之间的赋值兼容"问题。本节对照擦除前后行为并逐条验证禁令。
泛型出现之前,集合收的是 Object——什么都能装,代价是取出来必须强制转换,转错类型就是运行时崩溃:
import java.util.ArrayList; import java.util.List; public class RawDemo { public static void main(String[] args) { List box = new ArrayList(); // 原始类型:没有类型栏的登记表 box.add("登记记录"); box.add(42); // 整数也能塞进去 编译器不管 String s = (String) box.get(0); // 取出必须强转 System.out.println(s); // 输出:登记记录 String bad = (String) box.get(1); // 编译能过 运行时 ClassCastException System.out.println(bad); // 这行执行不到 } }
问题出在错误暴露的时机:塞错元素的那一行和崩溃的那一行隔着任意远的距离,排查时看到的是崩溃点的堆栈,而不是真正的犯错点。泛型把类型栏加在登记表上:
import java.util.ArrayList; import java.util.List; public class GenericDemo { public static void main(String[] args) { List<String> box = new ArrayList<>(); // 类型栏:只收 String box.add("登记记录"); // box.add(42); // 编译报错:不兼容的类型 —— 在犯错点当场拦截 String s = box.get(0); // 取出免强转 类型栏已声明 System.out.println(s); // 输出:登记记录 } }
对比一目了然:塞错元素的行从"运行时炸在别处"变成"编译时红在当场",取出从"强转赌运气"变成"声明即类型"。菱形写法 new ArrayList<>() 里尖括号留空,编译器从左边推断——这是日常标准姿势。
类型参数不止能用在类上,方法也能带:泛型方法在自己的签名里声明类型参数,调用时按实参自动推断,常见于"参数是什么类型就返回什么类型"的工具方法。类型参数的习惯命名是单个大写字母:T 表类型、E 表元素、K 与 V 表键值、R 表返回。
泛型的实现方式不是把每种参数化类型都生成一份类,而是编译期检查加运行期擦除:编译器用类型栏做完全部检查,然后把栏位擦掉——编译产物里 List 与 List 是同一个类。这个设计换来的是与旧代码的兼容(不用泛型的老代码照样跑),代价是运行时泛型信息不存在:
import java.util.ArrayList; import java.util.List; public class ErasureDemo { public static void main(String[] args) { List<String> a = new ArrayList<>(); List<Integer> b = new ArrayList<>(); System.out.println(a.getClass() == b.getClass()); // 输出:true 同一个类 // if (a instanceof List<String>) { } // 编译报错:无法对参数化类型做判断 // List<String> c = new ArrayList<String>() 的运行时对象里查不到 String // 禁令一:不能直接创建泛型类型的实例 // T t = new T(); // 编译报错 擦除后 T 未知 // 禁令二:不能创建泛型数组 // List<String>[] arr = new List<String>[10]; // 编译报错 // 禁令三:静态上下文不能用类的类型参数 // static T shared; // 编译报错 静态成员属于类 参数属于对象 System.out.println("三条禁令均以注释形式验证 编译期逐一拦截"); } }

三禁令的共同根源都是"运行时没有类型栏":new T 需要知道造什么类、instanceof 需要运行时比对类型、泛型数组要保证元素类型可查——擦除之后统统无从谈起。绕行手段是存在的:想造 T 的实例,让调用方把类型对应的构造器或工厂对象传进来;想要运行时类型信息,传 Class 对象进来(第 8 章反射正是这套思路的全面展开)。
泛型没有"继承传递":String 是 Object 的子类,但 List 不是 List 的子类——如果算,就能通过后者把整数塞进一个字符串列表,类型栏形同虚设。于是有了通配符:
import java.util.ArrayList; import java.util.List; public class WildDemo { static void printAll(List<?> list) { // 无限定:任何列表都能传 只读 for (Object o : list) System.out.print(o + " "); System.out.println(); } static double total(List<? extends Number> nums) { // 上界:Number 家族均可 能读 double sum = 0; for (Number n : nums) sum += n.doubleValue(); // 按上界读 安全 // nums.add(1); // 编译报错:不知道确切类型 不许写入 return sum; } public static void main(String[] args) { List<String> names = List.of("登记", "变更", "注销"); List<Integer> ints = List.of(1, 2, 3); List<Double> doubles = List.of(1.5, 2.5); printAll(names); // 输出:登记 变更 注销 printAll(ints); // 输出:1 2 3 System.out.println(total(ints)); // 输出:6.0 System.out.println(total(doubles)); // 输出:4.0 } }
记忆口诀:** extends 是生产者(读)、super 是消费者(写)**——从列表取数据用上界、往列表放数据用下界、又读又写就别用通配符(老老实实用确切类型)。日常业务代码里通配符出现频率不高,读懂标准库与框架签名是它的主要用途,比如排序工具的参数签名里就常能见到它们。
工具房二楼(集合)至此逛完。下一章进最后一间工具房:数据怎么离开内存——字节流与字符流的传送带、缓冲与编码的提速带、文件与序列化的档案柜,以及盖时间戳的新窗口。