Appearance
Java 基础:泛型
1. 泛型方法
注意,类型变量放在修饰符(例如 public static)的后面,并在返回类型的前面。如:
Java
class ArrayAlg {
public static <T> T getMiddle(T... a) {
return a[a.length / 2];
}
}1
2
3
4
5
2
3
4
5
当调用一个泛型方法时,可以把具体类型包围在尖括号中,放在方法名前面:
Java
String middle = ArrayAlg.<String>getMiddle("John", "Q.", "Public");当编译器有足够的信息推断出你想要的方法时,方法调用的类型参数可以省略。
2. 类型变量限定
类型变量限定的语法格式:
EBNF
'<' T 'extends' BoundingType {'&' BoundingType} '>'可以根据需要拥有多个接口超类型,但最多有一个限定可以是类。如果有一个类作为限定,它必须是限定列表中的第一个限定。
3. 泛型代码和虚拟机
3.1. 类型擦除
定义泛型类型时,会自动生成一个对应的原始类型(raw type)。这个原始类型的名字就是去掉类型参数后的泛型类型名。类型变量会被擦除(erased), 并用其限定类型进行替换。原始类型会用第一个限定类型替代类型变量,如果没有指定限定类型,则用 Object 替代。例如,Pair<T> 的原始类型如下所示:
Java
public class Pair {
private Object first;
private Object second;
public Pair(Object first, Object second) {
this.first = first;
this.second = second;
}
public Object getFirst() { return first; }
public Object getSecond() { return second; }
public void setFirst(Object newValue) { first = newValue; }
public void setSecond(Object newValue) { second = newValue; }
}1
2
3
4
5
6
7
8
9
10
11
12
13
14
2
3
4
5
6
7
8
9
10
11
12
13
14
假定我们声明了一个稍有不同的类型:
Java
public class Interval<T extends Comparable & Serializable> implements Serializable {
private T lower;
private T upper;
// ...
public Interval(T first, T second) {
if (first.compareTo(second) <= 0) { lower = first; upper = second; }
else { lower = second; upper = first; }
}
}1
2
3
4
5
6
7
8
9
2
3
4
5
6
7
8
9
原始类型 Interval 如下所示:
Java
public class Interval implements Serializable {
private Comparable lower;
private Comparable upper;
// ...
public Interval(Comparable first, Comparable second) { ... }
}1
2
3
4
5
6
2
3
4
5
6
如果我们调整一下限定类型声明的顺序 class Interval<T extends Serializable & Comparable> 会发生什么。如果这样做,原始类型会用 Serializable 替换 T,而编译器在必要时要向 Comparable 插入强制类型转换。因此,为了提高效率,我们应该将标签(tagging)接口(即没有方法的接口)放在限定列表的末尾。
3.2. 方法上的擦除
方法的擦除带来了两个复杂问题。先看一个示例:
Java
class DateInterval extends Pair<LocalDate> {
public void setSecond(LocalDate second) {
if (second.compareTo(getFirst()) >= 0)
super.setSecond(second);
}
// ...
}1
2
3
4
5
6
7
2
3
4
5
6
7
我们覆写(override)了 setSecond 方法,以确保 second 永远不会小于 first。但是其背后的发生事情却并没有那么简单。
首先,在类型擦除后 DateInterval 会从 Pair 继承一个 public void setSecond(Object second) 的方法,该方法的入参类型(Object)显然和 DateInterval 中定义的(LocalDate)不一样。考虑下面情形:
Java
var interval = new DateInterval(...);
Pair<LocalDate> pair = interval; // OK--assignment to superclass
pair.setSecond(aDate);1
2
3
2
3
显然,在这里我们希望执行 pair.setSecond(aDate) 时调用的是 DateInterval.setSecond(LocalDate) 方法。为了解决这个问题,编译器在 DateInterval 类中生成了一个桥方法(bridge method):
Java
public void setSecond(Object second) { setSecond((LocalDate) second); }变量 pair 被声明为 Pair<LocalDate>,它的 setSecond 方法为 setSecond(Object)。虚拟机会在 pair 引用的对象(DateInterval)上调用这个方法,也就是 DateInterval.setSecond(Object) 方法 —— 桥方法。在桥方法中调用了 DateInterval.setSecond(LocalDate) 方法,而这正是我们想要的。
再考虑另外一种情况,假设 DateInterval 也覆写了 getSecond 方法:
Java
class DateInterval extends Pair<LocalDate> {
public LocalDate getSecond() { return (LocalDate) super.getSecond(); }
// ...
}1
2
3
4
2
3
4
同样地,此时在 DateInterval 中会有两个 getSecond 方法:
Java
LocalDate getSecond() // defined in DateInterval
Object getSecond() // overrides the method defined in Pair to call `LocalDate getSecond()` method1
2
2
在实际的开发过程中,我们无法同时编写以上两个方法。但是,在虚拟机中,会由参数类型和返回类型共同指定一个方法。因此,编译器可以为两个仅返回类型不同的方法生成字节码,虚拟机能够正确地处理这种情况。
Tip 1
实际上,桥方法不仅用于泛型类型。在协变返回类型中也有提到,一个方法覆盖另一个方法时,可以指定一个更严格的返回类型,这是合法的。例如:
Javapublic class Employee implements Cloneable { public Employee clone() throws CloneNotSupportedException { ... } }1
2
3实际上,
Employee类有两个clone方法:JavaEmployee clone() // defined above Object clone() // synthesized bridge method, overrides Object.clone1
2
Tip 2
总之,对于 Java 泛型的转换,需要记住以下几个事实:
- 虚拟机中没有泛型,只有普通的类和方法;
- 所有的类型参数都会替换为它们的限定类型;
- 会合成桥方法来保持多态;
- 为保持类型安全性,必要时会插入强制类型转换;
3.3. 调用遗留代码
考虑将一个方法返回的原始对象赋值给一个泛型对象,例如将 Dictionary 赋值给 Dictionary<Integer, String>,亦或是将一个泛型对象转换为原始对象作为参数传入某个方法。因为方法的内部没有泛型的约束,所以我们不能确保代码的安全性,因此你会收到来自编译器的警告。如果你能确保该方法是安全的,则我们可以使用注解来屏蔽这个警告:
Java
@SuppressWarnings("unchecked")
Dictionary<Integer, Components> labelTable = slider.getLabelTable(); // no warning1
2
2
或者,可以对整个方法加注解:
Java
@SuppressWarnings("unchecked")
public void configureSlider() { ... }1
2
2
4. 限制与局限性
4.1. 不能用基本类型实例化类型参数
不能用基本类型代替类型参数。因此,没有 Pair<double>,只有 Pair<Double>。
4.2. 运行时类型查询只适用于原始类型
如果试图查询一个对象是否属于某个泛型类型,你会得到一个编译器错误(使用 instanceof 时),或者得到一个警告(使用强制类型转换时):
Java
if (a instanceof Pair<String>) // ERRORJava
if (a instanceof Pair<T>) // ERRORJava
Pair<String> p = (Pair<String>) a; // WARNING, can only test that a is a Pair同样的道理,getClass 方法总是返回原始类型。例如:
Java
Pair<String> stringPair = ...;
Pair<Employee> employeePair = ...;
if (stringPair.getClass() == employeePair.getClass()) // they are equal1
2
3
2
3
其比较的结果是 true,这是因为两次 getClass 调用都返回 Pair.class。
4.3. 不能创建参数化类型的数组
不能实例化参数化类型的数组,例如:
Java
var table = new Pair<String>[10]; // ERROR需要说明的是,只是不允许创建这些数组,而声明类型为 Pair<String>[] 的变量仍是合法的。
Note 1
可以声明通配类型的数组,然后进行强制类型转换:
Javavar table = (Pair<String>[]) new Pair<?>[10];结果将是不安全的。如果在
table[0]中存储一个Pair<Employee>,然后对table.getFirst()调用一个String方法,会得到一个ClassCastException异常。
Note 2
如果需要收集参数化类型对象,简单地使用
ArrayList:ArrayList<Pair<String>>更安全、有效。
4.4. Varargs 警告
上一节中已经了解到,Java 不支持泛型类型的数组。这一节中我们再来讨论一个相关的问题:向参数个数可变的方法传递一个泛型类型的实例:
Java
public static <T> void addAll(Collection<T> coll, T... ts) {
for (T t : ts) coll.add(t);
}1
2
3
2
3
实际上参数 ts 是一个数组,包含提供的所有实参。现在考虑以下调用:
Java
Collection<Pair<String>> table = ...;
Pair<String> pair1 = ...;
Pair<String> pair2 = ...;
addAll(table, pair1, pair2);1
2
3
4
2
3
4
为了调用这个方法,Java 虚拟机必须建立一个 Pair 数组,这就违反了前面的规则。不过,对于这种情况,规则有所放松,你只会得到一个警告,而不是错误。可以采用两种方法来抑制这个警告。一种方法是为包含 addAll 调用的方法增加注解 @SuppressWarnings("Unchecked")。或者在 Java 7 中,还可以用 @SafeVarargs 直接注解 addAll 方法:
Java
@SafeVarargs
public static <T> void addAll(Collection<T> coll, T... ts)1
2
2
现在就可以提供泛型类型来调用这个方法了。对于任何只需要读取参数数组元素的方法(这肯定是最常见的情况),都可以使用这个注解。
Note:
@SafeVarargs只能用于声明为static、final或 Java 9 中private的构造器和方法。所有其他方法都可能被覆盖,使得这个注解没有什么意义。
4.5. 不能实例化类型变量
例如,下面的 Pair<T> 构造器就是非法的:
Java
public Pair() { first = new T(); second = new T(); } // ERROR在 Java 8 之后,最好的解决办法是让调用者提供一个构造器表达式。例如:
Java
Pair<String> p = Pair.makePair(String::new);makePair 方法接收一个 Supplier<T> 函数式接口,表示一个无参数而且返回类型为 T 的函数:
Java
public static <T> Pair<T> makePair(Supplier<T> constr) {
return new Pair<>(constr.get(), constr.get());
}1
2
3
2
3
再或者,比较传统的解决方法是通过反射调用 Constructor.newInstance 方法来构造泛型对象:
Java
public static <T> Pair<T> makePair(Class<T> cl) {
try {
var constructor = cl.getConstructor();
return new Pair<>(constructor.newInstance(), constructor.newInstance());
}
catch (Exception e) { return null; }
}1
2
3
4
5
6
7
2
3
4
5
6
7
调用方式如下:
Java
Pair<String> p = Pair.makePair(String.class);Note:
Class类本身是泛型的。例如,String.class是一个Class<String>的实例(事实上,它也是唯一的实例)。因此,makePair方法能够推断出所建立的类型。
4.6. 不能构造泛型数组
数组本身也带有类型,用来监控虚拟机中的数组存储。泛型数组中的类型会被擦除。例如,考虑下面的例子:
Java
public static <T extends Comparable> T[] minmax(T... a) {
T[] result = new T[2]; // ERROR
// ...
}1
2
3
4
2
3
4
类型擦除会让这个方法总是构造 Comparable[2] 数组。
再参考下面这个例子:
Java
public static <T extends Comparable> T[] minmax(T... a) {
var result = new Comparable[2]; // array of erased type
// ...
return (T[]) result; // compiles with warning
}1
2
3
4
5
2
3
4
5
以下调用:
Java
String[] names = ArrayAlg.minmax("Tom", "Dick", "Harry");编译时不会有任何警告。当方法返回后 Comparable[] 引用强制转换为 String[] 时,将会出现 ClassCastException 异常。
因此,我们最好让用户提供一个数组构造器表达式:
Java
String[] names = ArrayAlg.minmax(String[]::new, "Tom", "Dick", "Harry");构造器表达式 String::new 指示一个函数,给定所需的长度,会构造一个指定长度的 String 数组:
Java
public static <T extends Comparable> T[] minmax(IntFunction<T[]> constr, T... a) {
T[] result = constr.apply(2);
// ...
}1
2
3
4
2
3
4
再或者,比较传统的解决方法是通过反射调用 Array.newInstance 方法:
Java
public static <T extends Comparable> T[] minmax(T... a) {
var result = (T[]) Array.newInstance(a.getClass().getComponentType(), 2);
// ...
}1
2
3
4
2
3
4
ArrayList 类的 toArray 方法就没有这么幸运。它需要生成一个 T[] 数组,但没有元素类型。因此,有下面两种不同的形式:
Java
Object[] toArray()
T[] toArray(T[] result)1
2
2
第二个方法接收一个数组参数。如果数组足够大,就使用这个数组。否则,用 result 的元素类型构造一个足够大的新数组。
4.7. 泛型类的静态上下文中类型变量无效
禁止使用带有类型变量的静态字段和方法:
Java
public class Singleton<T> {
private static T singleInstance; // ERROR
public static T getSingleInstance() { // ERROR
if (singleInstance == null) {
// construct new instance of T
}
return singleInstance;
}
}1
2
3
4
5
6
7
8
9
2
3
4
5
6
7
8
9
类型擦除之后,只剩下 Singleton 类,它只包含一个 singleInstance 字段。
4.8. 不能抛出或捕获泛型类的实例
既不能抛出也不能捕获泛型类的对象。实际上,泛型类扩展 Throwable 甚至都是不合法的。例如:
Java
// ERROR -- can't extend Throwable
public class Problem<T> extends Exception { /* ... */ }1
2
2
4.9. 可以取消对检查型异常的检查
Java 异常处理的一个基本原则是,必须为所有检查型异常提供一个处理器。不过可以利用泛型取消这个机制。关键在于以下方法:
Java
@SuppressWarnings("unchecked")
static <T extends Throwable> void throwAs(Throwable t) throws T {
throw (T) t;
}1
2
3
4
2
3
4
下面使用这个技术解决一个棘手的问题。要在一个线程中运行代码,需要把代码放在一个实现了 Runnable 接口的类的 run 方法中。不过这个方法不允许抛出检查型异常。我们将提供一个从 Task 到 Runnable 的适配器,它的 run 方法可以抛出任意异常。
Java
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) {
Task.<RuntimeException>throwAs(e);
}
};
}
}1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
这里我们利用了泛型的擦除机制 “欺骗” 了编译器,让我们免于对该异常做 “包装” 处理。
4.10. 注意擦除后的冲突
考虑以下代码:
Java
public class Pair<T> {
public boolean equals(T value) { return first.equals(value) && second.equals(value); }
}1
2
3
2
3
从概念上讲,它有两个 equals 方法:
Java
boolean equals(String) // defined in Pair<T>
boolean equals(Object) // inherited from Object1
2
2
但是实际上,boolean equals(T) 在擦除后就是 boolean equals(Object) 这与继承来的 Object.equals 方法冲突了。
再考虑另外一种情况:
Java
class Employee implements Comparable<Employee> { ... }
class Manager extends Employee implements Comparable<Manager> { ... } // ERROR1
2
2
这里第二行代码是非法的,这可能与合成的桥方法产生冲突有关系。
5. 通配符类型
5.1. 通配符概念
在通配符类型中,允许类型参数发生变化。例如:
Java
Pair<? extends Employee>Note:注意这里是
?而不是T。
表示任何泛型 Pair 类型,它的类型参数是 Employee 的子类,如 Pair<Manager>,但不是 Pair<String>。类型 Pair<Manager> 是 Pair<? extends Employee> 的子类型,如下图所示:

? extends Employee 是子类型限定,它告诉编译器类型变量是 Employee 或其的某个子类。因此,我们可以正常地调用 getFirst 方法,并将其返回值赋值给一个 Employee 引用;但因为我们不清楚它具体是哪个类型,所以当我们调用 setFirst 方法时,将会提示一个错误:
Java
var managerBuddies = new Pair<Manager>(ceo, cfo);
Pair<? extends Employee> wildcardBuddies = managerBuddies;
Employee first = wildcardBuddies.getFirst(); // OK
wildcardBuddies.setFirst(lowlyEmployee); // compile-time error1
2
3
4
2
3
4
还可以指定超类型限定:
EBNF
'? super' ClassName超类型限定的通配符的行为与子类型限定相反。例如 Pair<? super Manager>,我们可以使用 Manager 或其子类为作为 setFirst 方法的参数进行调用,但是调用 getFirst 方法时,我们只能把它的返回值赋值给一个 Object 引用。
Tip:直观地讲,带有超类型限定的通配符允许你写入一个泛型对象,而带有子类型限定的通配符允许你读取一个泛型对象。
下面我们再来看超类型限定的另外一种用途,有以下方法:
Java
public static <T extends Comparable<T>> T min(T[] a)当 T 是 String 类型时,该方法工作得很好。但是,该方法却不能处理 LocalDate 对象的数组,因为 LocalDate 实现的是 Comparable<ChronoLocalDate> 而不是 Comparable<LocalDate>(LocalDate 实现了 ChronoLocalDate,再由 ChronoLocalDate 扩展了 Comparable<ChronoLocalDate>)。
此时,我们可以修改下以上方法的定义:
Java
public static <T extends Comparable<? super T>> T min(T[] a)5.2. 无限定通配符
例如,Pair<?>。初看起来,这好像与原始的 Pair 类型一样。实际上,这两种类型有很大的不同。Pair<?> 的 getFirst 的返回值只能赋给一个 Object,setFirst 方法不能被调用(除了 setFirst(null)),甚至不能用 Object 调用。Pair<?> 和 Pair 本质的不同在于:可以用任意 Object 对象调用原始 Pair 类的 setFirst 方法。
对于一些简单操作它很有用:
Java
public static boolean hasNulls(Pair<?> p) {
return p.getFirst() == null || p.getSecond() == null;
}1
2
3
2
3
虽然也可以写成泛型方法:
Java
public static <T> boolean hasNulls(Pair<T> p)但是,带有通配符的版本可读性更好。
6. 反射和泛型
6.1. 虚拟机中的泛型类型信息
可以通过 java.lang.reflect 包中的接口 Type 来表述泛型类型声明,这个接口包含以下子类型:
Class类,描述具体类型;TypeVariable接口,描述类型变量(如T extends Comparable<? super T>);WildcardType接口,描述通配符(如? super T);ParameterizedType接口,描述泛型类或接口类型(如Comparable<? super T>);GenericArrayType接口,描述泛型数组(如T[]);
下图给出了继承层次。注意,最后 4 个子类型是接口,虚拟机将实例化实现这些接口的适当的类。

6.2. 类型字面量
有时候,你会希望由值的类型决定程序的行为。通常的实现方法是将 Class 对象与一个动作关联,不过,对于泛型类的情况(例如 ArrayList<Integer> 等等),该方法就不能处理得很好了。这里有一个技巧,在某些情况下可以解决这个问题。
首先在方法中捕获 Type 接口的一个实例:
Java
class TypeLiteral<T> {
public TypeLiteral() {
Type parentType = getClass().getGenericSuperclass();
if (parentType instanceof ParameterizedType) {
type = ((ParameterizedType) parentType).getActualTypeArguments()[0];
} else
throw new UnsupportedOperationException("Construct as new TypeLiteral<...>(){}");
}
// ...
}1
2
3
4
5
6
7
8
9
10
2
3
4
5
6
7
8
9
10
然后在使用时构造一个匿名子类:
Java
var type = new TypeLiteral<ArrayList<Integer>>(){} // note the {}下面是完整的示例:
Java
package genericReflection;
import java.lang.reflect.*;
import java.util.*;
import java.util.function.*;
/**
* A type literal describes a type that can be generic, such as ArrayList<String>.
*/
class TypeLiteral<T> {
private Type type;
/**
* This constructor must be invoked from an anonymous subclass
* as new TypeLiteral<...>(){}
*/
public TypeLiteral() {
Type parentType = getClass().getGenericSuperclass();
if (parentType instanceof ParameterizedType) {
type = ((ParameterizedType) parentType).getActualTypeArguments()[0];
} else
throw new UnsupportedOperationException("Construct as new TypeLiteral<...>(){}");
}
private TypeLiteral(Type type) {
this.type = type;
}
/**
* Yields a type literal that describes the given type.
*/
public static TypeLiteral<?> of(Type type) {
return new TypeLiteral<Object>(type);
}
public String toString() {
if (type instanceof Class) return ((Class<?>) type).getName();
else return type.toString();
}
public boolean equals(Object otherObject) {
return otherObject instanceof TypeLiteral && type.equals(((TypeLiteral<?>) otherObject).type);
}
public int hashCode() {
return type.hashCode();
}
}
/**
* Formats objects, using rules that associate types with formatting functions.
*/
class Formatter {
private Map<TypeLiteral<?>, Function<?, String>> rules = new HashMap<>();
/**
* Add a formatting rule to this formatter.
*
* @param type the type to which this rule applies
* @param formatterForType the function that formats objects of this type
*/
public <T> void forType(TypeLiteral<T> type, Function<T, String> formatterForType) {
rules.put(type, formatterForType);
}
/**
* Formats all fields of an object using the rules of this formatter.
*
* @param obj an object
* @return a string with all field names and formatted values
*/
public String formatFields(Object obj) throws IllegalArgumentException, IllegalAccessException {
var result = new StringBuilder();
for (Field f : obj.getClass().getDeclaredFields()) {
result.append(f.getName());
result.append("=");
f.setAccessible(true);
Function<?, String> formatterForType = rules.get(TypeLiteral.of(f.getGenericType()));
if (formatterForType != null) {
// formatterForType has parameter type ?. Nothing can be passed to its apply
// method. Cast makes the parameter type to Object so we can invoke it.
@SuppressWarnings("unchecked")
Function<Object, String> objectFormatter = (Function<Object, String>) formatterForType;
result.append(objectFormatter.apply(f.get(obj)));
} else
result.append(f.get(obj).toString());
result.append("\n");
}
return result.toString();
}
}
public class TypeLiterals {
public static class Sample {
ArrayList<Integer> nums;
ArrayList<Character> chars;
ArrayList<String> strings;
public Sample() {
nums = new ArrayList<>();
nums.add(42);
nums.add(1729);
chars = new ArrayList<>();
chars.add('H');
chars.add('i');
strings = new ArrayList<>();
strings.add("Hello");
strings.add("World");
}
}
private static <T> String join(String separator, ArrayList<T> elements) {
var result = new StringBuilder();
for (T e : elements) {
if (result.length() > 0) result.append(separator);
result.append(e.toString());
}
return result.toString();
}
public static void main(String[] args) throws Exception {
var formatter = new Formatter();
formatter.forType(new TypeLiteral<ArrayList<Integer>>() { }, lst -> join(" ", lst));
formatter.forType(new TypeLiteral<ArrayList<Character>>() { }, lst -> "\"" + join("", lst) + "\"");
System.out.println(formatter.formatFields(new Sample()));
}
}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
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
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
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127