找回密码
 立即注册
首页 业界区 业界 Java 泛型详细解析

Java 泛型详细解析

上官银柳 2025-6-6 14:08:04
泛型的定义

泛型类的定义

下面定义了一个泛型类 Pair,它有一个泛型参数 T。
  1. public class Pair<T> {
  2.         private T start;
  3.         private T end;
  4. }
复制代码
实际使用的时候就可以给这个 T 指定任何实际的类型,比如下面所示,就指定了实际类型为 LocalDate,泛型给了我们一个错觉就是通过个这个模板类 Pair,我们可以在实际使用的时候动态的派生出各种实际的类型,比如这里的 Pair 类。
  1. Pair<LocalDate> period = new Pair<>();
复制代码
泛型类的继承

子类是一个泛型类的定义方法如下:
  1. public class Interval<T> extend Pair<T> {}
复制代码
这里的 Interval 类是一个泛型类,也可以像上面使用 Pair 类一样给它指定实际的类型。
子类是一个具体类的定义方法如下:
  1. public class DateInterval extends Pair<LocalDate> {}
复制代码
这里的 DateInterval 类就是一个具体的类,而不再是一个泛型类了。这里的语义是 DateInteral 类继承了 Pair 类,这里的 Pair 类也是一个具体类。但是由于 Java 的泛型实现机制,这里会带来多态上的一个问题,见下面的分析。
而像下面的这种定义具体类的写法是错误的:
  1. public class DateInterval extends Pair<LocalDate> {}
复制代码
泛型方法的定义

泛型方法定义时,类型变量放在修饰符的后面,返回值的前面。泛型方法既可以泛型类中定义,在普通类中定义。
  1. public static <T> T genericMethod(T a) {}
复制代码
这里顺便记录一下,因为是使用擦除来实现的泛型,因此字节码中的方法的签名是不会包含泛型信息的。对于泛型方法会多生成一个 Signature 的属性,用于记录方法带泛型信息的签名,反编译器也可以根据这个信息将泛型方法还原回来。
1.png

2.png

构造函数泛型

下面的代码定义了一个泛型类 ConstructorGeneric,它的泛型参数是 T,这个类的构造函数也是泛型的,它有一个泛型参数 X。
  1. class ConstructorGeneric<T> {
  2.         public <X> ConstructorGeneric(X a) {}
  3. }
复制代码
创建该对象的代码如下:
  1. ConstructorGeneric<Number> t = new <String>ConstructorGeneric<Number>("123");
复制代码
这里 new 后面的 String 是传给构造器的泛型 X 的,即 X 的实际类型为 String;类的范型参数是由 Number 传递的,即 T 的实际类型是 Number。这里两个都是省略,写在这里是为了显示区分出两个参数传递的位置。
类型变量的限定

带单个上界限定
下面的代码定义了一个 NatualNumber 类,它的泛型参数 T 限制为 Integer 或者 Integer 的子类。
  1. public class NaturalNumber<T extends Integer> {
  2.     private T n;
  3.     public NaturalNumber(T n)  { this.n = n; }
  4.     public boolean isEven() {
  5.         return n.intValue() % 2 == 0;
  6.     }
  7. }
复制代码
调用代码如下:
  1. // 正常
  2. NaturalNumber<Integer> natural1 = new NaturalNumber<>(1);  
  3. // 无法编译,因为这里和泛型类定义的上界不符合
  4. NaturalNumber<Double> natualral2 = new NaturalNumber<>(1.0);
复制代码
带多个上界的限定
多个上界之间使用 & 符号进行分隔,如果多个限定中有类,则类需要排在接口后面(因为 Java 不支持多继承,所以不存在有多个限定的类的情况)。使用时需要满足所有的限定条件才能执行,这个校验应该是在编译时期做的,因为擦除之后,只会保留第一个限定界。
  1. class A {}
  2. interface B {}
  3. class C extends A implements B {}
  4. public static <T extends A & B> void test(T a) {}
  5. public static void main(String[] args) {
  6.         // 编译错误,A 只能满足一个上界
  7.         test(new A());
  8.         // 正常
  9.         test(new C());
  10. }
复制代码
通配符

在泛型中使用 ? 表示通配符,它的语义是表示未知的类型。通配符可以用作方法的形参、字段的定义、局部变量的定义,以及有的时候作为函数的返回值。通配符不能作为实参调用泛型方法,不能创建对象,或者派生子类型。
上界通配符

当你想定义一个普通方法,这个普通方法可以处理某一类的 List 中的元素时,比如像:List,List,List 时,这个时候如果你把方法的入参定义为 List 是不行的,因为在 Java 中 List 不是 List 的子类。
  1. public static void process(List<Number> numbers) {}
  2. // 编译错误
  3. List<Number> numbers = new ArrayList<>();
  4. proess(numbers);
复制代码
假设 List 是 List 的子类,则可以实现如下的代码:
  1. List<Integer> integers = new ArrayList<>();
  2. // 假设下面是成立的
  3. List<Number> numbers = integers;
  4. // 下面这句也应该是合法的,但是这违背了 intergers 只能存放 Integer 的语义
  5. numbers.add(new Double());
复制代码
从上面的例子可以看出,如果允许 List 是 List 的子类型,则会破坏泛型的语义,因此这在 Java 中是不允许的。
但是又实际存在上面描述的这种需求,因此 Java 提供了上界通配符的语法,则方法定义可以定义为如下:
  1. public static void process(List<? extends Number> numbers) {
  2.         for (Number num : numbers) {
  3.                 // do something
  4.         }
  5. }
  6. // 下面的调用都是能够正常编译通过的
  7. List<Number> numbers = new ArrayList<>();
  8. process(numbers);
  9. List<Integer> integers = new ArrayList<>();
  10. process(integers);
  11. List<Double> doubles = new ArrayList<>();
  12. process(doubles);
复制代码
无界通配符定义的 List 里面的元素只能赋值给 Object 类型。这里可以想象一下 List 的 get() 方法的泛型参数 E 就变成了 ? 这个实际类型,它的语义是一个未知的类型,既然是一个未知的类型那么我只能赋值给 Object 类型的变量了。
  1. public static void process(List<? extends Number> numbers) {
  2.         numbers.add(new Integer());
  3. }
复制代码
无界通配符定义的 List 里面只能添加 null,不能添加其它的任何类型的元素,即使是 Object 也不行,因为添加了之后就会违背泛型的语义了。
无界通配符的主要使用场景是:

  • 需要使用 Object 类中的方法
  • 使用了泛型类中不用关心泛型的方法,比如 List 中的 size() ,clear() 方法
下界通配符

在使用上面的上界通配时,发现了一个问题,如果一个 List 类型形参声明为了上界通配符,是没有办法往这个 List 里面添加元素的,为了解决这个问题,可以使用下界通配符,可以定义如下的方法:
  1. public static void printList(List<?> list) {}
复制代码
为了解决这个问题这个时候就可以通过新建一个私有的泛型方法来帮助捕获通配符的类型,这个私有的泛型方法名称通常是原有方法加上Helper后缀,这种技巧称为通配符捕获。代码如下:
  1. public static void printList(List<?> list) {
  2.         for (Object obj : list) {
  3.                 // do something
  4.         }
  5. }
复制代码
对于泛型方法,因为 add() 方法的入参,get() 方法返回值的泛型参数都是 T,当传入一个 List 进来,虽然这个 List 里面的对象实际类型不知道,但是通过泛型参数可以判断 get() 方法返回类型和 add() 方法的入参类型都是一样的,都是 T 捕获到的一个实际类型 X。
对于带通配符参数的方法,因为方法的声明没有一个泛型参数,不能捕获到实际的参数类型 X。那么对于每次方法的调用编译器都会认为是一个不同的类型。比如编译器编译的时候 list.set(0, xxx),这里的入参的类型就会是 CAP#1, list.get(0) 返回的类型就是 CAP#2,因为没有一个泛型参数来告诉编译器说 CAP#1 和 CAP#2 是一样的类型,因此编译器就会认为这两个是不同的类型,从而拒绝编译。下图是编译器实际的提示信息:
3.png

4.png

从上面的图也可以看出,第二次调用方法时,类型又变成 CAP#3 和 CAP#4 了,这也证明了每次编译器都会认为是一个新的类型。
实际上这里也可以将这个私有的 Helper 方法定义为公共的,然后去掉通配符的方法。这两种定义实际上是达到了相同的效果,但是 Java 语言规范 5.1.10 章节中更推荐采用通配符的方式定义,但它上面阐述的原因没太看懂,但是在另外一篇博客里面看到一个观点感觉有点道理。
5.png

它说如果定义成一个泛型方法,那么老的遗留的没有用泛型的代码调用这个方法就会产生一个警告,但是如果是使用通配符则不会有警告产生。
  1. public static void addNumbers(List<? super Number> list) {
  2.         list.add(new Integer());
  3.         list.add(new Double());
  4. }
复制代码
然而实际上 JDK 中真正的实现并没有采用这种方式,而是直接用注解忽略了异常,直接用的原生类型来实现的。Collections 中的 reverse() 方法内部实现逻辑如下:
[code]@SuppressWarnings({"rawtypes", "unchecked"})  public static void reverse(List list) {      int size = list.size();      if (size < REVERSE_THRESHOLD || list instanceof RandomAccess) {          for (int i=0, mid=size>>1, j=size-1; i
您需要登录后才可以回帖 登录 | 立即注册