Java-Generics-PartIII

泛型类型定义的限制

在上一篇中,我们提到参数化类型的使用存在一些限制:无法创建参数化类型的数组、无法作为异常类型、无法作为类型字面量(如List<String>.class)、无法用于instanceof关键字的类型判断。

这里需要注意区分一个易混淆的概念:泛型类型定义时的限制,即定义泛型类型时,不可以将哪些特殊类作为泛型类。具体来说,泛型类型不能是枚举类、匿名内部类、异常类型

枚举类中的所有枚举值都是public static final修饰的静态实例,这些静态实例会在枚举类加载阶段完成初始化。我们既无法为枚举实例绑定具体的泛型类型,也无法在枚举类中引用泛型类型参数。因此,使用泛型枚举类毫无实际意义,Java语法直接禁止了这种用法。

泛型类型的实例化需要通过类名<>的方式指定类型实参,若不知道类名,则无法完成实例化。由于匿名内部类无法通过类名引用,因此泛型匿名内部类也失去了存在的意义,同样被禁止。

异常类型的限制在上一篇中已详细说明,此处不再赘述。

泛型类使用和定义时的限制,这种相似概念的歧义,极易混淆读者。

还比如上一篇提到“无法创建参数化类型数组”。纠正这个不严谨的表述,实际并非所有参数化类型数组都非法——创建无界通配符参数化类型数组是合法的;同时,声明参数化类型数组的引用也是合法的

1
2
3
new List<String>[5]; // 非法
new List<?>[5]; // 合法
List<String>[] strListArr; // 合法

关于泛型数组的具体用法及适用场景,相关内容较为繁杂。尽管无界通配符参数化类型数组在语法上合法,但实际开发中仍不推荐使用,因此本文不再展开介绍。若对原始类型数组、无界通配符参数化类型数组、具体参数化类型集合的区别与用法感兴趣,可参考该链接资料。

通配符实例化

参数化类型可细分为两类,分别是具体参数化类型(concrete parameterized type)和通配符参数化类型(wildcard parameterized type),对应的实例化过程也称为具体实例化(concrete Instantiation)和通配符实例化(wildcard Instantiation)。

若对通配符参数化类型进一步细分,可分为无界通配符参数化类型(unbounded wildcard parameterized types)和有界通配符参数化类型(bounded wildcard parameterized types)。前文讨论参数化类型时,存在一些表述不严谨,所提及的参数化类型,大多指具体参数化类型。

graph TD
    A[参数化类型] --> B[具体参数化类型] 
    A[参数化类型] --> C[通配符参数化类型]
    C[通配符参数化类型] --> D[无界通配符参数化类型] 
    C[通配符参数化类型] --> E[有界通配符参数化类型] 

通配符参数化类型,指泛型类实例化时,其类型实参中至少包含一个?(通配符)。通配符用于表示未知类型,因此通配符参数化类型整体用于表示一组具体参数化类型的集合。例如Collection<?>,它代表任意类型实参的Collection具体参数化类型,包括Collection<String>Collection<Integer>等所有Collection的实例化类型。

其中,无界通配符指对通配符所代表的类型无任何限制;有界通配符则是在通配符后通过extendssuper关键字绑定某个类或接口,用于限定该通配符参数化类型所代表的具体参数化类型中,其类型实参必须满足与绑定类/接口的父子关系。例如Collection<? extends Animal>,假设Animal不是final类,那么该有界通配符参数化类型可表示所有Animal及其子类作为类型实参的Collection集合。

需要注意的是,通配符参数化类型无法作为new关键字后的实例化类型——这一点与接口类似,它仅能作为变量的引用类型,无法直接实例化对象。当使用通配符参数化类型引用一个变量时,意味着该变量的实际类型属于该通配符参数化类型所代表的那一组具体参数化类型中的某一种。

1
Collection<?> coll = new ArrayList<String>();

如上代码所示,Collection<?>类型的变量coll引用了一个ArrayList<String>类型的对象,该赋值是完全合法的——因为ArrayList<String>本身就属于Collection<?>所代表的具体参数化类型集合中的一员。

通配符参数化类型的作用

Java引入通配符参数化类型的核心目的,是弥补泛型无法协变的缺陷。上一篇文章中提到,泛型容器内元素的继承关系,不会传递给容器本身,即容器之间不具备协变性。这种特性会导致我们无法编写通用的处理代码。

假设需要定义一个方法,该方法接受一个容器参数,要求容器内的元素是Animal类或其任意子类,方法内部仅调用Animal类中定义的通用方法。直觉上,我们会将Collection<Animal>作为方法参数,但实际上,该参数仅能接收Collection<Animal>这一种具体参数化类型的实例——由于泛型不支持协变,Collection<Dog>Collection<Cat>(假设二者均为Animal的子类)均无法传入该方法。

一种直观但错误的解决方案是,针对Animal的每一个子类,都定义一个同名方法,仅将参数类型替换为对应的Collection<子类>。这种方式不仅需要随着Animal子类数量的增加而重复定义大量方法。更关键的是,在泛型类型擦除后,这些方法的签名会完全一致,无法在同一个类中定义多个签名相同的方法,因此该方案不可行。

另一种方案是使用原始类型(如Collection)作为方法参数,这种写法在语法上是合法的,但不推荐使用——相较于通配符参数化类型,原始类型会丢失泛型的类型安全校验,更容易引发类型转换异常等问题。另一方面,也无法在方法参数层面校验容器内元素是否与Animal类存在父子继承关系,完全失去了泛型对类型的约束价值。

而通配符参数化类型的引入,恰好解决了这一问题:我们可以将方法参数定义为通配符参数化类型,使一个方法能够接受同一个泛型类的所有相关实例化类型,从而弥补泛型无法协变的缺陷。但与此同时,由于通配符代表未知类型,无法确保容器内元素的具体类型,因此在方法中引用该容器的元素时,会受到一些限制。

通配符参数化类型的限制

1
2
3
4
5
6
7
8
class Box<T> {
private T t;
public Box(T t) { this.t = t; }
public void put(T t) { this.t = t;}
public T take() { return t; }
public boolean equalTo(Box<T> other) { return this.t.equals(other.t); }
public Box<T> copy() { return new Box<T>(t); }
}

假设我们通过Box<?>引用一个变量,并尝试调用其put方法——根据该方法的签名,它需要接收一个与类型参数T匹配的参数。此时,由于类型实参为?(未知类型),直觉上可能会认为该方法可以接受任意类型的参数,但实际情况恰好相反:该方法仅能接收null作为参数,无法传入其他任何类型的值。

核心问题在于对?的理解:当?作为类型实参、实例化为通配符参数化类型时,它代表的是“任意类型实参对应的具体参数化类型的集合”。而实际使用时,Box<?>引用的变量,其实际类型是该集合中的某一种具体类型,可能是Box<String>,也可能是Box<Integer>,对应的put方法分别只能接收StringInteger类型的参数。

Java 编译器会先梳理所有潜在的参数类型情况,取其交集作为最终允许传入的参数类型范围。由于我们无法确定Box<?>对应的具体类型,且不存在一个能作为所有类子类的“万能类型”,因此为了保证类型安全,Java编译器仅允许将null作为put方法的入参——因为null是所有引用类型都允许的常量值,不会引发类型不匹配问题。

但对于返回值为类型参数T的方法(如上例中的take方法),调用方可以使用Object类作为接收返回值的类型。其逻辑与上述一致:通配符参数化类型的类型实参是未知的,take方法的返回值类型也随之未知。而Object类是所有Java类的父类,因此将返回值赋值给Object类型变量,属于多态的隐式向上转型,是完全合法的。

1
2
3
4
5
6
7
8
9
10
11
12
13
Box<?> box = new Box<String>("abc");

box.put("xyz"); // error
box.put(null); // ok

String s = box.take(); // error
Object o = box.take(); // ok

boolean equal = box.equalTo(box); // error
equal = box.equalTo(new Box<String>("abc")); // error

Box<?> box1 = box.copy(); // ok
Box<String> box2 = box.copy(); // error

此处有一个很反直觉的点:调用equalTo方法时,即使将变量自身作为参数传入,也是非法的。从表面上看,box的类型是Box<?>,而equalTo方法的参数类型是Box<T>,若用?替换T,似乎参数类型应为Box<?>,与box的类型一致,但实际并非如此。

其根本原因是,?(通配符)不能直接替换泛型类中的类型参数T。当使用?作为类型实参实例化泛型类时,编译器会为?生成一个临时的捕获类型(例如capture#1-of ?),该捕获类型可视为一个具体的未知类型。此时,equalTo方法的参数类型实际上是Box<capture#1-of ?>(一个具体参数化类型),而传入的box变量类型是Box<?>(通配符参数化类型),二者并非同一类型,因此调用非法。

简单来说,box变量的实际类型是某一种具体参数化类型,而equalTo方法仅接受与自身类型完全匹配的Box实例;若方法参数类型不明确,传入的参数可能与实际类型不匹配,从而引发类型安全问题。若将equalTo方法的参数类型调整为Box<?>,则该调用即可合法执行。这也是极易混淆的地方。

总结

  1. 讨论泛型的限制时,需明确区分“泛型类型定义时的限制”与“泛型类型使用时的限制”,避免混淆

  2. 参数化类型可细分为具体参数化类型和通配符参数化类型,其中通配符参数化类型又可进一步分为无界通配符和有界通配符两类

  3. 通配符参数化类型的本质是一组具体参数化类型的集合,用于表示某一类泛型实例的统称

  4. 当使用通配符参数化类型引用变量时,对该变量的方法调用、返回值处理会存在特定限制,其核心目的是保证泛型的类型安全


Java-Generics-PartIII
http://dracoyus.github.io/2026/01/22/Java-Generics-PartIII/
作者
DracoYu
发布于
2026年1月22日
许可协议