Java泛型教程

Updated on with 7,139 views and 4 comments

前言

本文译自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> 的子类型。

在使用泛型编程时,这是一个常见的误解,但这是一个需要学习的重要概念。

genericssubtypeRelationship.gif
图表显示 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> 的子类型。

genericssampleHierarchy.gif
集合层次结构的示例
现在假设我们想要定义我们自己的列表接口 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>。

genericspayloadListHierarchy.gif
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> 的公共父级是未知类型列表的关系图
genericslistParent.gif
公共父类是 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 类之间的关系。

genericswildcardSubtyping.gif
几个泛型列表类声明的层次结构。
“通配符使用指南”一节提供了有关上界通配符和下界通配符使用上的更多信息。

通配符捕获和助手函数

在某些情况下,编译器会推断出通配符的类型。例如,列表可以定义为 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 表达式中不能使用非具体化类型,以及不能使用非具体化类型作为数组中的元素。

堆污染

当参数化类型的变量引用非该参数化类型的对象时,就会发生堆污染。如果程序执行了一些在编译时引起未经检查的警告的操作,就会发生这种情况。如果在编译时(在编译时类型检查规则的限制内)或在运行时,无法验证涉及参数化类型的操作(例如,强制转换或方法调用)的正确性,则会生成未经检查的警告。例如,当混合原始类型和参数化类型时,或者当执行未经检的强制转换时,就会发生堆污染。

在正常情况下,当所有代码同时编译时,编译器会发出未经检查的警告,以提醒你注意潜在的堆污染。如果单独编译部分代码,则很难检测到堆污染的潜在风险。如果确保代码编译时没有警告产生,则不会发生堆污染。

具有非具体化类型的形式参数的可变参数方法的潜在漏洞

包含 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),并将生成编译时错误。

问题和练习

  1. 编写一个泛型方法来计算集合中具有特定属性的元素数目(例如,奇数,素数,回文数)。

  2. 下面的类能编译通过吗?如果不能编译通过,为什么呢?

    public final class Algorithm {
        public static <T> T max(T x, T y) {
            return x > y ? x : y;
        }
    }
    
  3. 编写一个泛型方法来交换数组中两个不同元素的位置。

  4. 如果编译器在编译时擦除了所有形式类型参数,为什么要使用泛型?

  5. 类型擦除后,以下类转换成什么样子?

    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;
    }
    
  6. 类型擦除后,以下方法转换成什么样子?

    public static <T extends Comparable<T>>
        int findFirstGreaterThan(T[] at, T elem) {
        // ...
    }
    
  7. 下面的方法能编译通过吗?如果不能编译通过,为什么呢?

    public static void print(List<? extends Number> list) {
        for (Number n : list)
            System.out.print(n + " ");
        System.out.println();
    }
    
  8. 编写一个泛型方法,在列表的范围[begin, end)中找到最大元素。

  9. 下面的类能编译通过吗?如果不能编译通过,为什么呢?

    public class Singleton<T> {
    
        public static T getInstance() {
            if (instance == null)
                instance = new Singleton<T>();
    
            return instance;
        }
    
        private static T instance = null;
    }
    
  10. 给出以下类:

    class Shape { /* ... */ }
    class Circle extends Shape { /* ... */ }
    class Rectangle extends Shape { /* ... */ }
    
    class Node<T> { /* ... */ }
    

    下面的代码能编译通过吗?如果不能编译通过,为什么呢?

    Node<Circle> nc = new Node<>();
    Node<Shape>  ns = nc;
    
  11. 考虑这个类:

    class Node<T> implements Comparable<T> {
        public int compareTo(T obj) { /* ... */ }
        // ...
    }
    

    下面的代码能编译通过吗?如果不能编译通过,为什么呢?

    Node<String> node = new Node<>();
    Comparable<String> comp = node;
    
  12. 如何调用以下方法来查找列表中的第一个和指定列表中的整数互质的整数?

    public static <T> int findFirst(List<T> list, int begin, int end, UnaryPredicate<T> p)
    

    注意,如果 gcd(a, b) = 1,则两个整数 a 和 b 互质,其中 gcd 是最大公因子的缩写。

检查你的答案。

问题和练习的答案

  1. 编写一个泛型方法来计算集合中具有特定属性的元素数目(例如,奇数,素数,回文数)。

    答案:

    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
    
  2. 下面的类能编译通过吗?如果不能编译通过,为什么呢?

    public final class Algorithm {
        public static <T> T max(T x, T y) {
            return x > y ? x : y;
        }
    }
    

    答案: 不能。大于(>)运算符仅适用于基本数字类型。

  3. 编写一个泛型方法来交换数组中两个不同元素的位置。

    答案:

    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;
        }
    }
    
  4. 如果编译器在编译时擦除了所有形式类型参数,为什么要使用泛型?

    答案: 你应该使用泛型,因为:
    Java 编译器在编译时对泛型代码强制执行更严格的类型检查。
    泛型支持类型作为参数的编程。
    泛型使你能够实现泛型算法。

  5. 类型擦除后,以下类转换成什么样子?

    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;
    }
    
  6. 类型擦除后,以下方法转换成什么样子?

    public static <T extends Comparable<T>>
        int findFirstGreaterThan(T[] at, T elem) {
        // ...
    }
    

    答案:

    public static int findFirstGreaterThan(Comparable[] at, Comparable elem) {
        // ...
    }
    
  7. 下面的方法能编译通过吗?如果不能编译通过,为什么呢?

    public static void print(List<? extends Number> list) {
        for (Number n : list)
            System.out.print(n + " ");
        System.out.println();
    }
    

    答案: 能。

  8. 编写一个泛型方法,在列表的范围[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;
        }
    }
    
  9. 下面的类能编译通过吗?如果不能编译通过,为什么呢?

    public class Singleton<T> {
    
        public static T getInstance() {
            if (instance == null)
                instance = new Singleton<T>();
    
            return instance;
        }
    
        private static T instance = null;
    }
    

    答案: 不能。无法创建声明类型为形式类型参数 T 的静态字段。

  10. 给出以下类:

    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) { /* ... */ }
        // ...
    }
    
  11. 考虑这个类:

    class Node<T> implements Comparable<T> {
        public int compareTo(T obj) { /* ... */ }
        // ...
    }
    

    下面的代码能编译通过吗?如果不能编译通过,为什么呢?

    Node<String> node = new Node<>();
    Comparable<String> comp = node;
    

    答案: 能。

  12. 如何调用以下方法来查找列表中的第一个和指定列表中的整数互质的整数?

    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
    

标题:Java泛型教程
作者:zhoujin7
地址:https://zhoujin7.com/javase-tutorial-java-generics

Responses