JAVA官方文档2024——泛型

原文:https://docs.oracle.com/javase/tutorial/java/generics/index.html
本文参考此文,并与官方原文比对,补充了丢失的部分并进行了翻译,对其中部分错误进行了修正,并补充了个人在阅读过程中产生的一些思考和疑惑。

文章目录

一、泛型介绍(Generics)

JDK5.0为Java编程语言引入了几个新的扩展。其中之一就是泛型(generics)的引入。

这条线索是对泛型的介绍。您可能熟悉其他语言中的类似结构,尤其是C++模板。如果是这样的话,你会发现两者既有相似之处,也有重要的区别。如果你不熟悉其他地方的相似结构,那就更好了;你可以重新开始,而不必忘记任何误解。

泛型允许对类型进行抽象。最常见的例子是容器类型,例如“集合”层次结构中的容器类型。

二、为什么要使用泛型?(Why Use Generics?)

简而言之,泛型使类型(类和接口)在定义类、接口和方法时成为参数。与方法声明中使用的更熟悉的形式参数非常相似,类型参数为您提供了一种使用不同输入重用相同代码的方法。不同之处在于,形式参数的输入是值,而类型参数的输入则是类型。

使用泛型的代码比非泛型代码有很多好处:

  • 在编译时进行更强的类型检查(Stronger type checks at compile time)

Java编译器对泛型代码应用强类型检查,如果代码违反类型安全,则会发出错误。修复编译时错误比修复运行时错误更容易,因为运行时错误很难找到。

  • 减少转型(Elimination of casts)

以下没有泛型的代码段需要强制转换:

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
  • 使程序员能够实现通用算法(Enabling programmers to implement generic algorithms)

通过使用泛型(generics),程序员可以实现泛型算法,这些算法适用于不同类型的集合,可以自定义,并且类型安全且更易于阅读。

三、泛型类型(Generic Types)

泛型类型是在类型上参数化的泛型类或接口。将修改以下 Box 类以演示该概念。

1、一个简单的盒子类(A Simple Box Class)

首先检查一个对任何类型的对象进行操作的非泛型Box类。它只需要提供两种方法:set和get,前者向框中添加对象,后者检索对象:

public class Box {
    private Object object;

    public void set(Object object) { this.object = object; }
    public Object get() { return object; }
}

由于它的方法接受或返回Object,所以只要它不是**基本类型(primitive types)**之一,就可以自由地传入任何您想要的内容。在编译时,无法验证类是如何使用的。代码的一部分可能会将Integer放在框中,并期望从中取出Integers,而代码的另一部分可能错误地传入String,从而导致运行时错误。

2、盒子类的泛型版本(A Generic Version of the Box Class)

泛型类使用以下格式定义:

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; }
}

正如您所看到的,Object的所有出现都被T替换。类型变量可以是您指定的任何非基本类型:任何类型、任何接口类型、任何数组类型,甚至其他类型变量。

个人补充:

// 类
Box<Character> box;
// 接口
Box<CharSequence> boxInf;
// 数组
Box<Character[]> boxArr;

这种相同的技术可以应用于创建通用接口。

3、类型参数命名约定(Type Parameter Naming Conventions)

按照约定,类型参数名称是单个大写字母。这与你已经知道的变量命名约定形成鲜明对比,并且有充分的理由:如果没有这个约定,就很难区分类型变量和普通类或接口名称之间的区别。

最常用的类型参数名称包括:

  • E - Element(被Java集合框架广泛使用)
  • K - 键
  • N - 数字
  • T - 类型
  • V - 值
  • S,U,V等- 第2、3、4类

您将看到这些名称在整个 Java SE API 和本课程的其余部分使用。

4、调用和实例化泛型类型(Invoking and Instantiating a Generic Type)

要从代码中引用泛型Box类,必须执行泛型类型调用,该调用将T替换为一些具体值,例如Integer:

Box<Integer> integerBox;

您可以将泛型类型调用视为类似于普通方法调用,但不是将参数传递给方法,而是将类型参数(在本例中为Integer)传递给Box类本身。

与任何其他变量声明一样,此代码实际上并没有创建新的Box对象。它只是简单地声明integerBox将保存对“Box of Integer”的引用,这就是读取Box<Integer>的方式。

个人补充

字节码如下,可以看到信息被保存在签名中,同时Class方法有个私有native方法getGenericSignature0可以获取签名

LOCALVARIABLE box Lgenerics/Box; L1 L4 1
// signature Lgenerics/Box<Ljava/lang/Character;>;
// declaration: box extends generics.Box<java.lang.Character>
LOCALVARIABLE boxInf Lgenerics/Box; L2 L4 2
// signature Lgenerics/Box<Ljava/lang/CharSequence;>;
// declaration: boxInf extends generics.Box<java.lang.CharSequence>
LOCALVARIABLE boxArr Lgenerics/Box; L3 L4 3
// signature Lgenerics/Box<[Ljava/lang/Character;>;
// declaration: boxArr extends generics.Box<java.lang.Character[]>

泛型类型的调用(An invocation of a generic type)通常被称为参数化类型( parameterized type)

要实例化这个类,请像往常一样使用new关键字,但将<Integer>放在类名和括号之间:

Box<Integer> integerBox = new Box<Integer>();
5、钻石(Diamond)

在Java SE 7及更高版本中,只要编译器能够从上下文中确定或推断类型参数,就可以将调用泛型类的构造函数所需的类型参数替换为一组空的类型参数(<>)。这对尖括号,<>,被非正式地称为钻石。例如,您可以使用以下语句创建Box<Integer>的实例:

Box<Integer> integerBox = new Box<>();
6、多种类型参数(Multiple Type Parameters)

如前所述,泛型类可以有多个类型参数。例如,泛型 OrderedPair 类,它实现了泛型 Pair 接口:

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 的构造函数的参数类型分别为字符串和整数。由于自动装箱,将字符串和 int 传递给类是有效的。

如 The Diamond 中所述,由于 Java 编译器可以从声明 OrderedPair<String, Integer> 推断出 K 和 V 类型,因此可以使用菱形表示法缩短这些语句:

OrderedPair<String, Integer> p1 = new OrderedPair<>("Even", 8);
OrderedPair<String, String>  p2 = new OrderedPair<>("hello", "world");

若要创建泛型接口,请遵循与创建泛型类相同的约定。

7、参数化类型(parameterized type)

还可以将类型参数type parameter(即 K 或 V)替换为参数化类型parameterized type(即 List)。例如,使用 OrderedPair<K, V> 示例:

OrderedPair<String, Box<Integer>> p = new OrderedPair<>("primes", new Box<Integer>(...));
8、原始类型(Raw Types)

**原始类型(raw type)**是指不包含任何类型参数的泛型类或接口,例如,给定泛型 Box 类:

public class Box<T> {
    public void set(T t) { /* ... */ }
    // ...
}

为了创建 Box<T>参数化类型(parameterized type),你需要为形式类型参数 T 提供一个实际的类型参数:

Box<Integer> intBox = new Box<>();// Integer就是参数化类型

如果省略了实际的类型参数,你将创建 Box<T> 的原始类型:

Box rawBox = new Box();

因此,Box 是泛型类型 Box<T> 的原始类型(raw type)。然而,非泛型类或接口类型并不算作原始类型。

原始类型在历史代码中很常见,因为许多 API 类(如 Collections 类)在 JDK 5.0 之前并不是泛型的。在使用原始类型时,你基本上会得到泛型出现之前的那种行为(就是没有泛型的状态) —— 一个 Box 会给你返回 Object 类型的对象。为了向后兼容,将参数化类型赋值给其原始类型是被允许的

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 call to set(T)

警告表明原始类型绕过了泛型类型检查,将不安全代码的捕获延迟到了运行时。因此,你应该避免使用原始类型
关于 Java 编译器如何使用原始类型,以及更多关于类型擦除(Type Erasure)的信息,可以在 类型擦除 这一部分找到详细说明。

类型擦除是 Java 泛型实现的一个关键概念,它描述了编译器如何处理泛型类型,以便能在 JVM 上运行,因为 JVM 本身并不理解泛型的概念。在编译阶段,所有泛型信息都被擦除,转换为原始类型,同时保留必要的类型检查和转换,以确保运行时的类型安全。尽管如此,使用原始类型仍然会丢失类型信息,从而可能引入类型安全问题,因此在现代 Java 编程实践中应尽量避免。

未检查的错误信息(Unchecked Error Messages)

警告可能包括但不限于以下错误

  • 使用原始类型。
  • 将参数化类型赋值给其原始类型。
  • 调用泛型类型的方法时使用了原始类型。
  • 执行未经检查的转换,例如将泛型类型的实例赋值给非泛型类型的变量,这可能导致运行时的 ClassCastException。

正如之前提到的,当混合使用历史代码与泛型代码时,你可能会遇到类似以下的警告信息:

Note: Example.java uses unchecked or unsafe operations.
Note: Recompile with -Xlint:unchecked for details.

这可能发生在使用较旧的 API 且该 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 的语法不熟悉,可以查阅 注解Annotations 相关文档。

四、泛型方法(Generic Methods)

泛型方法是引入自己的类型参数的方法。这类似于声明泛型类型,但类型参数的作用域仅限于声明它的方法。允许使用静态和非静态泛型方法以及泛型类构造函数

泛型方法的语法包括一个类型参数列表,位于尖括号内,显示在方法的返回类型之前。对于静态泛型方法,类型参数部分必须出现在方法的返回类型之前。

Util类包括一个通用方法compare,用于比较两个Pair对象:

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);

此功能称为类型推断,允许您将泛型方法作为普通方法调用,而无需在尖括号之间指定类型

五、有界类型参数(Bounded Type Parameters)

有时可能需要限制可以用作参数化类型中的类型参数的类型。例如,一个对数字进行操作的方法可能只想接受Number或其子类的实例。这就是有界类型参数的作用。

要声明有界类型参数,请列出类型参数的名称,后跟extends关键字,然后是其上界,在本例中为Number。请注意,在本文中,extends在一般意义上用于表示 “扩展(extends)”(如在类中)或 “ 实现(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!
    }
}

除了限制可用于实例化泛型类型的类型外,有界类型参数还允许您调用在边界中定义的方法

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方法。

多个边界(Multiple Bounds)

前面的示例说明了使用具有单个边界的类型参数,但类型参数可以具有多个边界:

<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
静态方法和有界类型参数(Generic Methods and Bounded Type Parameters)

有界类型参数对于实现通用算法很关键。考虑下面的方法去计算数组T[]中大于指定元素elem的元素的数量

public static <T> int countGreaterThan(T[] anArray, T elem) {
    int count = 0;
    for (T e : anArray)
        if (e > elem)  // compiler error
            ++count;
    return count;
}

该方法的实现很直接,但是它不能编译,因为大于号运算符 (>) 只能应用于诸如 shortintdoublelongfloatbytechar 这样的基本类型。你不能使用 > 运算符来比较对象。为了解决这个问题,可以使用由 Comparable<T> 接口限定的类型参数:

public interface Comparable<T> {
    public int compareTo(T o);
}

最后的代码将会变成:

public static <T extends Comparable<T>> int countGreaterThan(T[] anArray, T elem) {
    int count = 0;
    for (T e : anArray)
        if (e.compareTo(elem) > 0)
            ++count;
    return count;
}

六、泛型、继承和子类型(Generics, Inheritance, and Subtypes)

正如您已经知道的,只要类型兼容,就可以将一种类型的对象分配给另一种类型。例如,您可以将Integer指定给Object,因为Object是Integer的**超类型(supertypes)**之一:

Object someObject = new Object();
Integer someInteger = new Integer(10);
someObject = someInteger;   // OK

在面向对象的术语中,这被称为 “is a” 关系。由于Integer是Object中的一类,因此可以进行赋值。但Integer也是Number中的一类,因此以下代码也是有效的:

原文:In object-oriented terminology, this is called an “is a” relationship. Since an Integer is a kind of Object, the assignment is allowed. But Integer is also a kind of Number, so the following code is valid as well:

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>的子类型。

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

泛型类和子类型(Generic Classes and Subtyping)

您可以通过扩展或实现泛型类或接口来对其进行子类型划分。一个类或接口的类型参数与另一个类的类型参数之间的关系由extends和implements子句决定。

以Collections类为例,ArrayList<E>实现了List<E>,List<E>扩展了Collection<E>。所以ArrayList<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>

七、类型推断(Type Inference)

类型推断是Java编译器查看每个方法调用和相应声明的能力,以确定使调用适用的类型参数。推理算法确定参数的类型,如果可用,还确定分配或返回结果的类型。最后,推理算法试图找到适用于所有参数的最具体的类型。

为了说明最后一点,在以下示例中,推断确定传递给pick方法的第二个参数的类型为Serializable:

static <T> T pick(T a1, T a2) { return a2; }
Serializable s = pick("d", new ArrayList<String>());
类型推断和泛型方法

泛型方法向您介绍了类型推理,它使您能够像调用普通方法一样调用泛型方法,而无需在尖括号之间指定类型。考虑以下示例BoxDemo,它需要Box类:

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,可以指定具有类型见证的类型参数,如下所示:

BoxDemo.<Integer>addBox(Integer.valueOf(10), listOfIntegerBoxes);

或者,如果省略类型见证,Java编译器会自动推断(从方法的参数)类型参数是Integer:

BoxDemo.addBox(Integer.valueOf(20), listOfIntegerBoxes);
泛型类的类型推断和实例化

只要编译器可以从上下文中推断类型参数,就可以用一组空的类型parameters (<>)替换调用泛型类的构造函数所需的类型参数。这对尖括号被非正式地称为diamond。

例如,考虑以下变量声明:

Map<String, List<String>> myMap = new HashMap<String, List<String>>();

您可以将构造函数的参数化类型替换为一组空的类型parameters(<>):

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

目标类型(Target Types)

Java编译器利用目标类型来推断泛型方法调用的类型参数。表达式的目标类型是Java编译器所期望的数据类型,具体取决于表达式的出现位置。考虑Collections.emptyList方法,该方法声明如下:

static <T> List<T> emptyList();

请考虑以下赋值语句:

List<String> listOne = Collections.emptyList();

此语句需要List<String>的一个实例;此数据类型是目标类型。由于方法emptyList返回List<T>类型的值,因此编译器推断类型参数T必须是值String。这在Java SE 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());

See Target Typing in Lambda Expressions for more information.

八、通配符(Wildcards)

在泛型代码中,被称为通配符的问号(?)表示未知类型。通配符可以用于各种情况:作为参数、字段或局部变量的类型;有时作为返回类型(尽管更具体一些是更好的编程实践)。通配符永远不会用作泛型方法调用、泛型类实例创建或超类型的类型参数(The wildcard is never used as a type argument for a generic method invocation, a generic class instance creation, or a supertype.)。

以下部分将更详细地讨论通配符,包括上界通配符、下界通配符和通配符捕获。

1、上限通配符(Upper Bounded Wildcards)

可以使用上限通配符来放宽对变量的限制。例如,假设您想编写一个在List<Integer>List<Double>List<Number>上工作的方法;您可以通过使用上限通配符来实现这一点。

要声明一个上界通配符,请使用通配符符号(“?”),后跟extends关键字,再后跟其上界。请注意,在本文中,extends在大多场景上用于表示“扩展”(如在类中)或“实现”(如接口中)。

要编写适用于Number列表和Number子类型(如Integer、Double和Float)的方法,您需要指定List<?extends Number>。术语 List<Number>List<? extends Number>限制性更强,因为前者只匹配Number类型的列表,而后者匹配Number类型或其任何子类的列表。

请考虑以下处理方法:

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));

双精度值列表可以使用相同的 sumOfList 方法。以下代码打印 sum = 7.0:

List<Double> ld = Arrays.asList(1.2, 2.3, 3.5);
System.out.println("sum = " + sumOfList(ld));
2、无限通配符

个人补充:所有类都是Object的子类型,如DoubleObject子类型,但对于泛型类,List<Double>之类的不是List<Object>的子类型,这也是通配符出现的原因

重点:List<Object>可以插入任何子类型,但List<?>只能插入null

无界通配符类型是使用通配符(?)指定的,例如List<?>。这被称为未知类型的列表。有两种情况下,无界通配符是一种有用的方法:

  • 如果您正在编写一个可以使用Object类中提供的功能实现的方法。

  • 当代码在泛型类中使用不依赖于类型参数的方法时。例如List.sizeList.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的目标是打印任何类型的列表,但它无法实现这一目标——它只打印对象实例的列表;它无法打印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();
}

// 个人补充:需要注意的是,由于类型擦除,以上两个方法参数是一样的,所以需要修改方法名称

因为对于任何具体类型AList<A>都是List<?>的子类型,您可以使用printList打印任何类型的列表:

List<Integer> li = Arrays.asList(1, 2, 3);
List<String>  ls = Arrays.asList("one", "two", "three");
printList(li);
printList(ls);

Note: Arrays.asList 方法被贯穿使用于本节课程。这个**静态工厂(static factory )**方法会转换声明的数组并返回固定大小的列表


需要注意的是,List<Object>List<?>不一样。您可以将Object或Object的任何子类型插入到List<Object>中。但您只能在List<?>中插入null。通配符使用指南部分Guidelines for Wildcard Use提供了有关如何确定在给定情况下应使用哪种通配符(如果有的话)的更多信息。

3、下限通配符

上界通配符章节展示了上界通配符如何限制未知类型特定类型或该类型的子类型(subtype ),这通过使用 extends 关键字来表示。类似地,下界通配符限制未知类型为特定类型或该类型的超类型(super type)

下界通配符通过使用通配符字符(?),后跟 super 关键字,再后跟其下界来表示:<? super A>


注意: 你可以为通配符指定上界,或者指定下界,但不能同时指定两者。


假设你想要编写一个方法,用于向列表中添加 Integer 对象。为了最大化灵活性,你希望该方法能在 List<Integer>List<Number>List<Object> 等任何可以容纳 Integer 值的列表上工作。

为了编写一个能在 Integer 类型的列表及其超类型列表上工作的方法,如 IntegerNumberObject,你会指定 List<? super Integer>List<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);
    }
}

Guidelines for Wildcard Use 章节将展示何时使用上限通配符、何时使用下限通配符

4、通配符与子类型

泛型、继承与子类型 中所述,不能仅仅因为它们的类型之间存在关联,就认为泛型类或接口之间就存在关联。然而,你可以使用通配符来创建泛型类或接口之间的关系。

考虑以下两个常规(非泛型)类:

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

鉴于 IntegerNumber 的子类型,List<Integer>List<Number> 之间是什么关系呢?

图解显示 List<Number>List<Integer> 的共同父类型是未知类型的列表

共同的父类型是 List<?>

尽管 IntegerNumber 的子类型,但

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>

由于 IntegerNumber 的子类型,而 numListNumber 对象的列表,现在 intListInteger 对象的列表)与 numList 之间存在一种关系。下图展示了使用上界和下界通配符声明的多个 List 类之间的关系。

​ 多个泛型 List 类声明的层次结构

图解显示(extend看继承,super看是否更严格)

  • List<Integer> 同时是 List<? extends Integer>List<? super Integer> 的子类型。
  • List<? extends Integer>List<? extends Number> 的子类型,
  • List<? extends Number> 又是 List<?> 的子类型。
  • List<Number>List<? super Number>List<? extends Number> 的子类型。
  • List<? super Number>List<? super Integer> 的子类型
  • List<? super Integer> 又是 List<?> 的子类型。

通配符使用指南 部分提供了更多关于使用上界和下界通配符的影响的信息。

思考: List<? extends Integer>List<? super Integer> 的下面等式结果是什么?把a2换成List<? extends Number>List<?>呢?答案会不一样吗?

List<? super Integer> a = new ArrayList<>();
List<? extends Integer> a2 = new ArrayList<>();
System.out.println(a == a2);
System.out.println(a.equals(a2));
System.out.println(a.getClass() == a2.getClass());

编译后的字节码如下

LOCALVARIABLE a Ljava/util/List; L11 L19 4
// signature Ljava/util/List<-Ljava/lang/Integer;>;
// declaration: a extends java.util.List<? super java.lang.Integer>
LOCALVARIABLE a2 Ljava/util/List; L12 L19 5
// signature Ljava/util/List<+Ljava/lang/Integer;>;
// declaration: a2 extends java.util.List<? extends java.lang.Integer>
5、通配符捕获与辅助方法

在某些情况下,编译器会推断出通配符的类型。例如,一个列表可能被定义为 List<?>,但在评估一个表达式时,编译器会从代码中推断出一个具体的类型。这种情形被称为通配符捕获

大多数情况下,你不必过于担心通配符捕获,除非你遇到包含“capture of”短语的错误信息。

WildcardError 示例在编译时会产生一个捕获错误:

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 语言中——为了在编译时强制类型安全

使用 Oracle 的 JDK 7 javac 实现编译 WildcardError 示例时,会生成以下错误:

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> list) {
        list.set(0, list.get(0));
    }

}

多亏了辅助方法,编译器使用推断确定 TCAP#1,即捕获变量,在调用中。现在这个示例可以成功编译。

按照惯例,辅助方法一般命名为 *原方法名*Helper

现在考虑一个更复杂的示例,WildcardErrorBad

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)); // 预期 CAP#1 extends Number,
                            // 得到的是 CAP#2 extends Number;
                            // 相同的边界,但不同的类型
      l2.set(0, temp);	    // 预期 CAP#1 extends Number,
                            // 得到的是 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

没有辅助方法可以绕过这个问题,因为代码本质上是错误的:从 Integer 值的列表中取出一项并试图放入 Double 值的列表中显然是不正确的。

6、通配符使用指南(Guidelines for Wildcard Use)

学习使用泛型编程时,最令人困惑的方面之一就是确定何时使用上界通配符以及何时使用下界通配符。本页面提供了一些设计代码时应遵循的指导原则。

为了便于讨论,可以将变量视为具有以下两种功能之一:

  • “输入”变量:向代码提供数据。想象一个带有两个参数的复制方法 copy(src, dest)src 参数提供要复制的数据,因此它是“输入”参数。
  • “输出”变量:保存数据供其他地方使用。在复制示例 copy(src, dest) 中,dest 参数接收数据,因此它是“输出”参数。

当然,有些变量同时用于“输入”和“输出”目的——这一点在指导原则中也有提及。

当你决定是否使用通配符以及使用哪种类型的通配符时,可以应用“输入”和“输出”的原则。下面列出了一些指导原则:


通配符指导原则:

  • “输入”变量使用上界通配符定义,使用 extends 关键字。
  • “输出”变量使用下界通配符定义,使用 super 关键字。
  • 如果“输入”变量仅能通过 Object 类中定义的方法访问,则使用无界通配符。
  • 如果代码需要将变量作为“输入”和“输出”变量使用,则不使用通配符。

这些指导原则不适用于方法的返回类型。避免将通配符用作返回类型,因为它迫使使用代码的程序员处理通配符问题。

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> 定义的列表在最严格的意义上并不是只读的,但你可能会这样认为,因为你不能在列表中存储新元素或更改现有元素

九、类型擦除(Type Erasure)

泛型被引入Java语言,以在编译时提供更严格的类型检查,并支持泛型编程。为了实现泛型,Java编译器将类型擦除应用于:

  • 将泛型类型中的所有类型参数替换为其边界,如果类型参数是无边界的,则替换为Object。因此,生成的字节码只包含普通的类、接口和方法。

  • 如有必要,请插入类型强制转换以保持类型安全。

  • 生成桥接方法以保留扩展泛型类型中的多态性。

类型擦除确保不会为参数化类型创建新的类;因此,泛型不会产生运行时开销。

1、通用类型的擦除

在类型擦除过程中,如果类型参数是有界的,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; }
    // ...
}

在以下示例中,泛型Node类使用有界类型参数:

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; }
    // ...
}
2、通用方法的擦除

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) { /* ... */ }
3、类型擦除与桥接方法的影响(Effects of Type Erasure and Bridge Methods)

类型擦除有时会导致一些意料之外的情况,以下示例展示了这种情况如何发生。例如编译器可能需要创建一个合成方法(称为桥接方法),作为类型擦除过程的一部分。
考虑以下两个类:

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");     // Causes a ClassCastException to be thrown.
Integer x = mn.data;    

类型擦除后,这段代码变为:

MyNode mn = new MyNode(5);
Node n = mn;            // A raw type - compiler throws an unchecked warning
                        // Note: This statement could instead be the following:
                        //     Node n = (Node)mn;
                        // However, the compiler doesn't generate a cast because
                        // it isn't required.
n.setData("Hello");     // Causes a ClassCastException to be thrown.
Integer x = (Integer)mn.data; 

下文解释了为什么在 n.setData(“Hello”); 这一行会抛出 ClassCastException。

4、桥接方法(Bridge Methods)

当编译一个【继承了参数化类】或【实现了参数化接口】的【类或接口】时,编译器可能需要创建一个合成方法,即桥接方法,作为类型擦除过程的一部分。通常你无需担心桥接方法,但如果它出现在堆栈跟踪中,你可能会感到困惑。
类型擦除后,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(T) 方法变成了 Node.setData(Object)。因此,MyNode.setData(Integer) 方法并没有覆盖 Node.setData(Object) 方法。
为了解决这个问题,并在类型擦除后保留泛型类型的多态性,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);
    }

    // ...
}

桥接方法 MyNode.setData(Object) 代理到原始的 MyNode.setData(Integer) 方法。结果是,n.setData("Hello"); 调用了 MyNode.setData(Object) 方法,由于 "Hello" 无法转换为 Integer 类型,因此抛出了 ClassCastException 异常。

字节码如下,桥接方法使用了**synthetic (合成)**关键字

public synthetic bridge setData(Ljava/lang/Object;)V
L0
LINENUMBER 3 L0
ALOAD 0
ALOAD 1
CHECKCAST java/lang/Integer
INVOKEVIRTUAL generics/bridgeMrthod/MyNode.setData (Ljava/lang/Integer;)V
RETURN
L1
LOCALVARIABLE this Lgenerics/bridgeMrthod/MyNode; L0 L1 0
MAXSTACK = 2
MAXLOCALS = 2

思考一下:synthetic合成关键字的其他使用场景

5、非可实例化类型

类型擦除Type Erasure) 讨论了编译器移除与类型参数和类型实参相关的信息的过程。类型擦除对那些变长参数(也称为 varargs)方法产生影响,特别是当这些方法的变长正式参数具有非可实例化类型时。有关变长参数方法的更多信息,请参阅 任意数量的参数Arbitrary Number of Arguments)部分,该部分位于 向方法或构造函数传递信息Passing Information to a Method or a Constructor )教程中。

本页面涵盖了以下主题:

非可实例化类型

可实例化类型是指在运行时可以完全获取其类型信息的类型,这包括基本类型、非泛型类型、原始类型,以及无界通配符的调用。非可实例化类型则是指那些在编译时由类型擦除过程移除了相关信息的泛型调用,具体来说,就是那些没有被定义为无界通配符的泛型调用。非可实例化类型在运行时并不保留所有类型信息。例如,List<String>List<Number> 这样的类型,在运行时 JVM 无法区分它们。如《泛型的限制(Restrictions on Generics)》章节所述,存在一些情况,非可实例化类型不能被使用,比如在 instanceof 表达式中,或者作为数组的元素

堆污染

当一个参数化类型的变量引用的对象并不是该参数化类型的实例时,就发生了堆污染。这种情况通常发生在程序执行了某些操作,导致在编译时产生了未检查警告。未检查警告会在编译时(在编译时类型检查规则的范围内)或运行时出现,当涉及到参数化类型的运算(如类型转换或方法调用)的正确性无法被验证时。例如,混合使用原始类型和参数化类型,或者进行未检查的类型转换,都可能导致堆污染。
在正常情况下,如果所有代码同时编译,编译器会发出未检查警告来提示你注意可能发生的堆污染。如果你分别编译代码的不同部分,很难检测到堆污染的潜在风险。如果你确保代码在没有警告的情况下编译,那么就不会发生堆污染。

含非可实例化正式参数的变长参数方法的潜在脆弱性

包含变长参数的泛型方法可能会引起堆污染。
考虑下面的 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 使用了 ArrayBuilder 类:

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

当编译器遇到变长参数方法时,它会将变长形式参数转换为数组。但是,Java 编程语言不允许创建参数化类型的数组。在 ArrayBuilder.addToList 方法中,编译器将变长形式参数 T… elements 转换为 T[] elements 形式的数组。然而,由于类型擦除,编译器实际上将其转换为 Object[] elements。因此,存在堆污染的风险。

下面的语句将变长形式参数 l 赋值给了 Object 数组 objectArray

Object[] objectArray = l;

这可能引入堆污染。可以将不符合变长形式参数 l 参数化类型的值赋给变量 objectArray,进而赋给 l。但是,编译器在此语句中不会生成未检查警告。编译器在将变长形式参数 List<String>... l 转换成 List[] l 时已经生成了警告。这个语句是有效的;变量 l 的类型是 List[],它是 Object[] 的子类型。

因此,如果你将任何类型的 List 对象赋值给 objectArray 数组的任意数组元素,如下面的语句所示,编译器不会发出警告或错误:

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> 类型的对象。

防止来自含非可实例化正式参数的变长参数方法的警告

如果你声明了一个具有参数化类型参数的变长参数方法,并且确保方法体不会因为不当处理变长形式参数而抛出 ClassCastException 或其他类似的异常,你可以通过在静态和非构造方法声明中添加以下注解来避免编译器为此类变长参数方法生成警告:

@SafeVarargs // 作用于方法和调用他的地方

@SafeVarargs 注解是方法契约(method’s contract)的一部分;这个注解声明方法的实现不会不当处理变长形式参数。

虽然不太理想,你也可以通过在方法声明中添加以下内容来抑制这类警告:

@SuppressWarnings({"unchecked", "varargs"}) // 只作用于内部,调用的地方还是会提示

但是,这种方法不会抑制从方法调用位置产生的警告。如果你对 @SuppressWarnings 语法不熟悉,可以查阅《注解(Annotations)》的相关资料。

十、泛型的限制(Restrictions on Generics)

为了有效地使用Java泛型,必须考虑以下限制:

不能用基本类型实例化泛型类型

考虑以下参数化类型:

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

你只能用非基本类型替代类型参数KV

Pair<Integer, Character> p = new Pair<>(8, 'a');

注意Java编译器自动装箱8Integer.valueOf(8)'a'Character('a')

Pair<Integer, Character> p = new Pair<>(Integer.valueOf(8), new Character('a'));

关于自动装箱的更多信息,请参阅数字和字符串课程Numbers and Strings中的“自动装箱和拆箱Autoboxing and Unboxing”。

不能创建类型参数的实例

你不能创建类型参数的实例。例如,以下代码会导致编译时错误:

public static <E> void append(List<E> list) {
    E elem = new E();  // compile-time error
    list.add(elem);
}

作为变通方案(workaround),你可以通过反射创建类型参数的对象:

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<>();

因为静态字段osphonepagerpc共享,os的实际类型是什么?它不能同时SmartphonePagerTabletPC。因此,你不能创建类型参数的静态字段

不能与参数化类型一起使用类型转换或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>。你能做的最多是使用无界通配符来验证列表是否为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

个人补充:但是下面这些代码就可以编译通过

ArrayList<int[]>[] arrayOfLists = new ArrayList[2];
for (int i = 0; i < arrayOfLists.length; i++) {
 arrayOfLists[i] = new ArrayList<>();
}
arrayOfLists[0].add(new int[]{0, 1});
System.out.println(Arrays.toString(arrayOfLists[0].get(0)));

以下代码说明了当不同类型的对象插入到数组中会发生什么:

Object[] strings = new String[2];
strings[0] = "hi";   // OK
strings[1] = 100;    // An ArrayStoreException is thrown.

如果你在泛型列表中尝试同样的事情,会有一样的问题:

Object[] stringLists = new List<String>[2];  // 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

个人补充:比如修改成下面这样就不会报错了,但这违背了泛型的设计理念——编译时强制类型检查

Object[] stringLists = new List[2]; 
stringLists[0] = new ArrayList<String>();
stringLists[1] = new ArrayList<Integer>();
不能创建、捕获或抛出参数化类型的对象

泛型类不能直接或间接扩展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
        // ...
    }
}
不能重载(入参类型Formal Parameter Types被类型擦除后都变为相同的原始类型的)方法

类不能有两个重载的方法,它们在类型擦除后将具有相同的签名

public class Example {
    public void print(Set<String> strSet) { }
    public void print(Set<Integer> intSet) { }
}

这些重载将在类文件表示中共享相同的签名,并将生成编译时错误

问题与练习:泛型

  1. 编写一个通用方法来计算集合中具有特定属性的元素数量。例如:奇数、素数、回文数

    答:

    public <T> int cal(T[] nums) {
     int count = 0;
     for(T t: nums) {
         if(...) {
             count++;
         }
     }
     return count;
    }
    

    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;
     }
    }
    
    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);
     }
    }
    
  2. 以下类是否会编译?如果不编译,为什么?

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

    答:不会编译,因为x跟y不一定可以通过>比较

    改:不会,大于号(>)只适用于基本数据类型

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

    public <T> void swap(T[] arr, int j, int k) {
        T tmp = arr[j];
        arr[j] = arr[k];
        arr[k] = tmp;
    }
    
  4. 如果编译器在编译时擦除了所有类型参数,为什么还要使用泛型?

泛型的设计之初就是为了在编译时确保类型正确,这样子在编译完成之后就可以使用而不会出错。

除此之外:泛型让开发者可以实现通用方法;泛型支持编程类型作为参数;

You should use generics because:

  • The Java compiler enforces tighter type checks on generic code at compile time.
  • Generics support programming types as parameters.
  • Generics enable you to implement generic algorithms.
  1. 以下类在类型擦除后会转换成什么样子?
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;
}
  1. 以下方法在类型擦除后会转换成什么样子?
public static <T extends Comparable<T>>
    int findFirstGreaterThan(T[] at, T elem) {
    // ...
}
public static <Comparable<Object>> int findFirstGreaterThan(Object[] at, Object elem) {
 // ...
}

改正

public static int findFirstGreaterThan(Comparable[] at, Comparable elem) {
 // ...
}
  1. 以下方法是否会编译?如果不编译,为什么?
public static void print(List<? extends Number> list) {
    for (Number n : list)
        System.out.print(n + " ");
    System.out.println();
}

可以编译

  1. 编写一个通用方法来查找列表中指定范围[begin, end)内的最大元素。

答:

public static  <T extends Comparable<T>> T max(T[] arr, int start, int end) {
 T ans = arr[start];
 for (int i = start + 1; i < end; i++) {
     if (arr[i].compareTo(ans) > 0) {
         ans = arr[i];
     }
 }
 return ans;
}

改正

import java.util.*;

// ? super T可以让父类实现了Comparable的子类也能进行比较,而上面只能比较实现了Comparable的类自身
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;
 }
}
  1. 以下类是否会编译?如果不编译,为什么?
public class Singleton<T> {

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

        return instance;
    }

    private static T instance = null;
}

答:不能编译,静态类型不能是泛型,因为多个动态对象不知道共享的该静态对象是什么类型

  1. 给定以下类
class Shape { /* ... */ }
class Circle extends Shape { /* ... */ }
class Rectangle extends Shape { /* ... */ }

class Node<T> { /* ... */ }

以下代码是否会编译?如果不编译,为什么?

Node<Circle> nc = new Node<>();
Node<Shape>  ns = nc;

答: 不能编译,Circle是Shape的子类,但是Node 不是Node的子类,所以无法向上转型

  1. 考虑这个类,
class Node<T> implements Comparable<T> {
    public int compareTo(T obj) { /* ... */ }
    // ...
}

以下代码是否会编译?如果不编译,为什么?

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

答:可以编译,Node是Comparable的子类(接口通过匿名内部类实例化也行)

  1. 如何调用以下方法来查找列表中第一个与一组指定整数互质的整数?
public static <T>
    int findFirst(List<T> list, int begin, int end, UnaryPredicate<T> p)

注意,如果两个整数a和b的最大公约数gcd(a, b)等于1,则称这两个整数a和b是互质的。

看答案

检查你的答案吧

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值