基本设计原则

  • how codes should vary in different types.
  • compatible with other release.
  • before generic class,generic programming was achieved with inheritance:在所有后来使用T的地方使用Object或者Object数组。经典的猫狗列表问题的来源。

基本语法

  • java 的泛型没有 template 关键字。表面上(superficially)看和 C++ 并无二致,实际上有大量差别
  • 三种基本概念:
    • Type Parameter(类型形参):在泛型类、接口或方法声明时使用的标识符。
      • class Box<T> 中的 T。
      • 但是在 Java 核心技术中,List<String>中的String也经常被描述为 Type Parameter。Container.class.getTypeParameters()得到的也是T
    • Type Variable(类型变量):类型参数在代码中的引用。
      • 可以认为是类型参数的一个”实例”。在class Box<T> { T value; } 中,value 的类型T是一个类型变量(而不是占位符T)。许多文档中”类型参数”和”类型变量”可互换使用。
    • Type Argument(类型实参):在使用泛型类型时,提供的具体类型。
      • 在调用或实例化时提供。Box<Integer>中的Integer
        1
        2
        3
        4
        5
        6
        7
        8
        9
        10
        11
        12
        13
        14
        // 类型参数T在类声明中引入
        public class List<T> {
        // T用作类型变量
        private T[] elements;

        // T继续用作类型变量
        public void add(T element) {...}

        // E是新的类型参数,用于此方法
        public <E> void addAll(Collection<E> items, Function<E, T> converter) {...}
        }

        // String是类型实参,实例化时提供
        List<String> names = new ArrayList<>();
        • 泛型类的 type parameter 的声明在类背后。泛型方法的 type parameter 在 modifier(public static)和return value之间。
    • 在 C++ 里,f(g<a,b>(c))有两个意思:使用g<a,b>(c)的结果调用f;或者使用g<ab>(c)调用 f。所以 java 泛型参数要写在方法前面,类型见证也在方法前面。
      • List<T> 是 generic type。List<String>是 Parameterized type。

边界/绑定类型(bounding type)

在Java泛型的上下文中,”bound”一词最准确的翻译是“边界”(boundary)而非”绑定”。Java官方文档一致使用”upper bound”(上边界)和”lower bound”(下边界)这样的术语,这些术语在数学和集合论中表示边界概念。

  • 在类型参数 T 声明时就指定 bound,而不是在作为局部变量或者返回值时。
  • 明确要绑定 type variable 到某个类型(T extends BoundinType,擦除到这个 BoundinType)-得到 type argument,才可以在接下来的方法里调用那个类型的方法,如需要 compareTo 方法,就必须<T extends Comparable<T>>extends本身可以后接多个绑定类型,用&分隔。
    • 这点同 C++ 是一样的,我们是为了调用某个方法才指定了 bound。
    • extends 代表 subtype concept 的 approximation,并不是扩展的意思,在 bound 的使用里,它统一了扩展与实现。
      • extends 只能有一个 class,用ampersand(&)来分隔各种类型,用,来分隔 type variables。这会制造有限的菱形继承问题-这和 java 单继承的思路一致。
        • T extends Comaprable & Serializable 会把 T 擦除到 Comaprable(first bound),在某些需要作为 Serializable 的时候则编译器插入一个 cast instruction(如果必要的话,java 甚至会对.取值操作符进行 cast指令插入) - java 的编译器做这种插入对程序的其他部分算得上是可插拔的了。

递归泛型边界模式(Recursive Generic Type Bounds)

这是被称作“F-bounded多态”(F-bounded polymorphism)或”递归泛型边界”的高级泛型设计模式。

1
2
public class RepresentationModel<T extends RepresentationModel<? extends T>> {
}
  • T必须是RepresentationModel的子类
  • 并且这个子类的泛型参数必须是T自身或T的某个子类

在这个例子里:RepresentationModel - type parameter 是 -> T <- 是子类,且这个子类的 type argument 是 T 自己 - RepresentationModel

如果不使用这种类型:

1
2
3
4
5
6
7
8
9
10
11
12
// 不使用递归泛型
public class ResourceModel {
public ResourceModel addLink(String rel, String href) {
// 添加链接的逻辑
return this;
}
}

public class UserModel extends ResourceModel {
private String username;
// 其他用户特有属性和方法...
}

那么使用时会遭遇编译错误:

1
2
3
4
5
UserModel user = new UserModel();
// 返回类型是ResourceModel,而不是UserModel
ResourceModel result = user.addLink("self", "/users/123");
// 编译错误:ResourceModel类型没有getUsername()方法
String username = result.getUsername();

这个问题的核心是,有些 fluent API 本身返回自身,这种自身不能返回子类时,父子混合 build 会产生奇怪的问题。

解法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
public class RepresentationModel<T extends RepresentationModel<? extends T>> {
public T addLink(String rel, String href) {
// 添加链接的逻辑
return self();
}

// 返回子类型的时候是强行转的,这一步是这类模式必须的
@SuppressWarnings("unchecked")
private T self() {
return (T) this;
}
}

public class UserModel extends RepresentationModel<UserModel> {
private String username;

public String getUsername() {
return username;
}
}

使用时:

1
2
3
4
5
UserModel user = new UserModel();
// 现在返回类型仍然是UserModel
UserModel result = user.addLink("self", "/users/123");
// 完全类型安全,编译通过
String username = result.getUsername();

这种方案的做法是:

  1. 父类所有方法都使用 T 作为 type variable。在父转子的时候,使用转型,忽略警告。
  2. 流式调用的时候都转为自己。

核心优势:

  • 类型安全的方法链:子类调用父类方法时返回的仍然是子类类型。
  • 代码复用:父类可以实现通用功能,子类无需重写这些方法。
  • IDE支持:智能提示和自动完成可以正确显示返回对象的可用方法。

这种模式广泛应用于各种Java库中:

  • Java的Enum<E extends Enum<E>>
    • public abstract class Enum<E extends Enum<E>> implements Comparable<E>, Serializable {}
      • 确保类型安全:<E extends Enum<E>> 这种递归泛型定义确保了枚举类型只能被其子类实例化
      • 提供类型正确的方法:使得像 compareTo 这样的方法能够正确接收子类类型的参数
      • 实现自引用泛型:允许枚举类引用自身类型
      • 极端重要!更复杂的例子<前边的部分,恰好是>前边的部分加上一个<?>,中间插入一个? extendsjava.lang.Object&java.lang.Serializable&java.lang.Comparable<? extends java.lang.Object&java.lang.Serializable&java.lang.Comparable<?>>
  • JPA的CriteriaBuilder
  • Stream API-易被忽略
  • 各种构建器模式实现

注意和 Comparable 比较

  1. <T extends Comparable<T>> 意味着 T 加入了一种以自己为目标的混型能力。
  2. RepresentationModel<T extends RepresentationModel<? extends T>>意味着这种类型要使用某些父子类型都共用的操作,必须让操作能够返回子类型而不是父类型。实现自引用类型层次结构
    • 类似的例子还有:public static <T> List<T> unmodifiableList(List<? extends T> list)。这种 T 总是在远处作为一个起点使用。实际的类型的多重绑定还要出现在方法参数或者返回值里。
  3. Comparable<T extends Comparable<? super T>>,意味着需要与父类型进行比较操作

类型擦除(type erasure)

  • 擦除的目的:为了 interoperability between generic and legacy code。所以把类型信息全擦除成无泛型,能在 java 1.0 上运行的字节码。
  • 类型擦除的结果:将 type variable 替换成绑定类型(BoundingType)而不是类型实参的 ordinary class。如果B<T extends Comparable>A extends B<C>。那么A编译过后,得到的是一个B内所有T都替换成Comparable的类,但是在写代码的时候,编译器会在赋值的时候检查,且在取值的时候转型,保证所有的T相关的方法的类型全部都是C而不是 Comparable。如果我们声明的是B<T>,我们会得到使用 raw type 的 class(如ArrayList之于ArrayList<String>)-在字节码层面T全部换成了Object,即使我们在IDE里感觉我们仍然在使用C这一类型。
  • 有了类型擦除,所有的泛型表达式都是可以被翻译的。带有泛型的代码被编译器翻译后通常分为两个部分:1. ordinary class,带有 bounding type,2. casting code,带有类型转换的代码。
  • cast code 是为了保证类型安全而存在的。这些代码有些(普通的泛型set)是在编译时通过检查实现的,有些则是在代码里加上一些字节码指令实现(get和桥接的set)的。
  • 假设B extends A<C>,我们调用calc(new C())的时候,父类型的相关方法总是会被擦除到 bounding type(假定 C 的擦除类型是 Object),所以子类的方法也带有这个 bounding type 的方法实现calc(Object object)。但为了保持多态,如果子类也实现了一个calc(C c),编译器就会生成了一个calc(Object c)去调用calc(C c)相当于把重载桥接成多态,见下面的“擦除的实际工作流程”),真正要使用多态,就必须产生一个 synthesized bridge method,执行calc(Object object) {calc((C)object);}。这个东西是为了保持多态(preserve polymorphism)。
  • 为了兼容性(compatibility)考虑,A a = new A<String>() 之类的赋值(从泛型到擦除类型的赋值总是会成立的)总是会成立,但编译器总是会报 warning(作为原始类型 'generic. Pair' 的成员对 'setFirst(T)' 的未检查的调用)。在 Jackson 中经常遇见如下问题:实际反序列化的生成 class 是 LinkedHashMap,但Entity<A<B>> = JsonUtil.toObject(str)等还是会赋值成功(此处 实际得到的是A<LinkedHashMap>)。

擦除的实际工作流程

假设我们有一个 Pair 类:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
public class Pair<T>
{
private T first;
private T second;

public Pair() { first = null; second = null; }
public Pair(T first, T second) { this.first = first; this.second = second; }

public T getFirst() { return first; }
public T getSecond() { return second; }

public void setFirst(T newValue) { first = newValue; }
public void setSecond(T newValue) { second = newValue; }
}

我们产生子类如下

1
2
class DateInterval extends Pair<LocalDate> {
}

按照常见的规则,编译器会把所有的方法签名都改掉,如setSecond(T newValue)会被先编译为setSecond(LocalDate newValue)

但是如果我们使用反编译器去尝试反编译这个类javap -c DateInterval.class,我们会发现如下事实:

1
2
3
4
5
6
7
8
Compiled from "Pair.java"
class com.magicliang.transaction.sys.common.util.DateInterval extends com.magicliang.transaction.sys.common.util.Pair<java.time.LocalDate> {
com.magicliang.transaction.sys.common.util.DateInterval();
Code:
0: aload_0
1: invokespecial #1 // Method com/magicliang/transaction/sys/common/util/Pair."<init>":()V
4: return
}

并不存在一个被重载的方法,什么换签名方法都不存在

而如果我们这样写:

1
2
3
4
5
6
7
class DateInterval extends Pair<LocalDate> {
public void setSecond(LocalDate second) {
if (second.compareTo(getFirst()) >0) {
super.setSecond(second);
}
}
}

我们会一次在子类里得到2个setSecond方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
Compiled from "Pair.java"
class com.magicliang.transaction.sys.common.util.DateInterval extends com.magicliang.transaction.sys.common.util.Pair<java.time.LocalDate> {
com.magicliang.transaction.sys.common.util.DateInterval();
Code:
0: aload_0
1: invokespecial #1 // Method com/magicliang/transaction/sys/common/util/Pair."<init>":()V
4: return

public void setSecond(java.time.LocalDate);
Code:
0: new #2 // class java/lang/Throwable
3: dup
4: invokespecial #3 // Method java/lang/Throwable."<init>":()V
7: astore_2
8: getstatic #4 // Field java/lang/System.out:Ljava/io/PrintStream;
11: aload_2
12: invokevirtual #5 // Method java/io/PrintStream.println:(Ljava/lang/Object;)V
15: aload_1
16: aload_0
17: invokevirtual #6 // Method getFirst:()Ljava/lang/Object;
20: checkcast #7 // class java/time/chrono/ChronoLocalDate
23: invokevirtual #8 // Method java/time/LocalDate.compareTo:(Ljava/time/chrono/ChronoLocalDate;)I
26: ifle 34
29: aload_0
30: aload_1
31: invokespecial #9 // Method com/magicliang/transaction/sys/common/util/Pair.setSecond:(Ljava/lang/Object;)V
34: return

public void setSecond(java.lang.Object);
Code:
0: aload_0
1: aload_1
2: checkcast #10 // class java/time/LocalDate
5: invokevirtual #11 // Method setSecond:(Ljava/time/LocalDate;)V
8: return
}

public void setSecond(java.time.LocalDate)是我们定义的显式方法,而public void setSecond(java.lang.Object);是编译器帮我们合成(synthesized)的桥接方法(bridige method)

1
2
3
4
5
6
public static void main(String[] args) {
DateInterval interval = new DateInterval();
Pair<LocalDate> pair = interval;
// 我们这里调进去可以看到多出一行隐藏栈帧,行数恰好是 class DateInterval extends Pair<LocalDate> 这一行
pair.setSecond(LocalDate.now());
}

所以总结起来:

  1. Pair<T>本质上 BoundingType 是 Object。extends Pair<LocalDate>得到的是一个所有 T 都 substitute 成 Object 的类型,作为运行时的无类型化类型使用。
  2. 但是编译器会在编译 set 方法的时候严格限制类型,如果使用了非 LocalDate 的类型设值必定发生编译错误,而如果使用 get 方法,取出来的值会隐藏一个 checkcast 的字节码指令
  3. 在调用子类实例的方法之前,会产生一个擦除类型的方法,即setSecond(java.lang.Object),在内部会有一个 checkcast 指令的实现,达到setScond((LocalDate)second)的效果。
  4. pair.setSecond(LocalDate.now());的时候,指向的是父类Pair<LocalDate>而不是DateInterval,调用setSecond只能指向父类方法,而不能是子类方法。但实际上我们调用的首先是桥接方法,然后才到具体类型的方法(栈帧为证)。也就是说,编译器知道推测方法的调用顺序,以及我们在试图override我们没有override的方法,因为父方法的签名是setSecond(java.lang.Object),我们声明一个setScond((LocalDate)second)实际上是在overload这个方法。编译器帮我们把overload转成了override,我们即使加上@override注解也没问题,而且调用的时候能够实现多态。

所谓的泛型确实只有编译器才存在,参数实例化的泛型类里只留存一些绑定类型的边界信息,无边界泛型类的父方法的类型信息都是 Object,类型安全都是在编译器通过1检查2插入桥接方法3取值的时候cast实现的。这种实现保证了运行时无类型,又确保程序有多态效应

验证桥接方法有2种方法:

  1. 反射检查:使用Method.isBridge()判断方法是否为桥接方法。Method method = DateInterval.class.getMethod("setSecond", Object.class);
  2. 字节码查看:通过javap -c DateInterval.class查看生成的桥接方法,其修饰符包含ACC_BRIDGE(本例中没有试出来)。如果直接查看 Pair.class,我们会看到方法仍然使用泛型:public void setSecond(T);桥接方法仅在子类主动重写父类泛型方法时生成

桥接方法的另一种例子

一个类型实现了 Clonable,天然是 Object 的子类。它实现自己的clone的时候,会得到两个方法:

1
2
Employee clone()
Object clone() // 这个方法是合成的,而且override了 Object 的同名方法,而且会调用另一个方法,这样用父类型实现多态调用的时候,调用父类的 clone,会桥接到多态的 clone 上的

类型 clash

1
2
3
4
5
6
7
8
9
10
11
12
13
14
public class GenericOverloadPuzzle {
public static <T> String method(T t) {
return "Method 1";
}

public static String method(Object o) {
return "Method 2";
}

public static void main(String[] args) {
System.out.println(method("Hello"));
System.out.println(method(new Object()));
}
}

两个method本身不是重载,直接签名冲突:'method(T)' 与 'method(Object)' 冲突;两个方法具有相同的擦除

与遗留代码的冲突

把 generic class 的实例赋给 raw type class 的实例会遇到编译器警告;反过来,把 raw type class 的实例赋给 generic class 的实例也会遇到编译器警告-在跨模块传递数据时,后者往往更危险。

现代代码编写原则是:

  1. 原理早期的数据结构。
  2. 全程使用 generic class 而不是 raw type class-除非你使用反射或者反序列化不得不得到 raw type,你必须十分清楚内存的实际类型,然后@SuppressWarnings("unchecked")
  3. 我们可能可以把List<Integer>赋给List<String>,这会导致堆污染(heap pollution)

Type Witness 类型见证/目击者

1
2
3
4
5
6
7
8
9
// 抽象模式
SomeClass.<TypeName>methodName(arguments)
// 当没有足够的上下文进行推断时:
var result = Collections.<String>emptyList(); // 没有var需要Type Witness
// 在复杂的泛型嵌套中:
someMethod(Collections.<Map<String, Integer>>emptyList());

// 在 java7 及以前,这行代码可能不能编译。processStringList 的参数 可能是个泛型类型,而实参赋值的时候无法推导列表类型
processStringList(Collections.emptyList());

其中第二种情况更常见,如果方法调用/=左边有足够的目标类型,编译器不会产生有歧义的推导(Inference)路径。

泛型不能做什么(Restricstions and Limitations)

所有泛型的 bug 都与一个基本假设有关,在虚拟机里运行的类型实际上使用的是 raw type,不注意这一点就可能出错。parameterized type vs raw type

不能使用基本类型实例化 type parameters

C++可以。

动态类型识别(inquiry)只能在 raw type 上工作

instanceof、Class ==、getClass() 这类操作都只能当做 raw type 工作:

  • 实际上不能a instanceof Pair<String>,这会导致一个编译错误
  • 可以(Pair<String>)a,这会导致一个编译警告。
  • stringPair.getClass() == employeePair.getClass() // true

不能创建 ParameterizedType 作元素的数组

但可以声明赋值到泛型数组,这就是 GenericArrayType 的用处。

  • GenericArrayType 是T[]List<String>[]List<String>是ParameterizedType。
  • -创建不可能
    • Pair<Sring>[] a = new Pair<String>[10]是不可能的。是Pair<String>而不是 T。
    • T[] arr = new T[10];是不可能的。
    • var table = (Pair<String>[])new Pair<?>[10]是合法的,可以 new 通配符数组,然后强转型。
  • Object[] arr = new Pair<String>[10]这样是合法的,而且这个arr实际上是Pair[],所以它可以接受任意的Pair-不管是Pair<String>还是Pair<Employee>,但不能接受String。这会导致 cast 异常。

类型参数可以和vargargs 一起工作

如果使用@safevarargs连警告都不会有。也就是说我们可以这样创建泛型数组Pair<String>[] pair = makePairs(pair1, pair2)// makePairs 的签名是(T... arr),且直接返回这个 arr(这一条破坏了“不能创建parameterized type 的数组”规则)。

但泛型数组还是危险的,因为数组本身还是 raw type,往里面写入一个Pair<Integer>可以,取出来会有转型问题。

不能初始化 type variables:

  • 最常见的非法写法是new T()
    • 因为 T 实际上会擦除到 Object,不能调用程序员意图里的那个构造器。
    • 但 T t 是可以且很常见的。
    • workaround:
      • java8:Supplier<T> constr constr.get(),然后在使用的时候,构造器的方法引用就是一个Supplier<T>
      • java8以前:在使用 T 的地方都使用Class<T>,然后借反射来生成对应的对象:Class<T> cl cl.getConstructor().newInstance()

不能实例化泛型数组

T[] a = new T[1] // error

如果我们需要得到某种类型的数组,可以采取如下方法:

  1. 先找到一个泛型 List,然后使用下文提到的“空泛型数组的拷贝方法”。
  2. 想办法生成一个数组,然后(T[])做转型-实例化不可以,但是转型可以

这一条和上面的 ParameterizedType 条目差不多。

泛型类的 type variables 不能在它的 static 上下文里工作

但静态方法自己可以有 type variables。

我们可以声明一个class Single<T>,但这个Single里的静态方法不能引用T。静态方法里可以自己声明另一个<U>

不能抛出和捕获泛型类

  1. T extends Throwable合法。
  2. method throws T合法,throw (T)t合法。
  3. catch(T t)非法。
  4. Problem<T> extends Exception非法。

巧妙地使用泛型,可以破坏 checked exception 的限制

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
interface Task
{
void run() throws Exception;

@SuppressWarnings("unchecked")
static <T extends Throwable> void throwAs(Throwable t) throws T
{
throw (T) t;
}

static Runnable asRunnable(Task task)
{
return () ->
{
try
{
task.run();
}
catch (Exception e)
{
// 编译器允许我们去掉 <RuntimeException>,似乎可以直接抛出运行时异常
Task.<RuntimeException>throwAs(e);
}
};
}
}

public class DefeatCheckedExceptionChecking
{
public static void main(String[] args)
{
var thread = new Thread(Task.asRunnable(() ->
{
Thread.sleep(1000);
System.out.println("Hello, World!");
throw new Exception("Check this out!");
}));
thread.start();
}
}

我们骗过了编译器,让它认为asRunnable抛出的不是受检异常:

  1. 调用 asRunnable 以后,我们不需要写受检异常的 handler,也不需要new RuntimeException(ex)来 wrap
  2. 运行时throw (T) t;不会报错,因为擦除是擦除到 Throwable,我们仍然在更上游遇到这个异常时和遇到普通异常的流程是一致的。

永远不要写boolean equals(T object)

因为每个泛型方法都有一个兜底的 raw type 方法兜底,如果兜底方法和父类的非泛型方法相冲突(clash),编译会报错。

编译器会擦除出 boolean equals(Object object),制造同签名的方法,连重载都算不上。

只能写:

1
2
3
4
5
6
@Override
public boolean equals(Object o) {
if (o == null || getClass() != o.getClass()) return false;
Pair<?> pair = (Pair<?>) o;
return Objects.equals(first, pair.first) && Objects.equals(second, pair.second);
}

这样构成了显式override。

所以这里有一个极端重要的认知:

  1. 我们不能在泛型类里声明擦除后有同签名的方法。foo(T o)foo(Object o)不算一种重载,算一种 clash。但我们可以写@override foo(Object o){}来 override。
  2. 我们可以在实例化的子类里面写具体类型来 overrload 擦除方法,编译器会帮我们把overload转成 override。例子见上面“擦除的实际工作流程”。

再谈数组和泛型类的继承和协变

数组隐藏有类型信息。所以虽然数组是协变的,但是

1
2
3
4
// 合法
A[] arr = new B[10]
// 但是 arr 实际上是一个 B 数组
arr[0] = new A(); // 会出运行时 ArrayStoreException,B 数组不允许存储 A 类型。

但是泛型类是完全擦除的,所以对于 A List 里插入 B 元素,如果允许继承,则真的会发生混合插入的问题。

Java 有一个习惯,就是不完全防止堆污染,有时候靠在取值的时候抛出异常来解决。

泛型能够做什么

  • type variable 可以拿来做目标参数,如T t = get()(T)(T[])
  • 我们始终可以把 parameterized type 往 raw type 转。
  • ArrayList<Integer>可以往List<Integer>转-我们经常是这样声明的。

通配符(wildcard)

  1. ? 表明了 Type variable 是怎样 vary的。
  2. 我们已经知道泛型不是协变的,但是 ? 用来实现某些人类似协变的需求,产生了一个泛型类家族的父类/子类(通配符允许泛型类的继承树在泛型层面出现,而普通泛型则只允许泛型类的继承树在外部类的层面出现)。这增加代码的灵活性,同时保持类型安全。
1
2
3
4
5
List<?> (根节点)
├── List<? extends Number> // 子类型范围:?可以是Number或其子类(如IntegerDouble
│ └── List<Integer> // 具体类型(非通配符)
└── List<? super Integer> // 子类型范围:?可以是Integer或其父类(如NumberObject
└── List<Number> // 具体类型(非通配符)

反直觉的两种子类关系,子类意味着类型兼容

  1. Pair<? extends Employee>意味着它的子类 可以是Pair<Employee>Pair<Manger>
  2. Pair<? super Employee>意味着它的子类(而不是超类)可以是Pair<Employee>Pair<Object>

PECS 原则

编译器在编译的并不会真的把 ? 换成具体类型-虽然我们经常在概念上这样想

PECS 讨论的是,Employee 作为目标类型,什么时候只能消费-作为参数/右值,什么时候只能生产-作为返回值/左值

? extends T作为一个 type parameter 证明可以在 T t 处读
? super T作为一个 type parameter 证明可以在 T t 处写

这个原则很好地示范了定义(简单性)-例子(复杂性)-助记符(简单性)之间的关系。

1
2
3
4
5
6
7
8
9
10

public void foo(Collection<? extends Number> e) {
// 编译错误,因为e可能 Collection<Integer> 或者 Collection<Long>
e.add(1);
}

// 合法
public Collection<? extends Number> bar() {
return new ArrayList<Integer>();
}

编译器不知道,所以干脆不允许 set/call(消费),但可以get。

1
2
3
4
5
6
7
8
9
public void foo(Collection<? super Number> e) {
// 合法,因为 1 可以被加入到 Collection<Number> 和 Collection<Object> 里
e.add(1);
}

// 非法,因为这里只允许赋值给 Collection<Number> 和 Collection<Object>
public Collection<? super Number> bar() {
return new ArrayList<Integer>();
}

用赋值和设值,用 collection 的 get 和 add 比较好理解这个规则。

日期的例子-?和 T 同时出现

LocalDateChronoLocalDate的子类(顺序就是这样,没有反过来),但 ChronoLocalDate 已然实现了 comparable<ChronoLocalDate>。这时候 LocalDate 的比较方法就应该声明成<T extends Comparable<? super T>>。这是减轻 caller 程序员负担,而把复杂度留给库本身的一种做法。

还有一个例子:int compareTo(? super T),这意味着通配符不是T的实参

从 A 派生出 B,则 B 的 comparable 方法必须声明为可以支持 super,这样对 A 的 compare 才能同时兼容 A、B - 而不只是 B,Lists 的 removeIf 方法的谓词同理。

举例,Java 8 中的 ArrayList 有个 removeIf 的方法,它的参数是个 predicate,但这个 predicate 的实参可以是,比如 Employee,也可以是 Object(用上了 super):default boolean removeIf(Predicate<? super E> filter)

更严格的<?>

?在没有其他类型 extends/super 的时候,只能在 get 赋值用,等同于<? exntends Object>。再次强调,?不是T的实参。

Pair<?>是个没用窝囊(wimpy)的类型,它的setFirst(? object) 方法甚至无法被使用-试想,根据 PECS 法则,setFirst怎样确定它的设值是兼容某个类型的?而getFirst方法也只能赋给 Object。

但 Pair 的 set 方法可以使用任意类型,而 get 方法可以赋给 Object。

但如果有些场景只是从某类Pair<T>内部读值,那么Pair<?>Pair<T> 更加简洁易读。

通配符捕获:单层<?>参数方法套着单层<T>参数方法,而逻辑是在内层方法里写的

? 不是 type variable,是 wildcard variable。

通配符捕获要解决的问题是:我们无法用 ? 来写代码,不能当作左值的类型来用。? a = Pair.getFirst() 是不合法的。所以我们无法写 swap 方法:

通配符捕获的解法是:引入一个 T 来捕获通配符,用 T 来写代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31

public class WildcardCapture {
// 辅助泛型方法:捕获通配符类型
private static <T> void swapHelper(List<T> list, int i, int j) {
// 在方法体内可以随意使用 temp。用有值的实例来获取泛型变量。
T temp = list.get(i);
list.set(i, list.get(j));
list.set(j, temp);
return temp;

}

// 对外暴露的通配符方法
public static void swap(List<?> list, int i, int j) {
Object o = swapHelper(list, i, j); // 通配符捕获在此发生
}
}
// 甲骨文的例子
public class WildcardFixed {

void foo(List<?> i) {
fooHelper(i);
}

// Helper method created so that the wildcard can be captured
// through type inference.
private <T> void fooHelper(List<T> l) {
l.set(0, l.get(0));
}

}

在这里,T 捕获了通配符。T 不知道 ? 的具体类型,但知道它是某个确定类型-就当 Object 用。

编译器只有在确定 T 可以捕获确定的通配符的时候才允许编译通过。例如List<Pair<?>> 多了一个间接层,一个 list 可能有不同的 Pair<?>(这足以证明?即使是在一个数据结构里,也可能产生多个类型Pair1<?>内部的的?是同一种?,但是 Pair2<?>完全是另一种),持有不同的具体类型,编译器不会允许捕获 List<Pair<T>>里的 T。

1
2
3
4
5
6
7
8
9
10
11
12
public class WildcardFixed {

void foo(List<List<?>> i) {
fooHelper(i);
}

// not working
private <T> void fooHelper(List<List<T>> l) {
l.set(0, l.get(0));
}

}

需要的类型: List <List<T>> 提供的类型: List <List<?>> 原因: 不兼容的相等约束: T 和 ?

幽灵类型

例子来自于《Java中的幽灵类型》《你真的懂 Builder 设计模式吗?论如何实现真正安全的 Builder 模式》

忽略了特别复杂的阶段式构造,因为它是与泛型无关的。

构造类型的用法

  1. 用接口子类当作状态,而不是枚举,这样可以作为 generic 的bound使用。
  2. 定义泛型类,泛型类只有私有构造器,不允许外部创建,因为这是泛型类,所以构造器在调用的时候是一定可以带有类型见证的,如new Plane<Landed>(10)
  3. 定义初始构造方法,只允许生成初始态实例。
  4. 关键:定义复制构造方法,允许? extend FlightStatus这个类型族的实例相互拷贝,这样产生了外层方法。
  5. 定义各种跳转方法, 关键:在签名和返回值上做幽灵类型的切换,内部只使用同一个参数族方法复制来复制去,每次复制的时候使用新的类型见证return new Plane<Flying>(p);

不基于接口的用法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
public class Team<S> {
private List<Member> members;
...
// phantom types
static abstract class READY {}
static abstract class STARTED {}
static abstract class END {}
}

public class Game {
public Team<Team.STARTED> start(Team<Team.READY> team) {...}
public Team<Team.END> end(Team<Team.STARTED> team) {...}
...
}

builder pattern

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
public class User {
private final String name;
private final String password;

private User(String name, String password) {
this.name = name;
this.password = password;
}

public static User build(Builder<TRUE, TRUE> builder) {
return new User(builder.name, builder.password);
}

public static Builder<FALSE, FALSE> builder() {
return new Builder<FALSE, FALSE>();
}

public static class Builder<HNAME, HPASSWORD> {
private String name;
private String password;

private Builder() {
}

private Builder(String name, String password) {
this.name = name;
this.password = password;
}

public Builder<TRUE, HPASSWORD> name(String name) {
this.name = name;
return new Builder<TRUE, HPASSWORD>(name, this.password);
}

public Builder<HNAME, TRUE> password(String password) {
this.password = password;
return new Builder<HNAME, TRUE>(this.name, password);
}
}
// phantom types
static abstract class TRUE {}
static abstract class FALSE {}
}
  1. 把 builder 泛型化,每个字段对应一个泛型参数。
  2. 定义 TRUE、FALSE 两个状态。
  3. 构造器要求字段对应的泛型参数全部都是 TRUE。
  4. 每个 builder 只保留一个本参数对应的类型实参,其他部分仍然泛化-部分泛型
  5. 如果一个字段有默认值,它不具有对应的泛型参数。

泛型与反射

泛型与 class

String.classClass<String>的一个 object。

Class 的 type variable 实际上限制了它方法的种种返回值。

Java 在5以后,确切地把各种constructor相关的方法都<T>参数化了。

反射能够在擦除后知道些什么

可以知道的以下东西:

  • 一个方法或者类型有个 type parameter T。
  • T 有 super 或者 extends 的 bound。
  • T 有个 wildcard variable - wildcard 和 T 会同时存在。

不可以知道的东西

  • 到底运行时绑定的 type parameter 是什么?how type parameter were resolved.

获取类型信息

Type[] actualTypes = ((ParameterizedType) type).getActualTypeArguments()什么时候能够得到String

丢失类型信息就无法 ParameterizedType 转型。

什么情况下会丢失类型信息

  • 方法参数(如void process(List<T> list))。
  • 局部变量(如List<T> list = new ArrayList<>())。
  • 泛型方法的类型推断(如Collections.emptyList())。

什么情况下能够获取到类型信息

  • 类或接口继承时显式指定泛型参数​​(如 class MyList extends ArrayList)。
  • 使用匿名内部类:List<String> list = new ArrayList<String>() {}; // 匿名子类
    • gson的类型令牌:List<String> list = gson.fromJson(json, new TypeToken<List<String>>() {}.getType());// 这里获得的 Type 是最顶层的java Type 接口
1
2
3
4
5
6
7
8
9
10
11
12
13
14
public abstract class TypeToken<T> {
private final Type type;

public TypeToken() {
Type superClass = getClass().getGenericSuperclass();
this.type = ((ParameterizedType) superClass).getActualTypeArguments()[0];
}

public Type getType() { return type; }
}

// 使用示例
TypeToken<List<String>> token = new TypeToken<List<String>>() {};
System.out.println(token.getType()); // java.util.List<java.lang.String>

反射的类型 hiarachy

java类型系统.png

反射的基础类型是Type接口,它有五个子类:

Class

描述具体类型(而不是接口)。我们常见的类型如果本身不带有<>就属于这一类型。对 Java 1.5 以后的泛型,一个Map.classMap<String, Integer>的 class 在求等上完全相等,但唯有引入其他 Type 子类才能说明StringInteger的存在。

  • 最常见的 Type 实现类。
  • 代表原始类型(raw types)和基本类型(primitive types)
1
2
Class<?> clazz = String.class;
Class<?> arrayClass = int[].class; // class [I

TypeVariable(类型变量)

  • 表示泛型中的类型变量
  • 例如泛型定义中的 T、K、V 等
1
2
3
4
5
6
7
8
public class Container<T> {
private T value;

public void showType() {
TypeVariable<?>[] typeParameters = Container.class.getTypeParameters();
// typeParameters[0] 就是 T
}
}

ParameterizedType(参数化类型)

  • 描述泛型类或接口类型,如Comparable<? super T>- 注意,如果丢失了类型信息(非子类型),此处是 T 而不是 String。常见的 ParameterizedType 有 Collection 的 Class 实例。可以简单把 ParameterizedType 理解为 指向 Collection 类的特殊的 class,它携带的 actualParamters 就是String/Integer 的 class,而 WildcardType 的 bounds 就是 extends A 的那个 A 的 class。它还有一个 getRawType(也就是 Map),和 ownerType(也就是 Map 之于 Entry)。
  • 主要方法:
    • getActualTypeArguments(): 获取泛型参数的实际类型
    • getRawType(): 获取原始类型
    • getOwnerType(): 获取所属类型
1
2
3
4
5
6
// Map<String, List<Integer>>
Map<String, List<Integer>> map = new HashMap<>();
Type type = map.getClass().getGenericSuperclass();
if (type instanceof ParameterizedType) {
Type[] actualTypeArguments = ((ParameterizedType) type).getActualTypeArguments();
}

GenericArrayType(泛型数组类型)

GenericArray 接口,描述泛型数组如 T[]。这可以被用在获取方法参数上。获取到具体参数以后,可以把它转化为 ParameterizedType,然后获取它携带的 actualParamters。例如:T[]List<String>[]

1
2
3
4
5
6
7
8
9
10
11
12
public class Example<T> {
T[] array;
List<String>[] lists;

public void check() {
Field arrayField = Example.class.getDeclaredField("array");
Type arrayType = arrayField.getGenericType();
if (arrayType instanceof GenericArrayType) {
Type componentType = ((GenericArrayType) arrayType).getGenericComponentType();
}
}
}

WildcardType(通配符类型)

表示通配符泛型,如 ?? extends Number? super Integer。它必然可以找到 up bound(最起码有 Object),不一定有 lower bound。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
public class WildcardExample {
List<? extends Number> numbers;

public void examine() {
Field field = WildcardExample.class.getDeclaredField("numbers");
Type type = field.getGenericType();
if (type instanceof ParameterizedType) {
Type[] actualTypeArguments = ((ParameterizedType) type).getActualTypeArguments();
WildcardType wildcard = (WildcardType) actualTypeArguments[0];
Type[] upperBounds = wildcard.getUpperBounds(); // 获取上界
Type[] lowerBounds = wildcard.getLowerBounds(); // 获取下界
}
}
}

主要使用场景

  1. 反射获取泛型信息
  2. 泛型类型系统的实现
  3. 框架开发中的类型判断和处理
  4. 注解处理器的开发
  5. 序列化/反序列化框架

这些类型系统的实现让 Java 能够在运行时获取和处理泛型信息,尽管 Java 的泛型是通过类型擦除实现的,但通过这些接口我们仍然可以在运行时获取到泛型的类型信息。

TypeLiteral

有一个可以捕获多重泛型的实参的方案。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68

// 1,当我们拿到一个Class,用Class. getGenericInterfaces()方法得到Type[],也就是这个类实现接口的Type类型列表。
// 2,当我们拿到一个Class,用Class.getDeclaredFields()方法得到Field[],也就是类的属性列表,然后用Field. getGenericType()方法得到这个属性的Type类型。
// 3,当我们拿到一个Method,用Method. getGenericParameterTypes()方法获得Type[],也就是方法的参数类型列表。

/**
* This constructor must be invoked from an anonymous subclass
* as new TypeLiteral<. . .>(){}.
*/
public TypeLiteral()
{
//
Type parentType = getClass().getGenericSuperclass();
if (parentType instanceof ParameterizedType)
{
// 重要:这里得到的 Type 是真的 Class 实例,不过做类型判定不能用 instanceof,只能用 assignableFrom。因为 Class 的自身类型和指代类型是不一样的。当然 Type 也可能是 ParameterizedType 等任意子类型,它们也可以通过 getClass 理解自身的类型。
type = ((ParameterizedType) parentType).getActualTypeArguments()[0];
}
else
throw new UnsupportedOperationException(
"Construct as new TypeLiteral&lt;. . .&gt;(){}");
}

// 上述 api 的例子
public static void main(String[] args) {
List<String> list = new ArrayList<>();
Class<? extends List> firstClazz = list.getClass();
Type genericSuperclass = firstClazz.getGenericSuperclass();
// java.util.AbstractList<E>
System.out.println(genericSuperclass);

ParameterizedType parameterizedSuperType = (ParameterizedType) genericSuperclass;
Type[] actualTypeArguments = parameterizedSuperType.getActualTypeArguments();
// E
Type actualTypeArgument = actualTypeArguments[0];
System.out.println(actualTypeArgument);

// java.util.AbstractCollection<E>
genericSuperclass = firstClazz.getSuperclass().getGenericSuperclass();
System.out.println(genericSuperclass);

parameterizedSuperType = (ParameterizedType) genericSuperclass;
actualTypeArguments = parameterizedSuperType.getActualTypeArguments();
// E
actualTypeArgument = actualTypeArguments[0];
System.out.println(actualTypeArgument);
}

// 但这个方法有一个例外,如:

private static class StringList extends ArrayList<String> {
}

public static void main(String[] args) {
List<String> list = new StringList();
Class<? extends List> firstClazz = list.getClass();
Type genericSuperclass = firstClazz.getGenericSuperclass();
// java.util.ArrayList<java.lang.String>
System.out.println(genericSuperclass);

ParameterizedType parameterizedSuperType = (ParameterizedType) genericSuperclass;
Type[] actualTypeArguments = parameterizedSuperType.getActualTypeArguments();
// class java.lang.String 类型不再是 typevariable
Type actualTypeArgument = actualTypeArguments[0];
System.out.println(actualTypeArgument);
}

// 这个例外下面有用。

运行时捕获类型参数的方法

方法一

1
2
3
4
5
6
7
8
9
10
11
12
public class GenericClass<T> {

private final Class<T> type;

public GenericClass(Class<T> type) {
this.type = type;
}

public Class<T> getMyType() {
return this.type;
}
}

方法二

typetools

方法三

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
public abstract class TypeReference<T> implements Comparable<TypeReference<T>>
{
protected final Type _type;

protected TypeReference()
{
Type superClass = getClass().getGenericSuperclass();
if (superClass instanceof Class<?>) { // sanity check, should never happen
throw new IllegalArgumentException("Internal error: TypeReference constructed without actual type information");
}
/* 22-Dec-2008, tatu: Not sure if this case is safe -- I suspect
* it is possible to make it fail?
* But let's deal with specific
* case when we know an actual use case, and thereby suitable
* workarounds for valid case(s) and/or error to throw
* on invalid one(s).
*/
_type = ((ParameterizedType) superClass).getActualTypeArguments()[0];
}
}

这个方法来自于 JDK 5 的作者的博客《super-type-tokens》

  1. abstract class 保证了这个类型必须通过子类确定,这样getGenericSuperclass必定会得到一个 ParameterizedType 而不仅仅是一个 GenericType。
  2. implements Comparable<TypeReference<T>> 并不是真的希望子类覆写一个比较方法,而是希望子类型不要实现成一个 raw type。

保证了这两天_type一定是一个 concret class。

各种泛型黑魔法

获取通配符的 bound

这里不是编程时拿?来用,而是确切知道?的 bound 是什么:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
public TestClass {

// 任何相关的问题都可以写一个成员类来直接通过反射来实验
private List<? extends Number> listNum;

@Test
@SuppressWarnings("unchecked")
void testGetWildCardType() throws NoSuchFieldException {

final Field fieldNum = TestClass.class.getDeclaredField("listNum");
// genericType几乎必然是 ParameterizedTypeImpl 的实例
final Type genericType = fieldNum.getGenericType();
final ParameterizedType parameterizedType = (ParameterizedType) genericType;
final Type[] actualTypeArguments = parameterizedType.getActualTypeArguments();
for (Type actualTypeArgument : actualTypeArguments) {
// wildCard 隐藏在 actualTypeArguments 里
if (actualTypeArgument instanceof WildcardType) {
final WildcardType wildcardType = (WildcardType) actualTypeArgument;
final Type[] lowerBounds = wildcardType.getLowerBounds();
// 所有的 bound 都是 class
log.info(Arrays.toString(lowerBounds));
final Type[] upperBounds = wildcardType.getUpperBounds();
log.info(Arrays.toString(upperBounds));
}
}

Assertions.assertTrue(true);
}
}

Optional 里通配符转成类型参数

可见@SuppressWarnings("unchecked") java 的基础库里到处都是。

1
2
3
4
5
6
7
8
9
10
/**
* Common instance for {@code empty()}.
*/
private static final Optional<?> EMPTY = new Optional<>();

public static<T> Optional<T> empty() {
@SuppressWarnings("unchecked")
Optional<T> t = (Optional<T>) EMPTY;
return t;
}

确认消费类型的两种方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
/**
* 验证一个类型是不是另一个类型的参数化子类
* <p>
* A implements B<C>
* 则 isGenericSubClass(A.class, B.class, C.class) 为 true
* 注意,如果存在 D extends C,C extends F
* D 作为第三个参数返回 true,而 F 返回 false
*
* @param subClass 子类型
* @param genericSuperClass 超类型,可以是借口
* @param targetGenericActualParameter 类型实参
* @return 认定结果
*/
public static boolean isGenericSubClass(Class<?> subClass, Class<?> genericSuperClass, Class<?> targetGenericActualParameter) {

if (!genericSuperClass.isAssignableFrom(subClass)) {
return false;
}
final Type genericSuperclass = subClass.getGenericSuperclass();

if (genericSuperclass instanceof ParameterizedType) {
final ParameterizedType parameterizedType = (ParameterizedType) genericSuperclass;
final Type rawType = parameterizedType.getRawType();
// 这里可以确认 Collection 是不是 Collection-而不是 List
final boolean rawTypeMatch = rawType == genericSuperClass;
final Type[] actualTypeArguments = parameterizedType.getActualTypeArguments();
// 这两行很重要,基本上能够获取类型实参就靠它了,但我们也只能得到 class
for (Type type : actualTypeArguments) {
if (type instanceof Class) {
if (rawTypeMatch && ((Class<?>) type).isAssignableFrom(targetGenericActualParameter)) {
return true;
}
}
}
}


final Type[] genericInterfaceTypes = subClass.getGenericInterfaces();
for (Type genericInterfaceType : genericInterfaceTypes) {
if (genericInterfaceType instanceof ParameterizedType) {
final ParameterizedType parameterizedType = (ParameterizedType) genericInterfaceType;
final boolean rawTypeMatch = parameterizedType.getRawType() == genericSuperClass;
final Type[] actualTypeArguments = parameterizedType.getActualTypeArguments();
// 这两行很重要,基本上能够获取类型实参就靠它了,但我们也只能得到 class
for (Type type : actualTypeArguments) {
if (type instanceof Class) {
if (rawTypeMatch && ((Class<?>) type).isAssignableFrom(targetGenericActualParameter)) {
return true;
}
}
}
}
}
return false;
}

/**
* isGenericSubClass 的 Spring 版本,只能验证第一个 TypeParameter,验证第二个参数必然返回 false
*
* @param subClass 子类型
* @param genericSuperClass 超类型,可以是借口
* @param targetGenericActualParameter 类型实参
* @return 认定结果
*/
public static boolean isParameterized(Class<?> subClass, Class<?> genericSuperClass, Class<?> targetGenericActualParameter) {
final ResolvableType generic = ResolvableType.forClass(subClass).as(genericSuperClass).getGeneric();
return generic.isAssignableFrom(ResolvableType.forClass(targetGenericActualParameter));
}

空泛型数组的拷贝方法

1
2
3
4
5
6
7
8
9
10
11
12
@Override
@SuppressWarnings("unchecked")
public <T> T[] toArray(T[] a) {
int size = size();
if (a.length < size)
return Arrays.copyOf(this.a, size,
(Class<? extends T[]>) a.getClass());
System.arraycopy(this.a, 0, a, 0, size);
if (a.length > size)
a[size] = null;
return a;
}

这个方法的特点:

  1. 自动调整数组大小
  2. 可以像 go 一样往参数填充数据
  3. 类型安全:String[] rightWay = list.toArray(new String[0]);优于String[] wrongWay = (String[]) list.toArray();

其他资料

angelikalanger 的 JavaGenericsFAQ