为什么需要泛型
泛型类型让你只写一次代码,就能在多种类型上复用,同时不放弃类型安全。你不必分别写 IntBox、StringBox 和 UserBox,只需写一个 Box<T>,其中 T 是由调用者填入的占位符。
每次你写 ArrayList<String>,或者像 HashMap<String, Integer> 这样的 HashMap 时,其实都已经在用泛型了。<...> 这部分就是类型实参。本页讲解如何编写你自己的泛型。
另一种做法是把所有东西都存成 Object,但这会丢掉类型信息,还会逼你写出难看又容易出错的强制转换:
最后一行的强制转换会在运行时抛出 ClassCastException,而泛型的设计目标,正是让这类异常根本不可能发生。
泛型类
在类名后面的尖括号里声明类型参数。按照惯例,它是一个大写字母:T 表示“type”(类型),E 表示“element”(元素),K/V 表示键和值。
在 Box 内部,每个 T 都会变成调用者提供的类型。Box<String> 是一个只能存放和返回 String 的盒子。程序还没运行,编译器就会拒绝 name.set(99)。
右边空的 <>(菱形运算符)让编译器根据左边推断类型实参,你就不用把 <String> 写两遍。
泛型方法
一个方法可以拥有自己的类型参数,与所在的类无关。把参数 <T> 写在返回类型之前:
你从来不需要显式传入 T,编译器会根据实参推断出来。Collections.sort 或 List.of 这样的工具方法,正是靠泛型方法在任何元素类型上都保持类型安全。
有界类型参数
有时泛型类型只对某些类型才有意义。extends 可以约束类型参数,让你能调用上界类型的方法。这里 T extends Number 表示 T 是 Number 或它的任何子类(Integer、Double 等),所以可以调用 doubleValue():
注意这里的 extends 意思是“是……的子类型”,对类和接口都适用。需要比较元素时,<T extends Comparable<T>> 是极其常见的写法。
通配符:? extends 和 ? super
一个不太明显的陷阱:List<Integer> 不是 List<Number>,尽管 Integer 是 Number。泛型是不变的(invariant)。当你只需要读取或只需要写入时,通配符可以放宽这个限制。
对于你从中读取的生产者,用 ? extends T;对于你向其中写入的消费者,用 ? super T(这就是“PECS”原则:Producer Extends, Consumer Super):
? extends Number 列表允许你把元素当作 Number 读取,但不能向其中添加元素(编译器无法知道确切的元素类型)。? super Integer 列表允许你添加 Integer,但读出来的是 Object。根据数据的流向选择合适的通配符。
类型擦除及其限制
泛型是编译时特性。编译之后类型参数会被擦除:在运行时,Box<String> 和 Box<Integer> 都只是 Box。这让泛型能与旧代码保持向后兼容,但也带来了实实在在的限制。
// None of these compile - the type parameter doesn't exist at runtime:
T value = new T(); // can't instantiate a type parameter
T[] array = new T[10]; // can't create a generic array
if (list instanceof List<String>) { } // can't test the type argument
由于类型在运行时已经不存在,你无法通过反射问出“T 原来是什么”,也不能重载只在泛型实参上不同的方法(foo(List<String>) 和 foo(List<Integer>) 擦除后是同一个签名)。如果你确实需要在运行时知道类型,就把一个 Class<T> 令牌作为构造函数或方法的参数传进去。
下一篇:Lambda 表达式
你已经看到,泛型方法可以把类型作为参数。下一步是把行为作为参数。Lambda 表达式让你把一段代码(一个函数)传给方法,这正是你对刚学会类型安全地声明的泛型集合进行排序、过滤和转换的方式。
常见问题
Java 中的泛型是什么?
泛型让你编写的类或方法可以处理一个之后才指定的类型,而不是被锁死在某一个具体类型上。你在尖括号里声明类型参数,比如 class Box<T>,调用者再填入实际类型,比如 Box<String>。之后编译器会在所有地方强制检查这个类型,所以类型不匹配会在编译时被发现,也省去了手动强制转换。
为什么要用泛型而不是 Object?
使用 Object 会丢失所有类型信息:编译器无法阻止你放进错误的东西,取出的每个值也都要强制转换(运行时可能抛出 ClassCastException)。泛型把这些检查提前到编译时。List<String> 根本不接受 Integer,而 get() 直接返回 String:不用转换,运行时也不会有意外。
Java 泛型中的类型擦除是什么?
类型擦除是指泛型的类型信息只存在于编译时。编译之后,List<String> 和 List<Integer> 在运行时都只是 List,类型参数被擦除了。所以你不能写 new T[10],不能写 list instanceof List<String>,也不能通过反射读取类型参数。泛型给你的是编译时的安全,而不是运行时的类型数据。