前言
本文译自The Java™ Tutorials - Generics。
翻译之前,我只找到了两份机翻的版本,机翻的很多语句都不通顺,几乎没有经过人工修订,实在看不下去,索性自己翻译了一遍。翻译过程中,才发现已有人翻译过了,不过我还是坚持翻译完了。我英语水平很一般,有的句子我也琢磨了半天,不能保证100%没有错误或纰漏,如有问题请提交反馈。其他一些备注放在本文的最后面。
其他人翻译的版本:
泛型概述
任何不简单的软件项目中都会存在bug。虽然仔细的计划、编程和测试可以降低bug率,但无法避免bug。随着新特性的引入,以及代码库的规模和复杂性的增加,这一点变得尤为明显。
幸运的是,有些bug比其他bug更容易被发现。例如,编译时bug可以在早期被检测到;你可以使用编译器的错误消息来找出问题所在并及时修复。然而,运行时bug可能更成问题;它们并不总是立即出现,当它们出现时,可能是在程序中与问题的实际原因相去甚远的一个点上。
泛型使更多的bug在编译时可检测,从而增加了代码的稳定性。学完本课程后,你可能希望继续学习由Gilad Bracha编写的“泛型”课程。
为什么要使用泛型?
简而言之,泛型使类型(类和接口)成为定义类、接口和方法时的参数(parameters)。就像方法声明中使用的更为常见的形式参数(formal parameters)一样,形式类型参数(type parameters)为你提供了一种在不同输入下重用相同代码的办法。不同之处在于形式参数的输入是值,而形式类型参数的输入是类型。
使用泛型的代码比非泛型代码更有优势:
-
在编译时进行更强的类型检查。
Java编译器对泛型代码应用强类型检查,并在代码违反类型安全性时发出错误。修复编译时错误比修复运行时错误更容易,运行时错误很难被发现。 -
消除强制类型转换。
以下不使用泛型的代码片段需要强制转换:List list = new ArrayList(); list.add("hello"); String s = (String) list.get(0);
当重新使用泛型编写后,代码不需要强制转换:
List<String> list = new ArrayList<String>(); list.add("hello"); String s = list.get(0); // no cast
-
使程序员能够实现泛型算法。
通过使用泛型,程序员可以实现可定制的能够处理不同类型集合的泛型算法,实现的算法类型安全且易于阅读。
泛型类型
泛型类型是对类型进行了参数化(parameterized)的泛型类或接口。下面的Box类将被修改来演示这个概念。
一个简单的Box类
首先看看能存取任何类型对象的非泛型Box类。它有两个方法:set和get,前者将对象添加到box(Box的实例)中,后者从box中取出对象:
public class Box {
private Object object;
public void set(Object object) { this.object = object; }
public Object get() { return object; }
}
由于它的set方法接收Object类型的对象,get方法返回Object类型的对象,你可以向set方法传入任何非基本类型的对象。没有办法在编译时验证类的使用方式。代码的一部分可能会在box中放置一个Integer对象,并期望从box中取得Integer对象,而代码的另一部分可能会错误地传入一个String对象,从而导致运行时错误。
Box类的泛型版本
泛型类的定义格式如下:
class name<T1, T2, ..., Tn> { /* ... */ }
形式类型参数部分位于类名后面的尖括号(<>)中。它指定了形式类型参数(也称为类型变量)T1,T2,……和Tn。
要修改Box类以使用泛型,可以通过将代码“public class Box”更改为“public class Box<T>”来创建泛型类型声明。这引入了类型变量T,它可以在类中的任何位置使用。
通过这一更改,Box类变为:
/**
* Generic version of the Box class.
* @param <T> the type of the value being boxed
*/
public class Box<T> {
// T stands for "Type"
private T t;
public void set(T t) { this.t = t; }
public T get() { return t; }
}
如你所见,之前Box类中的Object都被T替换。类型变量可以是你指定的任何非基本类型:任何类类型、任何接口类型、任何数组类型,甚至是另一个类型变量。
同样的方式也可以用于创建泛型接口。
形式类型参数命名惯例
根据惯例,形式类型参数名称是单个大写字母。这与你已经知道的变量命名惯例形成鲜明对比,这样做是有充分理由的: 如果没有这个惯例,就很难区分类型变量与普通类名以及接口名之间的区别。
最常用的形式类型参数名称有:
E - Element (used extensively by the Java Collections Framework)
K - Key
N - Number
T - Type
V - Value
S,U,V etc. - 2nd, 3rd, 4th types
在整个Java SE API以及本课程的其余部分中都用到了这些形式类型参数名称。
调用(Invoking)和实例化(Instantiating)泛型类型
要从代码中引用泛型Box类,必须执行泛型类型调用(perform a generic type invocation),它将T替换为某些具体值,例如Integer:
Box<Integer> integerBox;
你可以认为泛型类型调用类似于普通的方法调用,方法调用是将实际参数(argument)传递给方法,泛型类型调用是将实际类型参数(type argument)传递给Box类本身,在本例中是将Integer传递给Box类本身。
形式类型参数(Type Parameter)和实际类型参数(Type Argument)术语: 许多开发人员互换地使用术语“形式类型参数”和“实际类型参数”,但这两个术语并不相同。在编写代码时,提供的实际类型参数可以用来创建参数化类型(parameterized type)。因此,Foo<T>中的T是形式类型参数,Foo<String> f中的String是实际类型参数。本课程在使用这些术语时会遵循此定义。
像任何其他变量声明一样,这段代码实际上并不创建新的Box对象。它只是声明integerBox将保存对“Box of Integer”的引用,Box<Integer>读作“Box of Integer”。
泛型类型的调用通常称为参数化类型。
要实例化此类,像往常一样使用new关键字,但要将<Integer>放在类名和括号之间:
Box<Integer> integerBox = new Box<Integer>();
菱形操作符
在Java SE 7及更高版本中,只要编译器能够从上下文中确定或推断出实际类型参数,就可以用一组空的实际类型参数(<>)替换掉调用泛型类的构造方法所需的实际类型参数。这对尖括号<>被通俗地称为菱形操作符。例如,你可以使用以下语句创建Box<Integer>的实例:
Box<Integer> integerBox = new Box<>();
有关菱形符号和类型推断的更多信息,请参阅“类型推断”一节。
多个形式类型参数
如前面所述,泛型类可以有多个形式类型参数。例如,实现了泛型接口Pair的泛型类OrderedPair:
public interface Pair<K, V> {
public K getKey();
public V getValue();
}
public class OrderedPair<K, V> implements Pair<K, V> {
private K key;
private V value;
public OrderedPair(K key, V value) {
this.key = key;
this.value = value;
}
public K getKey() { return key; }
public V getValue() { return value; }
}
以下语句创建了OrderedPair类的两个实例:
Pair<String, Integer> p1 = new OrderedPair<String, Integer>("Even", 8);
Pair<String, String> p2 = new OrderedPair<String, String>("hello", "world");
代码new OrderedPair<String,Integer>将K实例化为String,将V实例化为Integer。因此,OrderedPair的构造方法的形式参数类型分别是String和Integer。由于自动装箱,将String和int传递给该类是有效的。
正如“菱形操作符”一节中所述,因为Java编译器可以从声明OrderedPair<String,Integer>中推断出K和V的类型,所以可以使用菱形符号缩短这些语句:
OrderedPair<String, Integer> p1 = new OrderedPair<>("Even", 8);
OrderedPair<String, String> p2 = new OrderedPair<>("hello", "world");
若要创建泛型接口,请遵循与创建泛型类相同的惯例。
参数化类型
你还可以使用参数化类型(即List<String>)替换掉形式类型参数(即K或V)。例如,使用OrderedPair<K,V>的示例:
OrderedPair<String, Box<Integer>> p = new OrderedPair<>("primes", new Box<Integer>(...));
原始类型
没有任何实际类型参数的泛型类或接口被称为原始类型(raw type)。例如,给定的泛型Box类:
public class Box<T> {
public void set(T t) { /* ... */ }
// ...
}
要创建Box<T>的参数化类型,你需要为形式类型参数T提供一个实际类型参数:
Box<Integer> intBox = new Box<>();
如果省略了实际类型参数,则创建Box<T>的原始类型:
Box rawBox = new Box();
因此,Box是泛型类型Box<T>的原始类型。但是,非泛型类类型或非泛型接口类型不是原始类型。
原始类型出现在遗留代码中,因为许多API类(如Collections类)在JDK 5.0之前并不是泛型的。当使用原始类型时,你实际上得到了JDK引入泛型之前的行为——一个提供Object对象的Box类。为了向后兼容,允许将参数化类型赋值给其原始类型:
Box<String> stringBox = new Box<>();
Box rawBox = stringBox; // OK
但是,如果将原始类型赋值给参数化类型,则会收到警告:
Box rawBox = new Box(); // rawBox is a raw type of Box<T>
Box<Integer> intBox = rawBox; // warning: unchecked conversion
如果使用原始类型调用相应泛型类型中定义的泛型方法,也会收到警告:
Box<String> stringBox = new Box<>();
Box rawBox = stringBox;
rawBox.set(8); // warning: unchecked invocation to set(T)
警告显示,原始类型绕过了泛型类型检查,对不安全代码的捕获(catch)被推迟到了运行时。因此,你应该避免使用原始类型。
“类型擦除”一节有更多关于Java编译器如何使用原始类型的信息。
未经检查的(Unchecked)错误消息
如前面所述,将遗留代码与泛型代码混合时,可能会遇到类似于以下内容的警告消息:
Note: Example.java uses unchecked or unsafe operations.
Note: Recompile with -Xlint:unchecked for details.
当使用对原始类型进行操作的旧API时,可能会发生这种情况,如下面的示例所示:
public class WarningDemo {
public static void main(String[] args){
Box<Integer> bi;
bi = createBox();
}
static Box createBox(){
return new Box();
}
}
术语“未经检查的”意味着编译器没有足够的类型信息来执行确保类型安全所必需的所有类型检查。默认情况下,“未经检查的”警告被禁用,尽管编译器提供了提示。若要查看所有“未经检查的”警告,请使用-Xlint:unchecked重新编译。
使用-Xlint:unchecked重新编译上一个示例,会显示以下附加信息:
WarningDemo.java:4: warning: [unchecked] unchecked conversion
found : Box
required: Box<java.lang.Integer>
bi = createBox();
^
1 warning
要完全禁用未经检查的警告,请使用-Xlint:-unchecked标志。@SuppressWarnings("unchecked") 注解会抑制未经检查的警告。如果你不熟悉@SuppressWarnings这个语法,请参阅“注解”一节。
泛型方法
泛型方法是引入了它们自己的形式类型参数的方法。这类似于声明泛型类型,但形式类型参数的作用域仅限于声明它的方法。允许使用静态泛型方法和非静态泛型方法以及泛型类构造方法。
泛型方法的语法包括一个位于尖括号内,出现在方法的返回值类型之前的形式类型参数列表。对于静态泛型方法,形式类型参数部分必须出现在方法的返回值类型之前。
Util类包含一个比较两个Pair对象的泛型方法compare:
public class Util {
public static <K, V> boolean compare(Pair<K, V> p1, Pair<K, V> p2) {
return p1.getKey().equals(p2.getKey()) &&
p1.getValue().equals(p2.getValue());
}
}
public class Pair<K, V> {
private K key;
private V value;
public Pair(K key, V value) {
this.key = key;
this.value = value;
}
public void setKey(K key) { this.key = key; }
public void setValue(V value) { this.value = value; }
public K getKey() { return key; }
public V getValue() { return value; }
}
调用此方法的完整语法如下:
Pair<Integer, String> p1 = new Pair<>(1, "apple");
Pair<Integer, String> p2 = new Pair<>(2, "pear");
boolean same = Util.<Integer, String>compare(p1, p2);
已明确提供类型,如粗体所示。通常,这可以省略,编译器将推断出所需的类型:
Pair<Integer, String> p1 = new Pair<>(1, "apple");
Pair<Integer, String> p2 = new Pair<>(2, "pear");
boolean same = Util.compare(p1, p2);
这个特性被称为类型推断,它允许你像调用普通方法一样调用泛型方法,而无需在尖括号之间指定类型。这个话题将在下一节“类型推断”中作进一步讨论。
有界类型形式参数
有时,你可能希望限制可用作参数化类型中的实际类型参数的类型。例如,处理数字的方法可能只想接受Number或其子类的实例。这就是有界形式类型参数的用途。
若要声明有界形式类型参数,可以依次列出形式类型参数的名称,随后是extends关键字,最后是它的上界,在本例中它的上界为Number。注意,在这个上下文中,extends在一般意义上是指“extends”(继承类时使用的extends)或“implements”(实现接口时使用的implements)。
public class Box<T> {
private T t;
public void set(T t) {
this.t = t;
}
public T get() {
return t;
}
public <U extends Number> void inspect(U u){
System.out.println("T: " + t.getClass().getName());
System.out.println("U: " + u.getClass().getName());
}
public static void main(String[] args) {
Box<Integer> integerBox = new Box<Integer>();
integerBox.set(new Integer(10));
integerBox.inspect("some text"); // error: this is still String!
}
}
将这个有界形式类型参数加入我们的泛型方法后,编译将会失败,因为我们将String类型的实际参数传递给了inspect方法:
Box.java:21: <U>inspect(U) in Box<java.lang.Integer> cannot
be applied to (java.lang.String)
integerBox.inspect("10");
^
1 error
除了限制可用于实例化泛型类型的类型之外,有界形式类型参数还允许你调用在边界中定义的方法:
public class NaturalNumber<T extends Integer> {
private T n;
public NaturalNumber(T n) { this.n = n; }
public boolean isEven() {
return n.intValue() % 2 == 0;
}
// ...
}
isEven方法通过n调用Integer类中定义的intValue方法。
多个边界
前面的示例演示了如何使用带有单个边界的形式类型参数,但是形式类型参数可以有多个边界:
<T extends B1 & B2 & B3>
被多个边界约束的类型变量是这些边界的子类型。如果其中一个边界是类,则必须首先指定它。例如:
Class A { /* ... */ }
interface B { /* ... */ }
interface C { /* ... */ }
class D <T extends A & B & C> { /* ... */ }
如果没有首先指定边界A,则会出现编译时错误:
class D <T extends B & A & C> { /* ... */ } // compile-time error
泛型方法和有界类型形式参数
有时,你可能希望限制可用作参数化类型中的实际类型参数的类型。例如,处理数字的方法可能只想接受Number或其子类的实例。这就是有界形式类型参数的用途。
若要声明有界形式类型参数,可以依次列出形式类型参数的名称,随后是extends关键字,最后是它的上界,在本例中它的上界为Number。注意,在这个上下文中,extends在一般意义上是指“extends”(继承类时使用的extends)或“implements”(实现接口时使用的implements)。
public class Box<T> {
private T t;
public void set(T t) {
this.t = t;
}
public T get() {
return t;
}
public <U extends Number> void inspect(U u){
System.out.println("T: " + t.getClass().getName());
System.out.println("U: " + u.getClass().getName());
}
public static void main(String[] args) {
Box<Integer> integerBox = new Box<Integer>();
integerBox.set(new Integer(10));
integerBox.inspect("some text"); // error: this is still String!
}
}
将这个有界形式类型参数加入我们的泛型方法后,编译将会失败,因为我们将String类型的实际参数传递给了inspect方法:
Box.java:21: <U>inspect(U) in Box<java.lang.Integer> cannot
be applied to (java.lang.String)
integerBox.inspect("10");
^
1 error
除了限制可用于实例化泛型类型的类型之外,有界形式类型参数还允许你调用在边界中定义的方法:
public class NaturalNumber<T extends Integer> {
private T n;
public NaturalNumber(T n) { this.n = n; }
public boolean isEven() {
return n.intValue() % 2 == 0;
}
// ...
}
isEven方法通过n调用Integer类中定义的intValue方法。
多个边界
前面的示例演示了如何使用带有单个边界的形式类型参数,但是形式类型参数可以有多个边界:
<T extends B1 & B2 & B3>
被多个边界约束的类型变量是这些边界的子类型。如果其中一个边界是类,则必须首先指定它。例如:
Class A { /* ... */ }
interface B { /* ... */ }
interface C { /* ... */ }
class D <T extends A & B & C> { /* ... */ }
如果没有首先指定边界A,则会出现编译时错误:
class D <T extends B & A & C> { /* ... */ } // compile-time error
泛型、继承和子类型
你已经知道,只要类型兼容,就可以将一个类型的对象赋值给另一个类型的对象。例如,你可以将Integer对象赋值给Object对象,因为Object是Integer的父类型之一:
Object someObject = new Object();
Integer someInteger = new Integer(10);
someObject = someInteger; // OK
在面向对象的术语中,这被称为“is a”关系。因为Integer是一种Object,因此允许赋值。但是Integer也是一种Number,所以下面的代码也是有效的:
public void someMethod(Number n) { /* ... */ }
someMethod(new Integer(10)); // OK
someMethod(new Double(10.1)); // OK
泛型也是如此。你可以执行泛型类型调用,将Number作为其实际类型参数传递,如果参数与Number兼容,则允许任何后续的add调用:
Box<Number> box = new Box<Number>();
box.add(new Integer(10)); // OK
box.add(new Double(10.1)); // OK
现在请考虑以下方法:
public void boxTest(Box<Number> n) { /* ... */ }
它接受什么样的实际类型参数?通过查看它的签名,可以看到它接受一个类型为Box<Number>的参数。但这是什么意思?你是否可以像预期的那样传入Box<Integer>或Box<Double>?答案是否定的,因为Box<Integer>和Box<Double>不是Box<Number>的子类型。
在使用泛型编程时,这是一个常见的误解,但这是一个需要学习的重要概念。
图表显示Box<Integer>不是Box<Number>的子类型
Box<Integer>不是Box<Number>的子类型,即使Integer是Number的子类型。
注意: 给定的两个具体类型A和B(例如:Number和Integer),无论A和B是否相关,MyClass<A>与MyClass<B>都没有任何关系。MyClass<A>和MyClass<B>的公共父类是Object。
有关当形式类型参数相关时如何在两个泛型类之间创建类似子类型的关系的信息,请参阅“通配符和子类型多态”一节。
泛型类和子类型多态
你可以通过继承泛型类或实现泛型接口来得到它的子类型。一个类或接口的形式类型参数与另一个类或接口的形式类型参数之间的关系由extends和implements子句确定。
备注1。
以集合类为例,ArrayList<E>实现了List<E>,List<E>继承了Collection<E>。所以ArrayList<String>是List<String>的子类型,List<String>是Collection<String>的子类型。只要不改变实际类型参数,类型之间的子类型关系就会保持不变。
展示集合层次结构示例的图表:所以ArrayList<String>是List<String>的子类型,List<String>是Collection<String>的子类型。
集合层次结构的示例
现在假设我们想要定义我们自己的列表接口PayloadList,它将泛型类型P的可选值与每个元素相关联。它可能是这样声明的:
interface PayloadList<E,P> extends List<E> {
void setPayload(int index, P val);
...
}
PayloadList的以下参数化类型是List<String>的子类型:
PayloadList<String,String>
PayloadList<String,Integer>
PayloadList<String,Exception>
展示PayLoadList层次结构示例的图表:所以ArrayList<String>是List<String>的子类型,List<String>是Collection<String>的子类型。与PayloadList<String,String>在同一级别上是PayloadList<String,Integer>和PayloadList<String,Exceptions>。
PayloadList层次结构的示例
类型推断
类型推断指的是Java编译器能够检查每个方法调用和相应的声明,以此来确定使调用有效的实际类型参数(或实际参数)。推断算法先确定实际参数的类型,以及赋值操作等号左边的类型或将要被返回的值的类型。最后,推断算法会尝试找到适用于所有实际参数的最具体类型。
为了说明最后一点,在下面的示例中,推断算法确定了传递给pick方法的第二个参数是Serializable类型:
static <T> T pick(T a1, T a2) { return a2; }
Serializable s = pick("d", new ArrayList<String>());
备注2。
类型推断和泛型方法
“泛型方法”一节向你介绍了类型推断,这使你能够像调用普通方法一样调用泛型方法,而无需在尖括号之间指定类型。考虑下面的例子,需要Box类的BoxDemo:
public class BoxDemo {
public static <U> void addBox(U u,
java.util.List<Box<U>> boxes) {
Box<U> box = new Box<>();
box.set(u);
boxes.add(box);
}
public static <U> void outputBoxes(java.util.List<Box<U>> boxes) {
int counter = 0;
for (Box<U> box: boxes) {
U boxContents = box.get();
System.out.println("Box #" + counter + " contains [" +
boxContents.toString() + "]");
counter++;
}
}
public static void main(String[] args) {
java.util.ArrayList<Box<Integer>> listOfIntegerBoxes =
new java.util.ArrayList<>();
BoxDemo.<Integer>addBox(Integer.valueOf(10), listOfIntegerBoxes);
BoxDemo.addBox(Integer.valueOf(20), listOfIntegerBoxes);
BoxDemo.addBox(Integer.valueOf(30), listOfIntegerBoxes);
BoxDemo.outputBoxes(listOfIntegerBoxes);
}
}
以下是该示例的输出:
Box #0 contains [10]
Box #1 contains [20]
Box #2 contains [30]
泛型方法addBox定义了一个名为U的形式类型参数。通常,Java编译器可以推断出泛型方法调用的实际类型参数。因此,在大多数情况下,你不必指定它们。例如,要调用泛型方法addBox,可以使用类型见证(type witness)指定实际类型参数,如下所示:
BoxDemo.<Integer>addBox(Integer.valueOf(10), listOfIntegerBoxes);
或者,如果省略了类型见证,Java编译器会(从方法的实际参数)自动推断出实际类型参数是Integer:
BoxDemo.addBox(Integer.valueOf(20), listOfIntegerBoxes);
泛型类的类型推断和实例化
只要编译器可以从上下文中推断出实际类型参数,就可以用一组空的实际类型参数(<>)替换调用泛型类的构造方法所需的实际类型参数。这对尖括号被非正式地称为菱形操作符。
例如,考虑以下变量声明:
Map<String, List<String>> myMap = new HashMap<String, List<String>>();
你可以用一组空的实际类型参数(<>)替换掉构造方法的参数化类型:
Map<String, List<String>> myMap = new HashMap<>();
请注意,要在泛型类实例化期间利用类型推断,你必须使用菱形操作符。在以下示例中,编译器产生了未经检查的转换警告,因为HashMap()构造方法引用了HashMap的原始类型,而不是Map<String, List<String>>类型:
Map<String, List<String>> myMap = new HashMap(); // unchecked conversion warning
泛型类和非泛型类的类型推断和泛型构造方法
请注意,构造方法在泛型类和非泛型类中都可以是泛型的(换句话说,可以声明它们自己的形式类型参数)。请考虑以下示例:
class MyClass<X> {
<T> MyClass(T t) {
// ...
}
}
考虑以下MyClass类的实例化:
new MyClass<Integer>("")
该语句创建参数化类型MyClass<Integer>的实例;该语句显式地指定泛型类MyClass<X>的形式类型参数X的类型为Integer。请注意,此泛型类的构造方法包含一个形式类型参数T。编译器推断出此泛型类的构造方法的形式类型参数T的类型是String(因为此构造方法的实际参数是一个String对象)。
Java SE 7之前版本的编译器能够推断出泛型构造方法的形式类型参数的实际类型,这类似于泛型方法。然而,Java SE 7及更高版本中的编译器可以推断出要实例化的泛型类的形式类型参数的实际类型(需要使用菱形操作符<>)。请考虑以下示例:
MyClass<Integer> myObject = new MyClass<>("");
在这个例子中,编译器推断出泛型类MyClass<X>的形式类型参数X的类型是Integer。它推断出此泛型类的构造方法的形式类型参数T的类型是String。
注意: 值得注意的是,推断算法仅使用实际调用参数、目标类型,以及显然的预期返回值类型来推断类型。推断算法不使用程序后续的结果做类型推断。
目标类型
Java编译器利用目标类型来推断泛型方法调用的形式类型参数的实际类型。表达式的目标类型是Java编译器根据表达式出现的位置所期望的数据类型。考虑方法Collections.emptyList,声明如下:
static <T> List<T> emptyList();
考虑以下赋值语句:
List<String> listOne = Collections.emptyList();
该语句期待List<String>的实例;此数据类型是目标类型。因为方法emptyList返回List<T>类型的值,所以编译器推断出实际类型参数T的值必须是String。这在JavaSE 7和8中都适用。或者,你可以使用类型见证并指定T的值,如下所示:
List<String> listOne = Collections.<String>emptyList();
然而,在这种情况下,这是没有必要的。不过,在其他情况下,这是必要的。考虑以下方法:
void processStringList(List<String> stringList) {
// process stringList
}
假设你要使用一个空列表调用方法processStringList。在Java SE 7中,以下语句不能编译通过:
processStringList(Collections.emptyList());
Java SE 7编译器生成类似于以下内容的错误消息:
List<Object> cannot be converted to List<String>
编译器要求提供T的实际类型参数,该值默认为Object。因此,方法Collections.emptyList的调用返回一个List<Object>类型的值,该值与方法processStringList不兼容。因此,在Java SE 7中,你必须按如下方式指定实际类型参数的值:
processStringList(Collections.<String>emptyList());
这在Java SE 8中不再是必需的。目标类型的概念已经扩展到包括方法的实际参数,例如方法processStringList的实际参数。在这种情况下,方法processStringList需要一个List<String>类型的参数。方法Collections.emptyList返回List<T>的值,因此使用List<String>的目标类型时,编译器推断出实际类型参数T的值为String。因此,在Java SE 8中,以下语句能通过编译:
processStringList(Collections.emptyList());
有关更多信息,请参阅“Lambda表达式”中的“目标类型”一节。
通配符
在泛型代码中,问号(?),称为通配符,表示未知类型。通配符可用于多种情况:作为参数、字段或局部变量的类型;有时作为返回值类型(尽管使用更具体的类型的编程实践更好)。通配符从不用作泛型方法调用、泛型类实例创建或父类型的实际类型参数。
下面几节将更详细地讨论通配符,包括上界通配符、下界通配符和通配符捕获。
上界通配符
你可以使用上界通配符来放宽对变量的限制。例如,假设你要编写一个适用于List<Integer>、List<Double>和List<Number>的方法;你可以通过使用上界通配符来实现这一点。
若要声明上界通配符,请使用通配符符号(?),它后面跟着extends关键字,然后是其上界。注意,在这个上下文中,extends在一般意义上是指“extends”(继承类时使用的extends)或“implements”(实现接口时使用的implements)。
要编写适用于Number列表的方法以及Number子类型列表的方法,例如Integer列表、Double列表和Float列表,你可以指定List<?extends Number>。术语List<Number>比List<? extends Number>更具限制性,因为前者只匹配Number类型的列表,而后者匹配Number类型或其任意子类的列表。
考虑以下方法process:
public static void process(List<? extends Foo> list) { /* ... */ }
上界通配符,<? extends Foo>,其中Foo是任意类型,匹配Foo和Foo的任何子类型。process方法可以像Foo类型一样访问列表元素:
public static void process(List<? extends Foo> list) {
for (Foo elem : list) {
// ...
}
}
在foreach子句中,elem变量迭代列表中的每个元素。Foo类中定义的任何方法现在都可以在elem上使用。
sumOfList方法返回列表中数字的总和:
public static double sumOfList(List<? extends Number> list) {
double s = 0.0;
for (Number n : list)
s += n.doubleValue();
return s;
}
以下使用Integer对象的列表的代码输出结果是sum = 6.0:
List<Integer> li = Arrays.asList(1, 2, 3);
System.out.println("sum = " + sumOfList(li));
Double值的列表可以使用相同的sumOfList方法。以下代码输出结果是sum = 7.0:
List<Double> ld = Arrays.asList(1.2, 2.3, 3.5);
System.out.println("sum = " + sumOfList(ld));
无界通配符
无界通配符类型是使用通配符(?)指定的,例如List<?>。这被称为未知类型的列表。在两种情况下,无界通配符是一种有用的办法:
- 如果你正在编写可以使用Object类中提供的功能来实现的方法。
- 当代码使用泛型类中不依赖于形式类型参数的方法时。例如,List.size或List.clear。实际上,之所以经常使用Class<?>,是因为Class<T>中的大多数方法都不依赖于T。
考虑以下方法,printList:
public static void printList(List<Object> list) {
for (Object elem : list)
System.out.println(elem + " ");
System.out.println();
}
printList方法的目标是输出任何类型的列表,但它无法实现该目标——它只输出一个Object实例的列表;它不能输出List<Integer>,List<String>,List<Double>等,因为它们不是List<Object>的子类型。要编写泛型的printList方法,请使用List<?>:
public static void printList(List<?> list) {
for (Object elem: list)
System.out.print(elem + " ");
System.out.println();
}
因为对于任何具体类型A,List<A>是List<?>的子类型,你可以使用printList方法输出任何类型的列表:
List<Integer> li = Arrays.asList(1, 2, 3);
List<String> ls = Arrays.asList("one", "two", "three");
printList(li);
printList(ls);
注意: 在本课程的示例中使用了Arrays.asList方法。这个静态工厂方法转换指定的数组并返回固定大小的列表。
值得注意的是List<Object>和List<?>是不同的。你可以将Object或Object的任何子类型插入到List<Object>中。但是你只能在List<?>中插入null。“通配符使用指南”一节提供了有关如何确定在给定情况下应该使用哪种通配符(如果有的话)的更多信息。
下界通配符
在“上界通配符”一节中,上界通配符将未知类型限制为特定类型或该类型的子类型,并使用extends关键字表示。以类似的方式,下界通配符将未知类型限制为特定类型或该类型的父类型。
下界通配符使用通配符(?)表示,它后面跟着super关键字,然后是其下界:<? super A>。
注意: 你可以指定通配符的上界,也可以指定通配符的下界,但不能同时指定两者。
假设你要编写一个将Integer对象放入列表的方法。为了最大限度地提高灵活性,你希望该方法可以处理List<Integer>、List<Number>和List<Object>——任何可以保存Integer值的方法。
要编写适用于Integer列表和Integer父类型的列表的方法,例如Integer列表、Number列表和Object列表,你可以指定List<? super Integer>。术语Lis<Integer>比List<? super Integer>更具限制性,因为前者只匹配Integer类型的列表,而后者匹配Integer类型或其任意父类型的列表。
以下代码将数字1到10添加到列表的末尾:
public static void addNumbers(List<? super Integer> list) {
for (int i = 1; i <= 10; i++) {
list.add(i);
}
}
“通配符使用指南”一节提供了有关何时使用上界通配符以及何时使用下界通配符的指导。
通配符和子类型多态
正如“泛型、继承和子类型”一节中所述,泛型类或泛型接口并不仅仅因为它们的类型之间有关系就相互关联。但是,你可以使用通配符在泛型类或泛型接口之间创建关系。
给定以下两个常规(非泛型)类:
class A { /* ... */ }
class B extends A { /* ... */ }
编写以下代码是合理的:
B b = new B();
A a = b;
该示例演示了常规类的继承遵循这个子类型规则:如果B继承了A,则B类是A类的子类型。此规则不适用于泛型类型:
List<B> lb = new ArrayList<>();
List<A> la = lb; // compile-time error
鉴于Integer是Number的子类型,List<Integer>和List<Number>之间的关系是什么?
显示List<Number>和List<Integer>的公共父级是未知类型列表的关系图
公共父类是List<?>。
尽管Integer是Number的子类型,但List<Integer>不是List<Number>的子类型,实际上,这两种类型并不相关。List<Number>和List<Integer>的公共父类是List<?>。
为了在这些类之间创建关系,以便代码可以通过List<Integer>的元素访问Number的方法,请使用上界通配符:
List<? extends Integer> intList = new ArrayList<>();
List<? extends Number> numList = intList; // OK. List<? extends Integer> is a subtype of List<? extends Number>
因为Integer是Number的子类型,而numList是Number对象的列表,所以intList(Integer对象的列表)和numList之间现在存在关系。下图显示了使用上界通配符和下界通配符声明的多个List类之间的关系。
几个泛型列表类声明的层次结构。
“通配符使用指南”一节提供了有关上界通配符和下界通配符使用上的更多信息。
通配符捕获和助手函数
在某些情况下,编译器会推断出通配符的类型。例如,列表可以定义为List<?>,但是,在对表达式求值时,编译器会从代码中推断出特定的类型。这种情况称为通配符捕获。
在大多数情况下,你不必担心通配符捕获,除非你看到包含短语“capture of”的错误消息。
示例WildcharError在编译时产生一个捕获错误:
import java.util.List;
public class WildcardError {
void foo(List<?> i) {
i.set(0, i.get(0));
}
}
在本例中,编译器将输入参数i处理为Object类型。当foo方法调用List.set(int, E)时,编译器无法确认插入到列表中的对象的类型,从而产生错误。当这种类型的错误发生时,通常意味着编译器认为你给变量赋予了错误的类型。由于这个原因,Java语言中添加了泛型——以确保编译时的类型安全。
WildcarError示例在由Oracle的JDK 7 javac编译时生成以下错误:
WildcardError.java:6: error: method set in interface List<E> cannot be applied to given types;
i.set(0, i.get(0));
^
required: int,CAP#1
found: int,Object
reason: actual argument Object cannot be converted to CAP#1 by method invocation conversion
where E is a type-variable:
E extends Object declared in interface List
where CAP#1 is a fresh type-variable:
CAP#1 extends Object from capture of ?
1 error
在这个例子中,代码试图执行一个安全的操作,那么你如何处理这个编译器错误?你可以通过编写捕获通配符的私有助手方法来修复它。在这种情况下,你可以通过创建私有助手方法fooHelper来解决这个问题,如WildcardFixed中所示:
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在调用中是CAP#1,即捕获变量。这个示例现在编译成功。
按照惯例,助手方法通常被命名为originalMethodNameHelper。
现在考虑一个更复杂的例子,WildhardErrorBad:
import java.util.List;
public class WildcardErrorBad {
void swapFirst(List<? extends Number> l1, List<? extends Number> l2) {
Number temp = l1.get(0);
l1.set(0, l2.get(0)); // expected a CAP#1 extends Number,
// got a CAP#2 extends Number;
// same bound, but different types
l2.set(0, temp); // expected a CAP#1 extends Number,
// got a Number
}
}
在此示例中,代码试图执行不安全的操作。例如,考虑以下对swapFirst方法的调用:
List<Integer> li = Arrays.asList(1, 2, 3);
List<Double> ld = Arrays.asList(10.10, 20.20, 30.30);
swapFirst(li, ld);
List<Integer>和List<Double>都符合List<?extends Number>,从Integer值列表中取一个条目并尝试将其放入Double值列表中显然是不正确的。
使用Oracle的JDK javac编译器编译代码会产生以下错误:
WildcardErrorBad.java:7: error: method set in interface List<E> cannot be applied to given types;
l1.set(0, l2.get(0)); // expected a CAP#1 extends Number,
^
required: int,CAP#1
found: int,Number
reason: actual argument Number cannot be converted to CAP#1 by method invocation conversion
where E is a type-variable:
E extends Object declared in interface List
where CAP#1 is a fresh type-variable:
CAP#1 extends Number from capture of ? extends Number
WildcardErrorBad.java:10: error: method set in interface List<E> cannot be applied to given types;
l2.set(0, temp); // expected a CAP#1 extends Number,
^
required: int,CAP#1
found: int,Number
reason: actual argument Number cannot be converted to CAP#1 by method invocation conversion
where E is a type-variable:
E extends Object declared in interface List
where CAP#1 is a fresh type-variable:
CAP#1 extends Number from capture of ? extends Number
WildcardErrorBad.java:15: error: method set in interface List<E> cannot be applied to given types;
i.set(0, i.get(0));
^
required: int,CAP#1
found: int,Object
reason: actual argument Object cannot be converted to CAP#1 by method invocation conversion
where E is a type-variable:
E extends Object declared in interface List
where CAP#1 is a fresh type-variable:
CAP#1 extends Object from capture of ?
3 errors
没有助手方法可以解决这个问题,因为代码根本就是错误的。
通配符使用指南
学习使用泛型编程时,一个更令人困惑的地方是确定何时使用上界通配符以及何时使用下界通配符。本页提供了设计代码时要遵循的一些准则。
为了便于讨论,将变量视为提供两种功能之一是有帮助的:
“In”变量
“ in”变量为代码提供数据。假设一个具有两个参数的copy方法:copy(src,dest)。src的实际参数提供要复制的数据,因此它是“in”形式参数。
“Out”变量
“out”变量保存以供其他地方使用的数据。在复制示例中,copy(src, dest),dest的实际参数接受数据,因此它是“out”形式参数。
当然,有些变量同时用于“in”和“out”的目的——这种情况在指南中也有说明。
可以根据“in”和“out”原则来确定是否使用通配符以及使用哪种类型的通配符更合适。以下清单提供了要遵循的指导原则:
通配符指南:
- 使用extends关键字定义带有上界通配符的“in”变量。
- 使用super关键字定义带有下界通配符的“out”变量。
- 如果可以使用Object类中定义的方法访问“in”变量,请使用无界通配符。
- 在代码需要同时作为“in”和“out”变量访问变量的情况下,不要使用通配符。
这些指南不适用于方法的返回值类型。应该避免使用通配符作为返回值类型,因为它强制程序员使用代码来处理通配符。
List<? extends ...>可以被非正式地认为是只读的,但这不是严格的保证。假设你有以下两个类:
class NaturalNumber {
private int i;
public NaturalNumber(int i) { this.i = i; }
// ...
}
class EvenNumber extends NaturalNumber {
public EvenNumber(int i) { super(i); }
// ...
}
请考虑以下代码:
List<EvenNumber> le = new ArrayList<>();
List<? extends NaturalNumber> ln = le;
ln.add(new NaturalNumber(35)); // compile-time error
因为List<EvenNumber>是List<? extends NaturalNumber>的子类型,你可以将le赋值给ln。但是你不能使用ln将自然数添加到偶数列表中。以下清单上的操作是可能的:
- 你可以添加null。
- 你可以调用clear方法。
- 你可以获取迭代器并调用remove方法。
- 你可以捕获通配符并写入从列表中读取的元素。
你可以看到由List<? extends NaturalNumber>定义的列表在严格意义上不是只读的,但你可能会这样想,因为你无法存储新元素或更改列表中的现有元素。
类型擦除
Java语言引入了泛型,以便在编译时提供更严格的类型检查,并支持泛型编程。为了实现泛型,Java编译器将类型擦除应用于:
- 将泛型类型中的所有形式类型参数替换为它的边界,如果形式类型参数是无界的,则替换为Object。因此,生成的字节码只包含普通的类、普通的接口和普通的方法。
- 必要时插入强制类型转换以保证类型安全。
- 生成桥接方法用来在继承的泛型类型中保留多态性。
类型擦除确保不会为参数化类型创建新的类;因此,泛型不会产生运行时开销。
泛型类型的擦除
在类型擦除过程中,Java编译器会删除所有形式类型参数,对于每一个形式类型参数,如果形式类型参数是有界的,则用它的第一个边界来代替,或者如果形式类型参数是无界的,则用Object来代替。
考虑以下表示单链表中节点的泛型类:
public class Node<T> {
private T data;
private Node<T> next;
public Node(T data, Node<T> next) {
this.data = data;
this.next = next;
}
public T getData() { return data; }
// ...
}
因为形式类型参数T是无界的,所以Java编译器将其替换为Object:
public class Node {
private Object data;
private Node next;
public Node(Object data, Node next) {
this.data = data;
this.next = next;
}
public Object getData() { return data; }
// ...
}
在以下示例中,泛型节点类使用有界形式类型参数:
public class Node<T extends Comparable<T>> {
private T data;
private Node<T> next;
public Node(T data, Node<T> next) {
this.data = data;
this.next = next;
}
public T getData() { return data; }
// ...
}
Java编译器将有界形式类型参数T替换为它的第一个边界类Comparable:
public class Node {
private Comparable data;
private Node next;
public Node(Comparable data, Node next) {
this.data = data;
this.next = next;
}
public Comparable getData() { return data; }
// ...
}
泛型方法的擦除
Java编译器还会擦除泛型方法实际参数中的形式类型参数。考虑以下泛型方法:
// Counts the number of occurrences of elem in anArray.
//
public static <T> int count(T[] anArray, T elem) {
int cnt = 0;
for (T e : anArray)
if (e.equals(elem))
++cnt;
return cnt;
}
因为T是无界的,所以Java编译器将它替换为Object:
public static int count(Object[] anArray, Object elem) {
int cnt = 0;
for (Object e : anArray)
if (e.equals(elem))
++cnt;
return cnt;
}
假设定义了以下类:
class Shape { /* ... */ }
class Circle extends Shape { /* ... */ }
class Rectangle extends Shape { /* ... */ }
你可以编写一个泛型方法来绘制不同的形状:
public static <T extends Shape> void draw(T shape) { /* ... */ }
Java编译器将T替换为Shape:
public static void draw(Shape shape) { /* ... */ }
类型擦除的影响和桥接方法
有时候类型擦除会导致你可能没有预料到的情况。下面的示例演示了这种情况是如何发生的。示例(在Bridge方法中描述的)演示了编译器有时如何创建一个称为桥接方法的合成方法,作为类型擦除过程的一部分。
给出以下两个类:
public class Node<T> {
public T data;
public Node(T data) { this.data = data; }
public void setData(T data) {
System.out.println("Node.setData");
this.data = data;
}
}
public class MyNode extends Node<Integer> {
public MyNode(Integer data) { super(data); }
public void setData(Integer data) {
System.out.println("MyNode.setData");
super.setData(data);
}
}
考虑以下代码:
MyNode mn = new MyNode(5);
Node n = mn; // A raw type - compiler throws an unchecked warning
n.setData("Hello");
Integer x = mn.data; // Causes a ClassCastException to be thrown.
类型擦除后,该代码变为:
MyNode mn = new MyNode(5);
Node n = (MyNode)mn; // A raw type - compiler throws an unchecked warning
n.setData("Hello");
Integer x = (String)mn.data; // Causes a ClassCastException to be thrown.
下面是代码执行时发生的情况:
- n.setData("Hello");导致在MyNode类的对象上setData(Object)方法被执行。(MyNode类从Node类继承了setData(Object)方法)
- 在setData(Object)方法的主体中,n引用的对象的data字段被赋值为一个String对象。
- 通过mn引用的同一对象的data字段可以被访问,并且期待它是一个Integer对象(因为mn是一个MyNode对象,而一个MyNode对象是一个Node<Integer>对象)。
- 试图将String对象赋值给Integer对象会导致Java编译器在赋值时插入强制转换从而产生ClassCastException。
桥接方法
编译继承了参数化类的类时,或编译实现了参数化接口的类时,编译器可能需要创建一个称为桥接方法的合成方法,作为类型擦除过程的一部分。你通常不需要担心桥接方法,但是如果在堆栈跟踪信息中出现了桥接方法,你可能会感到困惑。
在类型擦除之后,Node和MyNode类变为:
public class Node {
public Object data;
public Node(Object data) { this.data = data; }
public void setData(Object data) {
System.out.println("Node.setData");
this.data = data;
}
}
public class MyNode extends Node {
public MyNode(Integer data) { super(data); }
public void setData(Integer data) {
System.out.println("MyNode.setData");
super.setData(data);
}
}
在类型擦除之后,方法签名不匹配。Node中的方法变为setData(Object),MyNode中的方法变为setData(Integer)。因此,MyNode中的setData方法不会重写Node中的setData方法。
为了解决这个问题,在类型擦除后保留泛型类型的多态性,Java编译器将生成一个桥接方法,以确保子类型多态按预期工作。对于MyNode类,编译器为setData生成以下桥接方法:
class MyNode extends Node {
// Bridge method generated by the compiler
//
public void setData(Object data) {
setData((Integer) data);
}
public void setData(Integer data) {
System.out.println("MyNode.setData");
super.setData(data);
}
// ...
}
如你所见,桥接方法在类型擦除之后与Node类的setData方法具有相同的方法签名,并把真正的实现委托给了原有的setData方法。
非具体化类型
“类型擦除”一节讨论了编译器删除与形式类型参数和实际类型参数的相关信息的过程。类型擦除的后果与varargs形式参数是非具体化类型的可变参数(也称为varargs)方法有关。有关可变参数方法的更多信息,请参阅“将信息传递给方法或构造方法”中的“任意数量的参数”一节。
此页面包含以下主题:
- 非具体化类型(Non-Reifiable Types)
- 堆污染(Heap Pollution)
- 具有非具体化类型的形式参数的可变参数方法的潜在漏洞
- 阻止从使用了非具体化类型的形式参数的可变参数方法产生警告
非具体化类型
具体化类型(reifiable type)是在运行时可以得到其全部类型信息的类型。这包括基本类型,非泛型类型,原始类型和无界通配符的调用。
非具体化类型是类型信息在编译时被擦除了的类型,即除了无界通配符的调用之外的其他泛型类型的调用。非具体化类型的类型信息在运行时不是全部可用的。非具体化类型的示例是list<string>和list<number>;JVM无法在运行时区分这些类型之间的差异。如“泛型的限制”一节中所示,在某些情况下,不能使用非具体化类型:例如,在instanceof表达式中不能使用非具体化类型,以及不能使用非具体化类型作为数组中的元素。
备注3。
堆污染
当参数化类型的变量引用非该参数化类型的对象时,就会发生堆污染。如果程序执行了一些在编译时引起未经检查的警告的操作,就会发生这种情况。如果在编译时(在编译时类型检查规则的限制内)或在运行时,无法验证涉及参数化类型的操作(例如,强制转换或方法调用)的正确性,则会生成未经检查的警告。例如,当混合原始类型和参数化类型时,或者当执行未经检的强制转换时,就会发生堆污染。
在正常情况下,当所有代码同时编译时,编译器会发出未经检查的警告,以提醒你注意潜在的堆污染。如果单独编译部分代码,则很难检测到堆污染的潜在风险。如果确保代码编译时没有警告产生,则不会发生堆污染。
具有非具体化类型的形式参数的可变参数方法的潜在漏洞
包含varargs形式参数的泛型方法可能会导致堆污染。
考虑以下ArrayBuilder类:
public class ArrayBuilder {
public static <T> void addToList (List<T> listArg, T... elements) {
for (T x : elements) {
listArg.add(x);
}
}
public static void faultyMethod(List<String>... l) {
Object[] objectArray = l; // Valid
objectArray[0] = Arrays.asList(42);
String s = l[0].get(0); // ClassCastException thrown here
}
}
以下示例HeapPollutionExample中用到了ArrayBuiler类:
public class HeapPollutionExample {
public static void main(String[] args) {
List<String> stringListA = new ArrayList<String>();
List<String> stringListB = new ArrayList<String>();
ArrayBuilder.addToList(stringListA, "Seven", "Eight", "Nine");
ArrayBuilder.addToList(stringListB, "Ten", "Eleven", "Twelve");
List<List<String>> listOfStringLists =
new ArrayList<List<String>>();
ArrayBuilder.addToList(listOfStringLists,
stringListA, stringListB);
ArrayBuilder.faultyMethod(Arrays.asList("Hello!"), Arrays.asList("World!"));
}
}
编译时,ArrayBuilder.addToList方法的定义会产生以下警告:
warning: [varargs] Possible heap pollution from parameterized vararg type T
当编译器遇到可变参数方法时,它会将varargs形式参数转换为数组。然而,Java编程语言不允许创建参数化类型的数组。在方法ArrayBuilder.addToList中,编译器将varargs形式参数T... elements转换为形式参数T[] elements,即数组。但是,由于类型擦除,编译器会将varargs形式参数转换为Object[]元素。因此,存在堆污染的可能性。
以下语句将varargs形式参数l赋值给Object数组objectArgs:
Object[] objectArray = l;
该语句可能会引入堆污染。与varargs形式参数l的参数化类型匹配的值可以赋值给变量objectArray,因此这些值可以赋值给l。但是,编译器不会在此语句中生成未经检查的警告。编译器在将varargs形式参数List<String>... l转换为形式参数List[] l时已生成警告。这个语句是有效的;变量l的类型为Object[]的子类型List[]。
因此,如果将任意类型的List对象赋值给objectArray数组的任意数组组件(array component),编译器不会发出警告或错误,如下所示:
objectArray[0] = Arrays.asList(42);
此语句将只包含一个Integer对象的List对象赋值给objectArray数组的第一个数组组件。
假设你使用以下语句调用ArrayBuilder.faultyMethod方法:
ArrayBuilder.faultyMethod(Arrays.asList("Hello!"), Arrays.asList("World!"));
在运行时,JVM会在以下语句中抛出ClassCastException:
// ClassCastException thrown here
String s = l[0].get(0);
存储在变量l的第一个数组组件中的对象具有List<Integer>类型,但此语句需要一个List<String>类型的对象。
阻止从使用了非具体化类型的形式参数的可变参数方法产生警告
如果你声明了一个具有参数化类型参数的可变参数方法,并且确保该方法的主体不会因为不正确地处理varargs形式参数而引发ClassCastException或其他类似的异常,则可以通过向静态方法、final方法和构造方法的方法声明添加以下注解来阻止编译器为这类可变参数方法生成警告:
@SafeVarargs
备注3。
@SafeVarargs注解作为方法契约(method's contract)的一部分陈述;此注解断言方法的实现不会不正确地处理varargs形式参数。
也可以通过在方法声明中添加以下内容来抑制这种警告,尽管这种做法不太可取:
@SuppressWarnings({"unchecked", "varargs"})
但是,此方法不会抑制从方法的调用位置(call site)生成的警告。如果你不熟悉@SuppressWarnings这个语法,请参阅“注解”一节。
泛型的限制
要有效地使用Java泛型,必须考虑以下限制:
- 不能用基本类型实例化泛型类型
- 不能创建形式类型参数的实例
- 不能声明类型为形式类型参数的静态字段
- 不能对参数化类型使用强制类型转换或instanceof
- 不能创建参数化类型的数组
- 不能创建、捕获或抛出参数化类型的对象
- 不能重载类型擦除后形式参数类型为相同原始类型的方法
不能用基本类型实例化泛型类型
考虑以下参数化类型:
class Pair<K, V> {
private K key;
private V value;
public Pair(K key, V value) {
this.key = key;
this.value = value;
}
// ...
}
创建Pair对象时,不能用基本类型替换掉形式类型参数K或V:
Pair<int, char> p = new Pair<>(8, 'a'); // compile-time error
只能用非基本类型替换掉形式类型参数K和V:
Pair<Integer, Character> p = new Pair<>(8, 'a');
请注意,Java编译器将8自动装箱到Integer.valueOf(8),将'a'自动装箱到Character('a'):
Pair<Integer, Character> p = new Pair<>(Integer.valueOf(8), new Character('a'));
有关自动装箱的详细信息,请参阅“Numbers和Strings”课程中的“自动装箱和自动拆箱”一节。
不能创建形式类型参数的实例
你不能创建形式类型参数的实例例如,以下代码会导致编译时错误:
public static <E> void append(List<E> list) {
E elem = new E(); // compile-time error
list.add(elem);
}
作为变通方法,你可以通过反射创建形式类型参数的对象:
public static <E> void append(List<E> list, Class<E> cls) throws Exception {
E elem = cls.newInstance(); // OK
list.add(elem);
}
你可以按以下方式调用append方法:
List<String> ls = new ArrayList<>();
append(ls, String.class);
不能声明类型为形式类型参数的静态字段
类的静态字段是类的所有非静态对象共享的类级变量。因此,声明类型为形式类型参数的静态字段是不允许的。考虑下面的类:
public class MobileDevice<T> {
private static T os;
// ...
}
如果允许声明类型为形式类型参数的静态字段,则以下代码将混淆:
MobileDevice<Smartphone> phone = new MobileDevice<>();
MobileDevice<Pager> pager = new MobileDevice<>();
MobileDevice<TabletPC> pc = new MobileDevice<>();
因为静态字段os是由phone、pager和pc这三个对象共享的,那么os的实际类型是什么?它不能同时是Smartphone,Pager和TabletPC。因此,你无法创建声明类型为形式类型参数的静态字段。
不能对参数化类型使用强制类型转换或instanceof
因为Java编译器会擦除泛型代码中的所有形式类型参数,所以无法验证运行时使用的泛型类型的参数化类型:
public static <E> void rtti(List<E> list) {
if (list instanceof ArrayList<Integer>) { // compile-time error
// ...
}
}
传递给rtti方法的参数化类型的集合是:
S = { ArrayList<Integer>, ArrayList<String> LinkedList<Character>, ... }
运行时不跟踪形式类型参数,因此无法区分ArrayList<Integer>和ArrayList<String>之间的区别。你最多可以做的是使用无界通配符来验证list是否为ArrayList:
public static void rtti(List<?> list) {
if (list instanceof ArrayList<?>) { // OK; instanceof requires a reifiable type
// ...
}
}
通常,除非参数化类型被无界通配符参数化,否则不能将其强制转换为参数化类型。例如:
List<Integer> li = new ArrayList<>();
List<Number> ln = (List<Number>) li; // compile-time error
但是,在某些情况下,编译器知道形式类型参数总是有效的,并且允许强制转换。例如:
List<String> l1 = ...;
ArrayList<String> l2 = (ArrayList<String>)l1; // OK
不能创建参数化类型的数组
你不能创建参数化类型的数组例如,以下代码不能通过编译:
List<Integer>[] arrayOfLists = new List<Integer>[2]; // compile-time error
以下代码说明了将不同类型插入数组时发生的情况:
Object[] strings = new String[2];
strings[0] = "hi"; // OK
strings[1] = 100; // An ArrayStoreException is thrown.
如果你使用泛型列表尝试相同的操作,则会出现问题:
Object[] stringLists = new List<String>[]; // compiler error, but pretend it's allowed
stringLists[0] = new ArrayList<String>(); // OK
stringLists[1] = new ArrayList<Integer>(); // An ArrayStoreException should be thrown,
// but the runtime can't detect it.
如果允许参数化类型的数组,则前面的代码将无法抛出所需的ArrayStoreException。
不能创建、捕获或抛出参数化类型的对象
泛型类不能直接或间接继承Throwable类。例如,以下类将无法编译:
// Extends Throwable indirectly
class MathException<T> extends Exception { /* ... */ } // compile-time error
// Extends Throwable directly
class QueueFullException<T> extends Throwable { /* ... */ // compile-time error
无法捕获形式类型参数的实例的方法:
public static <T extends Exception, J> void execute(List<J> jobs) {
try {
for (J job : jobs)
// ...
} catch (T e) { // compile-time error
// ...
}
}
不过,你可以在throws子句中使用形式类型参数:
class Parser<T extends Exception> {
public void parse(File file) throws T { // OK
// ...
}
}
不能重载类型擦除后形式参数类型为相同原始类型的方法
类不能有两个在类型擦除后具有相同的签名的重载方法。
public class Example {
public void print(Set<String> strSet) { }
public void print(Set<Integer> intSet) { }
}
重载将共享相同的类文件表示法(classfile representation),并将生成编译时错误。
问题和练习
-
编写一个泛型方法来计算集合中具有特定属性的元素数目(例如,奇数,素数,回文数)。
-
下面的类能编译通过吗?如果不能编译通过,为什么呢?
public final class Algorithm { public static <T> T max(T x, T y) { return x > y ? x : y; } }
-
编写一个泛型方法来交换数组中两个不同元素的位置。
-
如果编译器在编译时擦除了所有形式类型参数,为什么要使用泛型?
-
类型擦除后,以下类转换成什么样子?
public class Pair<K, V> { public Pair(K key, V value) { this.key = key; this.value = value; } public K getKey(); { return key; } public V getValue(); { return value; } public void setKey(K key) { this.key = key; } public void setValue(V value) { this.value = value; } private K key; private V value; }
-
类型擦除后,以下方法转换成什么样子?
public static <T extends Comparable<T>> int findFirstGreaterThan(T[] at, T elem) { // ... }
-
下面的方法能编译通过吗?如果不能编译通过,为什么呢?
public static void print(List<? extends Number> list) { for (Number n : list) System.out.print(n + " "); System.out.println(); }
-
编写一个泛型方法,在列表的范围[begin, end)中找到最大元素。
-
下面的类能编译通过吗?如果不能编译通过,为什么呢?
public class Singleton<T> { public static T getInstance() { if (instance == null) instance = new Singleton<T>(); return instance; } private static T instance = null; }
-
给出以下类:
class Shape { /* ... */ } class Circle extends Shape { /* ... */ } class Rectangle extends Shape { /* ... */ } class Node<T> { /* ... */ }
下面的代码能编译通过吗?如果不能编译通过,为什么呢?
Node<Circle> nc = new Node<>(); Node<Shape> ns = nc;
-
考虑这个类:
class Node<T> implements Comparable<T> { public int compareTo(T obj) { /* ... */ } // ... }
下面的代码能编译通过吗?如果不能编译通过,为什么呢?
Node<String> node = new Node<>(); Comparable<String> comp = node;
-
如何调用以下方法来查找列表中的第一个和指定列表中的整数互质的整数?
public static <T> int findFirst(List<T> list, int begin, int end, UnaryPredicate<T> p)
注意,如果gcd(a, b) = 1,则两个整数a和b互质,其中gcd是最大公因子的缩写。
检查你的答案。
问题和练习的答案
-
编写一个泛型方法来计算集合中具有特定属性的元素数目(例如,奇数,素数,回文数)。
答案:
public final class Algorithm { public static <T> int countIf(Collection<T> c, UnaryPredicate<T> p) { int count = 0; for (T elem : c) if (p.test(elem)) ++count; return count; } }
其中,泛型UnaryPredicate接口定义如下:
public interface UnaryPredicate<T> { public boolean test(T obj); }
例如,下面的程序计算一个整数列表中奇数的个数:
import java.util.*; class OddPredicate implements UnaryPredicate<Integer> { public boolean test(Integer i) { return i % 2 != 0; } } public class Test { public static void main(String[] args) { Collection<Integer> ci = Arrays.asList(1, 2, 3, 4); int count = Algorithm.countIf(ci, new OddPredicate()); System.out.println("Number of odd integers = " + count); } }
该程序输出如下:
Number of odd integers = 2
-
下面的类能编译通过吗?如果不能编译通过,为什么呢?
public final class Algorithm { public static <T> T max(T x, T y) { return x > y ? x : y; } }
答案: 不能。大于(>)运算符仅适用于基本数字类型。
-
编写一个泛型方法来交换数组中两个不同元素的位置。
答案:
public final class Algorithm { public static <T> void swap(T[] a, int i, int j) { T temp = a[i]; a[i] = a[j]; a[j] = temp; } }
-
如果编译器在编译时擦除了所有形式类型参数,为什么要使用泛型?
答案: 你应该使用泛型,因为:
Java 编译器在编译时对泛型代码强制执行更严格的类型检查。
泛型支持类型作为参数的编程。
泛型使你能够实现泛型算法。 -
类型擦除后,以下类转换成什么样子?
public class Pair<K, V> { public Pair(K key, V value) { this.key = key; this.value = value; } public K getKey(); { return key; } public V getValue(); { return value; } public void setKey(K key) { this.key = key; } public void setValue(V value) { this.value = value; } private K key; private V value; }
答案:
public class Pair { public Pair(Object key, Object value) { this.key = key; this.value = value; } public Object getKey() { return key; } public Object getValue() { return value; } public void setKey(Object key) { this.key = key; } public void setValue(Object value) { this.value = value; } private Object key; private Object value; }
-
类型擦除后,以下方法转换成什么样子?
public static <T extends Comparable<T>> int findFirstGreaterThan(T[] at, T elem) { // ... }
答案:
public static int findFirstGreaterThan(Comparable[] at, Comparable elem) { // ... }
-
下面的方法能编译通过吗?如果不能编译通过,为什么呢?
public static void print(List<? extends Number> list) { for (Number n : list) System.out.print(n + " "); System.out.println(); }
答案: 能。
-
编写一个泛型方法,在列表的范围[begin, end)中找到最大元素。
答案:
import java.util.*; public final class Algorithm { public static <T extends Object & Comparable<? super T>> T max(List<? extends T> list, int begin, int end) { T maxElem = list.get(begin); for (++begin; begin < end; ++begin) if (maxElem.compareTo(list.get(begin)) < 0) maxElem = list.get(begin); return maxElem; } }
-
下面的类能编译通过吗?如果不能编译通过,为什么呢?
public class Singleton<T> { public static T getInstance() { if (instance == null) instance = new Singleton<T>(); return instance; } private static T instance = null; }
答案: 不能。无法创建声明类型为形式类型参数T的静态字段。
-
给出以下类:
class Shape { /* ... */ } class Circle extends Shape { /* ... */ } class Rectangle extends Shape { /* ... */ } class Node<T> { /* ... */ }
下面的代码能编译通过吗?如果不能编译通过,为什么呢?
Node<Circle> nc = new Node<>(); Node<Shape> ns = nc;
答案: 不能。因为Node<Circle>不是Node<Shape>的子类型。
Consider this class: class Node<T> implements Comparable<T> { public int compareTo(T obj) { /* ... */ } // ... }
-
考虑这个类:
class Node<T> implements Comparable<T> { public int compareTo(T obj) { /* ... */ } // ... }
下面的代码能编译通过吗?如果不能编译通过,为什么呢?
Node<String> node = new Node<>(); Comparable<String> comp = node;
答案: 能。
-
如何调用以下方法来查找列表中的第一个和指定列表中的整数互质的整数?
public static <T> int findFirst(List<T> list, int begin, int end, UnaryPredicate<T> p)
注意,如果gcd(a, b) = 1,则两个整数a和b互质,其中gcd是最大公因子的缩写。
答案:
import java.util.*; public final class Algorithm { public static <T> int findFirst(List<T> list, int begin, int end, UnaryPredicate<T> p) { for (; begin < end; ++begin) if (p.test(list.get(begin))) return begin; return -1; } // x > 0 and y > 0 public static int gcd(int x, int y) { for (int r; (r = x % y) != 0; x = y, y = r) { } return y; } }
泛型UnaryPredicate接口定义如下:
public interface UnaryPredicate<T> { public boolean test(T obj); }
以下程序测试findFirst方法:
import java.util.*; class RelativelyPrimePredicate implements UnaryPredicate<Integer> { public RelativelyPrimePredicate(Collection<Integer> c) { this.c = c; } public boolean test(Integer x) { for (Integer i : c) if (Algorithm.gcd(x, i) != 1) return false; return c.size() > 0; } private Collection<Integer> c; } public class Test { public static void main(String[] args) throws Exception { List<Integer> li = Arrays.asList(3, 4, 6, 8, 11, 15, 28, 32); Collection<Integer> c = Arrays.asList(7, 18, 19, 25); UnaryPredicate<Integer> p = new RelativelyPrimePredicate(c); int i = ALgorithm.findFirst(li, 0, li.size(), p); if (i != -1) { System.out.print(li.get(i) + " is relatively prime to "); for (Integer k : c) System.out.print(k + " "); System.out.println(); } } }
该程序输出如下:
11 is relatively prime to 7 18 19 25
备注
备注1
Polymorphism (computer science) - Wikipedia
备注2
static <T> T pick(T a1, T a2) { return a2; }
Serializable s = pick("d", new ArrayList<String>());
The compiler looks at the expression and tries to find types that match, both for input and output. In your case we see that pick takes a T, what is T? It is used as return and as input of two things. In the call to pick we have "d", a String literal, we have an ArrayList, and we have a result of type Serializable. The least type that matches all three is Serializable.
备注3
java - Why we call unbounded wild-card parameterized type as reifiable? - Stack Overflow
备注4
You can use @SafeVarargs for constructors, for static methods, for final methods and since Java 9 for private methods.
java @SafeVarargs why do private methods need to be final - Stack Overflow
[JDK-7196160] Project Coin: Allow @SafeVarargs on private methods - Java Bug System