学习路线:
-
为什么要有泛型
-
泛型类:class Box
-
泛型方法:public static T getMiddle(T... values)
-
类型变量的限定:<T extends Comparable>
-
泛型代码和虚拟机:类型擦除
-
泛型的限制:不能 new T()、不能创建泛型数组等
-
通配符:? extends 和 ? super
-
反射与泛型
0. 为什么要有泛型
没有泛型时:
java
ArrayList list = new ArrayList();
list.add("Alice");
String name = (String) list.get(0);
问题是:
- 取元素时需要强制类型转换。
- 如果放错类型,编译器不一定能发现。
- 错误可能拖到运行期才爆发。
有了泛型:
java
ArrayList<String> list = new ArrayList<>();
list.add("Alice");
String name = list.get(0);
这时:
- list 只能放 String
- get 出来的结果自动就是 String
- 编译器会帮你检查类型错误
如果你写:
java
list.add(123);
编译器直接报错。
1. 泛型类
最简单的泛型类
java
public class Box<T> {
private T value;
public void set(T value) {
this.value = value;
}
public T get() {
return value;
}
}
这里的 T 是类型参数。创建对象时再决定它具体是什么类型:
java
Box<String> stringBox = new Box<>();
stringBox.set("hello");
String s = stringBox.get();
泛型类可以有多个类型参数
java
public class Pair<K, V> {
private K key;
private V value;
public Pair(K key, V value) {
this.key = key;
this.value = value;
}
public K getKey() {
return key;
}
public V getValue() {
return value;
}
}
使用:
java
Pair<String, Integer> score = new Pair<>("Alice", 95);
String name = score.getKey();
Integer points = score.getValue();
2. 泛型方法
泛型方法是方法自己带类型参数:
java
public static <T> T getFirst(T[] array) {
return array[0];
}
调用时:
java
String[] names = {"Alice", "Bob"};
String firstName = getFirst(names);
Integer[] numbers = {1, 2, 3};
Integer firstNumber = getFirst(numbers);
注意这里的 写在返回类型前面:
java
public static <T> T getFirst(T[] array)
第一个 是声明类型参数,后面的 T 才是返回类型。如果没有前面的 ,编译器不知道 T 是什么。
泛型方法不一定在泛型类里
这是一个容易误解的地方。
普通类里也可以有泛型方法:
java
public class ArrayUtil {
public static <T> T getFirst(T[] array) {
return array[0];
}
}
ArrayUtil 本身不是泛型类,但 getFirst 是泛型方法。
反过来,泛型类里的普通方法不一定是泛型方法:
java
public class Box<T> {
private T value;
public T get() {
return value;
}
}
这里 get() 使用了类上的 T,但它自己没有声明新的类型参数,所以它不是"泛型方法",而是"泛型类中的普通方法"。
3. 类型变量的限定
有时候,泛型不能太自由。
比如你要写一个方法,找出数组中的最小值:
java
public static <T> T min(T[] values) {
// 怎么比较两个 T?
}
问题来了:不是所有对象都能比较大小。
所以要限制 T 必须实现 Comparable:
java
public static <T extends Comparable<T>> T min(T[] values) {
T smallest = values[0];
for (T value : values) {
if (value.compareTo(smallest) < 0) {
smallest = value;
}
}
return smallest;
}
这里:
java
<T extends Comparable<T>>
意思是:T 必须是实现了 Comparable 的类型。
比如 String、Integer 都可以比较,所以能用:
java
String[] words = {"apple", "orange", "banana"};
String minWord = min(words);
为什么用 extends,不用 implements
哪怕后面跟的是接口,也写 extends:
java
<T extends Comparable<T>>
而不是:
java
<T implements Comparable<T>> // 错
在泛型限定里,extends 统一表示"是某个类型的子类型":
多个限定
一个类型变量可以有多个限定:
java
<T extends Comparable<T> & Serializable>
意思是:T 必须既能比较,又能序列化
如果同时有类和接口,类必须放在第一个:
java
<T extends Employee & Comparable<T> & Serializable>
不能写成:
java
<T extends Comparable<T> & Employee> // 错
因为 Java 的泛型擦除机制需要用第一个限定作为主要擦除类型。这个先记结论就好,后面讲类型擦除时会更清楚
限定之后能做什么
没有限定时:
java
public static <T> void test(T value) {
value.compareTo(...); // 不行
}
因为 T 只能当成 Object 用。
有了限定:
java
public static <T extends Comparable<T>> void test(T value, T other) {
value.compareTo(other); // 可以
}
编译器知道 value 至少是一个 Comparable。
再比如:
java
public static <T extends Number> double sum(T[] values) {
double result = 0;
for (T value : values) {
result += value.doubleValue();
}
return result;
}
因为 T extends Number,所以可以调用:
java
value.doubleValue()
限定不等于继承泛型类
这一点也容易混。
java
class Box<T extends Number> {
private T value;
}
这表示:Box 里面的 T 只能是 Number 或 Number 的子类
所以可以:
java
Box<Integer> a;
Box<Double> b;
Box<Number> c;
不可以:
java
Box<String> d; // 编译错误
但它不表示 Box 是 Box 的子类。
也就是说:
java
Box<Integer> intBox = new Box<>();
Box<Number> numberBox = intBox; // 错
这是泛型里非常重要的一条:
即使 Integer 是 Number 的子类,Box 也不是 Box 的子类。
4. 类型擦除
类型擦除是什么
Java 的泛型主要存在于编译期。编译器会检查类型是否安全,但生成字节码时,会把很多泛型类型信息"擦掉"。
比如你写:
java
public class Pair<T> {
private T first;
private T second;
public T getFirst() {
return first;
}
public void setFirst(T first) {
this.first = first;
}
}
如果 T 没有限定,编译后大致会变成:
java
public class Pair {
private Object first;
private Object second;
public Object getFirst() {
return first;
}
public void setFirst(Object first) {
this.first = first;
}
}
也就是说:T 被擦除成 Object
所以:
java
Pair<String>
Pair<Integer>
Pair<Employee>
在运行时本质上都是同一个类:Pair
这就是为什么下面代码输出通常是 true:
java
Pair<String> p1 = new Pair<>();
Pair<Integer> p2 = new Pair<>();
System.out.println(p1.getClass() == p2.getClass());
因为运行时只有一个 Pair.class。
4.1 有上界时擦除成上界
如果类型变量有限定:
java
public class Interval<T extends Comparable<T>> {
private T lower;
private T upper;
public T getLower() {
return lower;
}
}
T 不会擦除成 Object,而是擦除成它的第一个限定:
java
public class Interval {
private Comparable lower;
private Comparable upper;
public Comparable getLower() {
return lower;
}
}
如果是多个限定:
java
<T extends Employee & Comparable<T> & Serializable>
擦除成第一个限定:Employee
这也是为什么多个限定中,类要放在最前面。
4.2 编译器会插入强制类型转换
你写:
java
Pair<String> pair = new Pair<>();
String s = pair.getFirst();
擦除后 getFirst() 实际返回的是 Object。
那为什么能赋给 String?
因为编译器帮你插入了强制类型转换:
java
String s = (String) pair.getFirst();
所以泛型不是让 JVM 真正生成一个 Pair 类,而是:
编译期检查类型安全
擦除泛型信息
必要时插入类型转换
4.3 桥方法
类型擦除还有一个比较经典的副作用:桥方法。
看这个例子:
java
class Pair<T> {
public void setSecond(T second) {
// ...
}
}
class DateInterval extends Pair<LocalDate> {
@Override
public void setSecond(LocalDate second) {
// ...
}
}
泛型擦除后,父类方法大致变成:
java
public void setSecond(Object second)
子类方法是:
java
public void setSecond(LocalDate second)
问题来了:这两个方法参数不同,按普通规则不算重写。
但从源码语义上,DateInterval 明明是在重写 Pair 的 setSecond。
为了保持多态,编译器会在子类里生成一个桥方法:
java
public void setSecond(Object second) {
setSecond((LocalDate) second);
}
这样通过父类引用调用时,多态仍然成立:
java
Pair<LocalDate> pair = new DateInterval();
pair.setSecond(LocalDate.now());
桥方法是编译器自动生成的,平时你不用手写,但它解释了为什么类型擦除后泛型方法仍能保持多态。
4.4 不能在运行时判断具体泛型参数
因为类型擦除,所以不能这样:
java
if (pair instanceof Pair<String>) {
// 错
}
只能这样:
java
if (pair instanceof Pair) {
// 可以
}
同理,运行时也不能区分:
java
ArrayList<String>
ArrayList<Integer>
它们都是:ArrayList
所以这类代码不可靠,也不允许
java
list instanceof ArrayList<String>
不能 new T()
很多人初学泛型会想写:
java
public class Box<T> {
public T create() {
return new T(); // 错
}
}
为什么不行?
因为运行时 T 已经被擦除了。JVM 不知道你想创建的是 String、Employee 还是别的类型。
如果需要创建对象,通常要传入构造器、Class 或工厂:
java
public class Box<T> {
private Supplier<T> supplier;
public Box(Supplier<T> supplier) {
this.supplier = supplier;
}
public T create() {
return supplier.get();
}
}
使用:
java
Box<Employee> box = new Box<>(Employee::new);
Employee e = box.create();
一句话总结类型擦除
Java 泛型是编译期的类型安全机制;运行时大多数泛型类型参数会被擦除成 Object 或上界类型。
5. 泛型的限制与局限性
这些限制看起来零散,但根源大多都是一个:类型擦除。也就是运行时 JVM 通常不知道 T 到底是什么。
- 不能用基本类型作为类型参数
不能这样写:
java
ArrayList<int> numbers = new ArrayList<>(); // 错
必须写包装类型:
java
ArrayList<Integer> numbers = new ArrayList<>();
因为泛型类型参数必须是引用类型,不能是基本类型。
- 不能用 instanceof 检查参数化类型
不能这样:
java
if (list instanceof ArrayList<String>) {
// 错
}
因为运行时 ArrayList 和 ArrayList 都被擦除成 ArrayList。
只能这样:
java
if (list instanceof ArrayList) {
// 可以
}
或者更推荐:
java
if (list instanceof ArrayList<?>) {
// 可以
}
ArrayList<?> 表示"某种元素类型的 ArrayList",但不关心具体是什么。
- 不能创建参数化类型数组
不能这样:
java
ArrayList<String>[] lists = new ArrayList<String>[10]; // 错
为什么?
数组在运行时会记住自己的元素类型,而泛型在运行时被擦除了。这两套机制放在一起容易破坏类型安全。
比如如果允许:
java
ArrayList<String>[] lists = new ArrayList<String>[10];
Object[] objects = lists;
objects[0] = new ArrayList<Integer>();
String s = lists[0].get(0);
这里就可能把 ArrayList 塞进 ArrayList\[\] 里,最后取 String 时出问题。
所以 Java 干脆禁止创建参数化类型数组。
可以声明,但不能创建
java
ArrayList<String>[] lists; // 可以声明
lists = new ArrayList<String>[10]; // 不可以创建
实际开发中通常用集合替代数组:
java
ArrayList<ArrayList<String>> lists = new ArrayList<>();
- 不能实例化类型变量
不能这样:
java
public class Box<T> {
public T create() {
return new T(); // 错
}
}
因为运行时不知道 T 是什么。
常见替代方案是传入 Supplier:
java
public class Box<T> {
private final Supplier<T> supplier;
public Box(Supplier<T> supplier) {
this.supplier = supplier;
}
public T create() {
return supplier.get();
}
}
使用:
java
Box<Employee> box = new Box<>(Employee::new);
Employee e = box.create();
或者传入 Class,用反射创建,不过现在更推荐工厂或 Supplier。
- 不能创建泛型数组
不能这样:
java
public class Stack<T> {
private T[] elements = new T[10]; // 错
}
因为 T 被擦除,运行时不知道数组元素类型。
常见做法:用 Object\[\] 保存,取出时转换:
java
public class Stack<T> {
private Object[] elements = new Object[10];
private int size = 0;
public void push(T value) {
elements[size++] = value;
}
@SuppressWarnings("unchecked")
public T pop() {
return (T) elements[--size];
}
}
- 泛型类的静态上下文不能使用类型变量 T
不能这样:
java
public class Box<T> {
private static T value; // 错
}
也不能:
java
public class Box<T> {
public static T getValue() { // 错
return null;
}
}
原因是:T 属于某个具体的 Box 实例类型,而 static 属于整个类。
运行时只有一个 Box.class,不是:
java
Box<String>.class
Box<Integer>.class
如果允许 static T value,那 Box 和 Box 到底共享一个什么类型的静态变量?说不清。
不过静态方法可以声明自己的类型参数:
java
public class Box<T> {
public static <U> U identity(U value) {
return value;
}
}
这里的 U 是静态方法自己的类型参数,和类的 T 无关。
- 不能抛出或捕获泛型类的异常
不能让泛型类继承 Throwable:
java
class Problem<T> extends Exception { // 错
}
也不能捕获类型变量:
java
public static <T extends Throwable> void test() {
try {
// ...
} catch (T e) { // 错
}
}
因为异常捕获是在运行时按异常类型匹配的,而泛型类型参数会被擦除。
不过方法可以声明抛出类型变量:
java
public static <T extends Throwable> void doWork(T exception) throws T {
throw exception;
}
这个可以,常见于一些高级库代码。
- 擦除后不能造成方法冲突
比如:
java
public void print(List<String> list) {}
public void print(List<Integer> list) {}
这两个方法不能同时存在。
因为类型擦除后都变成:
java
public void print(List list) {}
6. 泛型通? extends 与 ? super
也就是这些写法:
java
List<?>
List<? extends Number>
List<? super Integer>
先抓住一个大方向:通配符是为了让泛型类型之间的关系更灵活。
因为 Java 泛型默认是不变的。
6.1 为什么需要通配符
我们知道:
java
Integer 是 Number 的子类
但这不代表:
java
List<Integer> 是 List<Number> 的子类
所以这段代码是错的:
java
List<Integer> integers = new ArrayList<>();
List<Number> numbers = integers; // 错
为什么 Java 不允许?
如果允许,就会出问题:
java
List<Integer> integers = new ArrayList<>();
List<Number> numbers = integers;
numbers.add(3.14); // Double 也是 Number
Integer x = integers.get(0); // 这里就炸了
所以 Java 禁止 List 赋给 List。
但现实中,我们经常需要一个方法能接收各种 Number 子类列表:
java
List<Integer>
List<Double>
List<BigDecimal>
这时就要用:
java
List<? extends Number>
6.2 上界通配符:? extends
java
List<? extends Number>
意思是:某种未知类型的 List,但这个未知类型一定是 Number 或 Number 的子类。
所以它可以接收:
java
List<Integer> integers = new ArrayList<>();
List<Double> doubles = new ArrayList<>();
List<? extends Number> numbers1 = integers;
List<? extends Number> numbers2 = doubles;
适合"读取"数据:
java
public static double sum(List<? extends Number> list) {
double result = 0;
for (Number n : list) {
result += n.doubleValue();
}
return result;
}
这个方法可以接收:
java
sum(List.of(1, 2, 3));
sum(List.of(1.5, 2.5, 3.5));
为什么可以读成 Number?
因为不管那个未知类型具体是 Integer、Double 还是 BigDecimal,它至少都是 Number。
但是,它不能安全地写入具体元素:
java
public static void addNumber(List<? extends Number> list) {
list.add(1); // 错
list.add(1.5); // 错
}
为什么?
因为 list 可能实际是:
java
List<Double>
如果你往里面加 Integer,就不安全。
也可能实际是:
如果你往里面加 Integer,就不安全。
也可能实际是:
java
List<BigDecimal>
如果你往里面加 Double,也不安全。
所以对于:
java
List<? extends Number>
编译器只知道:里面的元素"至少能当作 Number 读出来",但不知道具体能放什么进去。
唯一可以放的是:
java
list.add(null); // 可以,但没啥实际意义
所以记住:? extends T 适合读,不适合写。
6.3 下界通配符:? super
java
List<? super Integer>
意思是:某种未知类型的 List,但这个未知类型一定是 Integer 的父类型。
它可以接收:
java
List<Integer> integers = new ArrayList<>();
List<Number> numbers = new ArrayList<>();
List<Object> objects = new ArrayList<>();
List<? super Integer> a = integers;
List<? super Integer> b = numbers;
List<? super Integer> c = objects;
这种类型适合"写入"数据:
java
public static void addIntegers(List<? super Integer> list) {
list.add(1);
list.add(2);
list.add(3);
}
为什么可以加 Integer?
因为不管实际列表是:
java
List<Integer>
List<Number>
List<Object>
都能安全接收一个 Integer。
但读取时就弱了:
java
Object x = list.get(0); // 可以
Integer y = list.get(0); // 错
为什么不能直接读成 Integer?
因为实际列表可能是:
java
List<Object>
里面原本可能放着 "hello"、new Object() 等,并不保证取出来是 Integer。
所以记住:? super T 适合写,不适合精确读;读出来只能当作 Object。
6.5 无界通配符:?
java
List<?>
意思是:某种未知元素类型的 List。
它和 List 不一样。
List 表示:这是一个元素类型明确为 Object 的列表,可以放任何 Object
比如:
java
List<Object> objects = new ArrayList<>();
objects.add("hello");
objects.add(123);
而:
java
List<?> list
表示:这是某种类型的列表,但我不知道具体是什么类型
它可能是:
java
List<String>
List<Integer>
List<Employee>
所以不能随便加元素:
java
list.add("hello"); // 错
list.add(123); // 错
但可以读:
java
Object obj = list.get(0);
因为不管里面是什么,至少都是 Object。
常见用途:
java
public static void printAll(List<?> list) {
for (Object item : list) {
System.out.println(item);
}
}
这个方法可以接收任何类型的 List:
java
printAll(List.of("a", "b"));
printAll(List.of(1, 2, 3));
6.6 三者对比
java
List<? extends Number>
我不知道具体是什么列表,但元素一定是 Number 的某种子类。
可以读成 Number,基本不能写。
java
List<? super Integer>
我不知道具体是什么列表,但它一定能接收 Integer。
可以写入 Integer,读出来只能当作 Object。
java
List<?>
我不知道具体是什么列表。
可以读成 Object,基本不能写。
7. 反射与泛型
先给结论:
Java 泛型大多会被类型擦除,但类文件里仍然会保留一部分"泛型签名"信息,反射可以读取这些信息。
所以这里有两个层次:
-
运行时对象的真实类型:通常看不到具体泛型参数。
-
类、字段、方法声明上的泛型签名:可以通过反射读取。
-
运行时对象看不到泛型参数
比如:
java
ArrayList<String> names = new ArrayList<>();
ArrayList<Integer> numbers = new ArrayList<>();
System.out.println(names.getClass());
System.out.println(numbers.getClass());
System.out.println(names.getClass() == numbers.getClass());
输出类似:
java
class java.util.ArrayList
class java.util.ArrayList
true
运行时没有两个类:
java
ArrayList<String>
ArrayList<Integer>
只有一个:
java
ArrayList
所以你不能靠对象本身判断它是 ArrayList 还是 ArrayList。
这就是类型擦除。
- 但声明上的泛型信息可以读取
比如有一个类:
java
import java.util.List;
public class UserRepository {
private List<String> names;
public List<String> getNames() {
return names;
}
}
虽然运行时 new ArrayList() 的对象不知道自己是 String 列表,但字段声明:
java
private List<String> names;
这个"声明信息"会保留在 class 文件的 Signature 属性里。
可以用反射读取:
java
import java.lang.reflect.Field;
import java.lang.reflect.ParameterizedType;
import java.lang.reflect.Type;
import java.util.List;
public class ReflectionGenericDemo {
public static void main(String[] args) throws Exception {
Field field = UserRepository.class.getDeclaredField("names");
Type genericType = field.getGenericType();
System.out.println(genericType);
if (genericType instanceof ParameterizedType pType) {
Type rawType = pType.getRawType();
Type[] typeArgs = pType.getActualTypeArguments();
System.out.println(rawType);
System.out.println(typeArgs[0]);
}
}
}
输出类似:
java
java.util.List<java.lang.String>
interface java.util.List
class java.lang.String
这说明反射能看到字段声明里的 List。
- Class 和 Type 的区别
反射泛型里最容易迷糊的是这两个:
java
Class
Type
Class<?> 表示普通的运行时类:
java
String.class
Integer.class
ArrayList.class
但泛型类型可能不只是一个普通类,比如:
java
List<String>
T
? extends Number
List<? super Integer>
这些都不能简单用 Class 表示。
所以 Java 反射提供了更宽泛的接口:
java
java.lang.reflect.Type
Type 有几个常见子类型:
java
Class:普通类型,比如 String、Integer、ArrayList
ParameterizedType:参数化类型,比如 List<String>
TypeVariable:类型变量,比如 T
WildcardType:通配符类型,比如 ? extends Number
GenericArrayType:泛型数组,比如 T[]
可以这么理解:
Class 是 Type 的一种。
Type 比 Class 更能描述复杂泛型。
- 读取方法返回值的泛型类型
比如:
java
class UserService {
public List<String> findNames() {
return List.of("Alice", "Bob");
}
}
反射读取:
java
import java.lang.reflect.Method;
import java.lang.reflect.ParameterizedType;
import java.lang.reflect.Type;
Method method = UserService.class.getMethod("findNames");
Type returnType = method.getGenericReturnType();
if (returnType instanceof ParameterizedType pType) {
System.out.println(pType.getRawType()); // interface java.util.List
System.out.println(pType.getActualTypeArguments()[0]); // class java.lang.String
}
注意:
java
method.getReturnType()
只能得到擦除后的类型:
java
interface java.util.List
而:
java
method.getGenericReturnType()
可以得到:
java
java.util.List<java.lang.String>
所以对比一下:
java
getReturnType() // 擦除后的 Class
getGenericReturnType() // 带泛型信息的 Type
字段也是类似:
java
field.getType() // 擦除后的 Class
field.getGenericType() // 带泛型信息的 Type
参数也是类似:
java
method.getParameterTypes()
method.getGenericParameterTypes()
- 读取泛型父类
这在框架里很常见。
比如:
java
class BaseDao<T> {
}
class User {
}
class UserDao extends BaseDao<User> {
}
可以通过反射读取 UserDao 继承 BaseDao 时传入的类型参数:
java
import java.lang.reflect.ParameterizedType;
import java.lang.reflect.Type;
Type superType = UserDao.class.getGenericSuperclass();
if (superType instanceof ParameterizedType pType) {
Type actualType = pType.getActualTypeArguments()[0];
System.out.println(actualType); // class User
}
很多框架会用这种方式做类型推断,比如:
java
class UserRepository extends Repository<User, Long>
框架可以读取到:
java
User
Long
但前提是:类型参数要写在类的继承声明上。
- 什么时候读不到?
这点很重要。
如果只是创建对象:
java
List<String> names = new ArrayList<>();
你拿 names.getClass(),只能得到:
java
class java.util.ArrayList
读不到 String。
因为局部变量的泛型信息通常不能通过普通反射拿到,而且对象本身也不保存 String 这个参数。
能读到的通常是:
java
字段声明
方法返回类型声明
方法参数类型声明
父类/接口泛型声明
比如:
java
private List<String> names;
public List<String> getNames()
class UserDao extends BaseDao<User>
这些"声明上的泛型签名"能读到。
一句话总结
反射不能从普通泛型对象里恢复被擦除的类型参数,但可以读取类文件中保留下来的字段、方法、父类、接口等声明位置的泛型签名。
所以:
java
new ArrayList<String>().getClass()
看不到 String。
但:
java
Field field = SomeClass.class.getDeclaredField("names");
field.getGenericType()
如果字段声明是:
java
List<String> names;
就能看到 String。