Poison

Generics

Generics and Subtyping

先看如下代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
package me.tianshuang;

import java.util.ArrayList;
import java.util.List;

public class GenericsTest {

public static void main(String[] args) {
List<String> ls = new ArrayList<>(); // 1
List<Object> lo = ls; // 2
}

}

其中,第二行将造成编译错误,通常来说,如果 FooBar 的子类型(子类或子接口),而 G 是某种泛型类型声明,则 G<Foo> 不是 G<Bar> 的子类型。这可能听起来违反直觉,可以这样理解,假设第二行编译成功且程序可以运行,那么我们可以往 lo 中放入任意类型的对象,而此时 lols 指向的其实是同一个 List 的实例,那么我们可以从 ls 中获取到通过 lo 放入的对象,那么此时获取的对象的真实类型就不一定是 String 了,而是我们往 lo 中放入的对象的类型,比如以下代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
package me.tianshuang;

import java.util.ArrayList;
import java.util.List;

public class GenericsTest {

public static void main(String[] args) {
List<String> ls = new ArrayList<>(); // 1
List<Object> lo = ls; // 2
lo.add(new Object()); // 3
String s = ls.get(0); // 4: Attempts to assign an Object to a String!
}

}

其中第四行代码将尝试将一个 Object 类型的对象赋值给 String 类型的对象,所以 Java 编译器会阻止这种情况发生,在第二行时就会产生编译时错误,避免产生上述的问题。以上代码演示了泛型不是协变的,而在 Java 中数组是协变的,如果根据 《Effective Java中文版(第3版)》 泛型这一章中的说法,Java 中数组的设计是有缺陷的,比如以下代码:

1
2
3
4
5
6
7
8
9
10
package me.tianshuang;

public class GenericsTest {

public static void main(String[] args) {
Object[] objectArray = new Long[1];
objectArray[0] = "I don't fit in";
}

}

编译并不会报错,但是在执行时会抛出运行时异常 ArrayStoreException,异常信息如下:

1
2
Exception in thread "main" java.lang.ArrayStoreException: java.lang.String
at me.tianshuang.GenericsTest.main(GenericsTest.java:7)

利用数组,你会在运行时发现所犯的错误,利用列表,则可以在编译时发现错误。

Wildcards

Collection<?> 被称作未知类型的集合,在工具类 java.util.Collections 中可以看到对通配符的大量使用,如:java.util.Collections#shuffle(java.util.List<?>),什么时候应该使用通配符呢?可以看一个例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
package me.tianshuang;

import java.util.ArrayList;
import java.util.Collection;

public class GenericsTest {

public static void main(String[] args) {
Collection<String> ls = new ArrayList<>(); // 1
printCollection(ls); // 2
}

static void printCollection(Collection<Object> c) {
for (Object e : c) {
System.out.println(e);
}
}

}

此时,第二行会编译出错,这是因为 Collection<String> 不是 Collection<Object> 的子类型,即 Collection<Object> 不是 Collection<String> 的超类型,那各种集合类型的超类型是什么呢?是 Collection<?>,表示元素类型匹配任意东西的集合,将以上代码进行微调后:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
package me.tianshuang;

import java.util.ArrayList;
import java.util.Collection;

public class GenericsTest {

public static void main(String[] args) {
Collection<String> ls = new ArrayList<>(); // 1
printCollection(ls); // 2
}

static void printCollection(Collection<?> c) {
for (Object e : c) {
System.out.println(e);
}
}

}

即可通过编译,此时我们可以使用任意类型的集合来调用 printCollection 函数,注意在 printCollection 函数实现中,我们仍然可以从 c 中读取元素并赋予类型 Object,这总是安全的,因为无论集合的实际类型是什么,集合都包含对象。但是,向其中添加任意对象是不安全的,如以下代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
package me.tianshuang;

import java.util.ArrayList;
import java.util.Collection;

public class GenericsTest {

public static void main(String[] args) {
Collection<?> c = new ArrayList<String>(); // 1
c.add(new Object()); // 2: Compile time error
}

}

第二行将造成编译错误,因为我们不知道 c 集合中元素的类型代表什么,当实际的类型参数为 ? 时,它代表某种未知类型,我们传递给 add 方法的任何参数都必须是这种未知类型的子类型,因为我们不知道那是什么类型,所以我们不能传入任何对象,唯一的例外是 null,它是所有类型的成员。

另一方面,给定一个 List<?>,我们可以调用 get() 并使用结果。结果类型是未知类型,但我们始终知道它是一个对象。因此,将 get() 的结果分配给 Object 类型的变量或将其作为参数传递给需要类型 Object 的参数是安全的。

Class Literals as Runtime-Type Tokens

JDK 5.0 中的变化之一是 java.lang.Class 类是泛型的。这是一个将泛型用于容器类以外的东西的有趣示例。文档可以参见:Class,其中对类型参数的解释为:

Type Parameters:
T - the type of the class modeled by this Class object. For example, the type of String.class is Class. Use Class<?> if the class being modeled is unknown.

T 代表 Class 对象所表示的类型,这可用于提高反射代码的类型安全性。

fastjson 的核心类 JSON 中,可以看到对 Class<T> 的使用:

1
2
3
public static <T> T parseObject(String text, Class<T> clazz) {
return parseObject(text, clazz, new Feature[0]);
}

记录以上泛型相关的知识点是因为最近编写了基于 HTTP 请求的 SDK 库,在笔者的印象中,泛型大多数时候用于集合相关类,参考其他的请求库时发现不少实现将泛型用于对 API 请求的封装,类似于 Class<T> 的使用方式,且含有不少对 ? 的使用及对结果转换时 Class<T> 的使用,于是记录下来。

References

Generics and Subtyping (The Java™ Tutorials > Bonus > Generics)
Wildcards (The Java™ Tutorials > Bonus > Generics)
Class Literals as Runtime-Type Tokens (The Java™ Tutorials > Bonus > Generics)
java - Why are arrays covariant but generics are invariant? - Stack Overflow
AngelikaLanger.com - Java Generics FAQs - Frequently Asked Questions - Angelika Langer Training/Consulting