[关闭]
@yishuailuo 2016-08-13T13:50:38.000000Z 字数 32951 阅读 5526

Java 泛型教程

原文 : The Java Tutorials - Generic

Java 泛型 教程


泛型概述

在任何重要的软件项目中,bugs 的存在都是一个无法改变的事实。认真谨慎的计划,编程和测试固然能帮助减少它们四处蔓延,然后不知何故,它们总是会在某处找到一个方法注入你的代码中。在项目引入新特性并且代码库在规模和复杂度上都扩大时,这种情况显得尤为明显。

幸运的是,有些 bugs 比其他的更容易发现。例如编译时 bug (Compile-time bugs) 就能被更早地发现;你可以使用编译器的错误信息在编译时当场找出问题并且修复它。然后,运行时 bugs (Runtime bugs)就显得更为棘手;它们不会立即浮现,并且当它们能被找出时,它也可能并非导致问题的真正原因。

泛型通过在编译时检测更多的 bug 从而增加代码的稳定性。

为什么使用泛型

简单来说,泛型使得类型(类和接口)在定义类、接口和方法时可以作为参数。很像我们更为熟悉的在方法声明时使用的形式参数一样,类型参数(types parameters)提供一种方式:使用不同的输入去重用同样的代码。所不同的是,形式参数输入的是值,而类型参数输入的是类型。

使用泛型的代码比不使用泛型的会有更多的好处。

  1. // 下面这个没用泛型的代码片段,需要类型转换:
  2. List list = new ArrayList();
  3. list.add("hello");
  4. String s = (String) list.get(0); // cast by using (String)
  5. // 用泛型重写,代码将不需要类型转换:
  6. List<String> list = new ArrayList<String>();
  7. list.add("hello");
  8. String s = list.get(0); // no cast

泛型类型

泛型类型是被类型参数化的类或者接口。下面的 Box 类将会用来阐述这个概念。

一个简单的 Box 类

首先检查作用在任何类型上的对象的非泛型类,它只需要提供两个方法,set 方法:为 Box 添加对象 Object,和 get 方法: 获取 Object。

  1. public class Box{
  2. private Object object;
  3. public void set(Object object) { this.object = object; }
  4. public Object get() { return object; }
  5. }

既然 Box 类的方法接受和返回的都是 Object类型,只要不是基本数据类型,你可以任意传入你想传入的值。在编译期间,无法验证这个类是怎么用的。当程序里有一处代码把 Integer 类型的值放入 box 里,同时期望取出 Integer 类型的值,然而,另一处代码却错误地放入 String 类型,这将会导致运行时错误(runtime error)。

泛型版的 Box 类

泛型类的格式定义如下:

  1. class name<T1, T2, ..., Tn> {/*...*/}

类型参数部分跟在类名后,由尖括号(<>)隔开。它指定类型参数 T1, T2, ..., Tn。

为了使用泛型重写 Box 类,需要通过把 public class Box 更改为 public class Box< T > 来创建一个泛型类型声明,这里引入了参数类型 T, 它可以在类里面的任意地方被使用。

使用泛型后,Box 类变成:

  1. /**
  2. * Generic version of the Box class.
  3. * @param <T> the type of the value being boxed
  4. */
  5. public class Box<T> {
  6. // T stands for "Type"
  7. private T t;
  8. public void set(T t) { this.t = t; }
  9. public T get() { return t; }
  10. }

正如你所看到的,所有出现 Object 的地方都被替换成了 T。类型参数可以是你指定的任意的非基本数据类型:任何类类型,任何接口类型,任何数组类型,甚至是另一类型参数。

同样的方法可以被用来创建泛型接口。

类型参数的命名约定

按照约定,类型参数名是单个的大写字母。这和你所知的变量的命名约定形成鲜明的对比,并且有很好的理由这么命名:如果没有这个约定,将很难区分类型变量普通的类名或者接口名

最常用的类型参数命名为:

你将会看到这些名称被广泛使用在 Java SE API 上和这个教程的其余部分。

调用和实例化泛型类型

为了从代码内部引用泛型 Box 类,需要用某个具体类型,比如 Integer,替换 T 来执行一次泛型类型调用。

  1. Box<Integer> integerBox;

你可以认为一次泛型类型调用就像一次普通方法的调用一样,但是你是给 Box 类本身传入一个是类型参数(type argument ),这个例子里是Integer,而不是像方法那样,给它传入一个普通参数。

————————————————————————————————————————————————————————————————————
Type Parameter 和 Type Argument 术语:很多开发者互换着使用 “type Parameter” 和 “type Argument”,但是这两个术语并非同一个意思。当编写代码的时候,我们提供一个 type argument 去创建一个参数化的类型。所以,在 Foo < T > 里的 T 时一个 type parameter, 而在 Foo < String > 里的 String 则是一个 type argument。当这个课程使用到这些术语时,会遵循这个定义。
————————————————————————————————————————————————————————————————————

像任意其他变量声明一样,这段代码实际上并没有创建新的 Box 对象。它只是简单地声明了 integerBox 来保存一个引用,就像 Box < Integer > 所呈现的意思那样。

泛型类型的调用通常被称为参数化类型。

像通常所做的那样,使用 new 关键字去实例化一个类,但是将 < Integer > 置于类名和括号之间:

  1. Box<Integer> integerBox = new Box<Integer>();

菱形语法

在 Java SE 7 和更新的版本里,只要编译器可以从上下文中判断或者推断出,你就可以通过一个类型参数(type arguments)的空集(<>)替换类型参数去调用泛型类的构造函数。这一对尖括号非正式地称作菱形语法(The Diamond)。比如,你可以使用如下语句去创建 Box < Integer > 的实例:

  1. Box<Integer> integerBox = new Box<>();

更多关于菱形符号和类型推断的信息,请参考类型推断

多类型参数

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

  1. public interface Pair<K, V> {
  2. public K getKey();
  3. public V getValue();
  4. }
  5. public class OrderedPair<K, V> implements Pair<K, V> {
  6. private K key;
  7. private V value;
  8. public OrderedPair(K key, V value) {
  9. this.key = key;
  10. this.value = value;
  11. }
  12. public K getKey() { return key; }
  13. public V getValue() { return value; }
  14. }

下面的语句创建两个 OrderedPair 的实例:

  1. Pair<String, Integer> p1 = new OrderedPair<String, Integer>("Even", 8);
  2. 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, String> 的声明中推断出 K 和 V 类型,这些语句可以使用菱形语法简写成:

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

创建泛型接口时,跟创建泛型类一样遵循这些约定。

参数化类型

你同样可以用一个参数化类型(例如,List< String >)替换类型参数(例如 K 或者 V)。例如,以 OrderedPair为例:

  1. OrderedPair<String, Box<Integer>> p = new OrderedPaired<>("", new Box<Integer>(...));

原始类型

原始类型是没有任何类型参数的类或者接口。例如,给定泛型类 Box:

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

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

  1. Box<Integer> intBox = new Box<>();

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

  1. Box rawBox = new Box();

所以,Box 是泛型类 Box < T > 的原始类型。然而,一个非泛型类或者接口不是原始类型。

原始类型出现在遗留代码中,因为在 JDK 5.0 之前许多 API 类(比如集合类Collections)都不是泛型。当使用原始类型时,基本上你会获得前泛型时代的行为—— Box 会给你一个 Object。为了向前兼容,给一个原始类型赋一个参数化类型是允许的:

  1. Box<String> stringBox = new Box<>();
  2. Box rawBox = striingBox; // ok

但是如果给一个参数化类型赋一个原始类型,你会得到一个警告:

  1. Box rawBox = new Box(); // rawBox is a raw type of Box<T>
  2. Box<Integer> intBox = rawBox; // warning: unchecked conversion

如果使用一个原始类型去调用相应泛型类型定义的泛型方法,你同样会得到一个警告:

  1. Box<Striing> stringBox = new Box();
  2. Box rawBox = stringBox;
  3. rawBox.set(8); // warning: unchecked invocation to set(T)

这个警告表明原始类型绕开了泛型类型检查,延迟非安全代码的捕获到运行时。所以,应该避免使用原始类型。

关于 Java 编译器如何使用原始类型,类型擦除章节会有更多信息。

未经检查的错误信息

如前所述,当混淆遗留代码和泛型代码时,你可能会遇到像下面那样的警告信息:

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

当在原始类型上使用较老的 API 时,警告就会出现,就像下面的例子所呈现的那样:

  1. public class WarningDemo {
  2. public static void main(String[] args){
  3. Box<Integer> bi;
  4. bi = createBox();
  5. }
  6. static Box createBox(){
  7. return new Box();
  8. }
  9. }

“未经检查” 这个术语意味着编译器没有足够的类型信息去执行所有必需的类型检查
去擦除类型安全。默认情况下,即便编译器给出一个提示,“未经检查”的警告仍是关闭的。要查看所有“未经检查”的警告,使用 --Xlint:unchecked 重新编译。

使用 -Xlint:unchecked 重新编译之前的例子揭示了以下附加信息:

  1. WarningDemo.java:4: warning: [unchecked] unchecked conversion
  2. found : Box
  3. required: Box<java.lang.Integer>
  4. bi = createBox();
  5. ^
  6. 1 warning

要完全关闭未经检查的警告,使用 -Xlint:-unchecked 标志。@SuppressWarnings("unchecked")注解禁止未经检查的异常。如果你对@SuppressWarnings不熟悉,请参考Annotation

泛型方法

泛型方式是引入了它们自己的类型参数的方法。这类似于声明泛型类,不过参数类型的范围仅限于在声明的方法里。静态的和非静态的方法,以及泛型类的构造方法都是允许的。

创建泛型方法的语法包括一个在方法返回类型前,且在尖括号里的类型参数。对于静态泛型方法,类型参数部分必须出现在方法的返回类型前。

Util 类包含一个泛型方法 compare,它比较两个 Pair 对象:

  1. public class Util {
  2. public static <T> boolean compare(Pair<K, V> p1, Pair<K, V> p2) {
  3. return p1.getKey.equals(p2.getKey) && p1.getValue.equals(p2.getValue);
  4. }
  5. }
  6. public class Pair<K, V> {
  7. private K key;
  8. private V value;
  9. public Pair(K key, V value) {
  10. this.key = key;
  11. this.value = value;
  12. }
  13. public void setKey(K key) { this.key = key; }
  14. public void setValue(V value) { this.value = value; }
  15. public K getKey() { return key; }
  16. public V getValue() { return value; }
  17. }

完成的调用这个方法的语法是:

  1. Pair<Integer, String> p1 = new Pair<>(1, "apple");
  2. Pair<Integer, String> p2 = new Pair<>(2, "pear");
  3. boolean same = Util.<Integer, String>compare(p1, p2);

如粗体字(译者注:指的是< Integer, String> 那里)显示的那样,类型已经被显式地提供。一般来说,这个可以被删除掉,编译器会推断出所需的类型:

  1. Pair<Integer, String> p1 = new Pair<>(1, "apple");
  2. Pair<Integer, String> p2 = new Pair<>(2, "pear");
  3. boolean same = Util.compare(p1, p2);

这个被称作类型推断的特性,允许你像调用一个普通方法一样去调用泛型方法,而无需再尖括号里指明类型。这个话题会在接下来的章节类型推断里做进一步的讨论。

有界类型参数

有些时候你想把可以用于作为类型参数(type arguments)的类型限制为一个参数化类型。例如,一个操作数字的方法可能只想接受 Number 类或者它的子类的实例。这就是有界参数类型。

要声明一个有界类型参数,需要列出类型参数名,后面跟着 extends 关键字,再紧跟着它的上界,这个例子里上界是 Number 类。注意,在这种场景下,extends 关键字是类关系中的 “extends” 的意思,或者是接口关系中的 “implements” 的意思。

  1. public class Box<T> {
  2. private T t;
  3. public void set(T t) {
  4. this.t = t;
  5. }
  6. public T get() {
  7. return t;
  8. }
  9. public <U extends Number> void inspect(U u) {
  10. System.out.println("T: " + t.getClass().getName());
  11. System.out.println("U: " + u.getClass().getName());
  12. }
  13. public static void main(String[] args){
  14. Box<Integer> integerBox = new Box<Integer>();
  15. integerBox.set(new Integer(10));
  16. integerBox.inspect("some text"); // error: this is still String!
  17. }
  18. }

通过修改泛型方法去包含这个有界参数类型,编译将立即失败,因为 inspect 方法的调用包含了一个 String 类型的值:

  1. Box.java:21: <U>inspect(U) in Box<java.lang.Integer> cannot
  2. be applied to (java.lang.String)
  3. integerBox.inspect("some text");
  4. ^
  5. 1 error

除了限定实例化泛型类型的类型,有界类型参数允许你调用定义在界上的方法:

  1. public class NaturalNumber<T extends Integer> {
  2. private T n;
  3. public NaturalNumber(T n) { this.n = n; }
  4. public boolean isEven() {
  5. return n.intValue() % 2 == 0;
  6. }
  7. // ...
  8. }

isEven 方法通过 n 调用了定义在 Integer 上界的 iniValue 方法。

多界

前面的例子展示了单个边界的类型参数的使用,然而一个参数类型可以有多个边界:

  1. Class A { /*...*/ }
  2. interface B { /*...*/ }
  3. interface C { /*...*/ }
  4. class D <T extends A & B & C> { /*...*/ }

如果边界 A 不是第一个被指定的,将会得到一个编译时错误:

  1. class D <T extends B & A & C> { /* ... */ } // compile-time error

泛型方法与有界类型参数

有界类型参数是实现泛型算法的关键。考虑以下计算数组 T[] 中大于给定的元素 elem 的元素个数的方法:

  1. public static<T> int countGreaterThan(T[] anArray, T elem) {
  2. int count = 0;
  3. for (T e : anArray) {
  4. if (e > elem)
  5. count++; // complier error
  6. return count;
  7. }
  8. }

这个方法的实现很简单,但是因为大于操作符(>)只能用在类似 short,int,double,long,float,byte和 char 这样的基本类型上,所以不能通过编译。你不能用 > 操作符比较对象。要解决这个问题,需要用到绑定了 Comparable < T >接口的参数类型:

  1. public interface Comparable<T> {
  2. public int compareTo(T o);
  3. }

最终产生的代码为:

  1. public static <T extends Comparable<T>> int countGreaterThan(T[] anArray, T elem){
  2. int count = 0;
  3. for (T e : anArray) {
  4. if (e.compareTo(elem) > 0)
  5. count++;
  6. return count;
  7. }
  8. }

泛型,继承与子类型

如你所知,如果两个类型互相兼容的话,将一个类型的对象赋值给另一个类型是很有可能的。例如,因为 Object 类型是 Integer 类型的一个父类,所以可以将 Integer 类型的值赋值给 Object 类型:

  1. Object someObject = new Object();
  2. Integer someInteger = new Integer(10);
  3. someObject = somInteger; // OK

在面向对象的术语里,这个叫 “is a” 的关系。因为 Integer 类型 is a Object,所以这种赋值是允许的。但是 Integer 也是 Number 的一种,所以如下代码也是有效的:

  1. public void someMethod(Number n) { /* ... */ }
  2. someMethod(new Integer(10)); // OK
  3. someMethod(new Double(10.1)); // OK

泛型也是如此。在执行一次泛型类型调用时,你可以传入一个 Number 类型作为它的参数类型,而且在随后的 add 方法调用中,如果实参与 Number 兼容的话,调用也是允许的:

  1. Box<Number> box = new Box<Number>();
  2. box.add(new Integer(10)); // OK
  3. box.add(new Double(10.1)); // OK

现在,考虑下面的方法:

  1. public void boxTest(Box<Number> n) { /*...*/ }

它接受什么类型的实参呢? 通过观察方法的签名,你可以看到它接受一个类型是 Box < Number > 的实参。但是这什么意思呢?就像你可能期待的那样,传入一个 Box < Integer > 类型的值 或者一个 Box < Double > 类型的值是允许的么?答案是“不”。因为 Box < Double > 都 Box < Double > 都不是 Box < Number > 的子类型。

当提到泛型编程的时候,这是一个很常见的误解,但这是一个很重要的概念。

Generics_Inheritance_and_Subtypes01

虽然 Integer 是 Number 的子类型,但是 Box < Integer > 并非 Box < Number > 的子类型

——————————————————————————————————————————————————————————————————————————————————
注:给定两个具体类型 A 和 B (例如,Number 和 Integer),不管 A 跟 B 是否相关,MyClass < A > 跟 MyClass < B > 都毫无关系。MyClass < A > 和 MyClass < B > 的共同父类是 Object。

关于在参数类型相关的情况下,如何在两个泛型类中创建一个类似子类型的关系的信息,请参考通配符与子类化
——————————————————————————————————————————————————————————————————————————————————

泛型类与子类化

你可以通过继承类或者实现接口的方式子类化一个类或者接口。类或接口的类型参数与另一个类或者接口的类型参数的关系通过 extends 或者 implements 子句来确定的。

用 Collecttions 类做例子,ArrayList < E > 实现 List < E >,List < E > 集成 Collections < E >。所以 ArrayList < String > 是 List < String > 的子类,而 List < String > 是 Collections < String > 的子类。只要不改变参数类型,这个子类化的关系会在这些类型中保持着。

Generics_Inheritance_and_Subtypes02

集合样例的继承层次结构

现在想象下我们想定义一个自己的 List 接口 PayloadList,它把泛型类型 P 的一个可选值和里面每一个元素联系在一起。这个声明可能类似这样:

  1. interface PayloadList<E, P> extends List<E> {
  2. void setPayload(int index, P val);
  3. ...
  4. }

下面 PayloadList 的参数化类型是 List < String > 的子类:

Generics_Inheritance_and_Subtypes03

PayloadList 样例的继承层次结构

类型推断

类型推断是指 Java 编译器能够通过查看每个方法调用以及相关的声明来确定类型参数,从而让方法调用适用。推断算法确定参数的类型,并且如果可信的话,也确定最后赋值的类型或者返回的类型。最后,推断算法尝试找到能匹配所有参数的最具体的类型。

为了解释最后一点,下面这个例子里,类型推断确定传给 pick 方法的第二个参数是 Serializable 类型:

  1. static <T>T pick(T a1, T a2) { return a2; }
  2. Serializable s = pick("d", new ArrayList<String>());

译者注:String 类型(“d”) 和 ArrayList < String > 类型最小的共同父类(即最具体的类型)是 Serializable 类型。

类型推断与泛型方法

泛型方法引入了类型推断,这使得你可以像调用普通方法一样调用泛型方法,而不需要在尖括号里指定一个类型。考虑以下例子,需要 Box 类的 BoxDemo:

  1. public class BoxDemo {
  2. public static <U> void addBox(U u,
  3. java.util.List<Box<U>> boxes) {
  4. Box<U> box = new Box<>();
  5. box.set(u);
  6. boxes.add(box);
  7. }
  8. public static <U> void outputBoxes(java.util.List<Box<U>> boxes) {
  9. int counter = 0;
  10. for (Box<U> box: boxes) {
  11. U boxContents = box.get();
  12. System.out.println("Box #" + counter + " contains [" +
  13. boxContents.toString() + "]");
  14. counter++;
  15. }
  16. }
  17. public static void main(String[] args) {
  18. java.util.ArrayList<Box<Integer>> listOfIntegerBoxes =
  19. new java.util.ArrayList<>();
  20. BoxDemo.<Integer>addBox(Integer.valueOf(10), listOfIntegerBoxes);
  21. BoxDemo.addBox(Integer.valueOf(20), listOfIntegerBoxes);
  22. BoxDemo.addBox(Integer.valueOf(30), listOfIntegerBoxes);
  23. BoxDemo.outputBoxes(listOfIntegerBoxes);
  24. }
  25. }

以下是这个例子的输出:

  1. Box #0 contains [10]
  2. Box #1 contains [20]
  3. Box #2 contains [30]

泛型方法 addBox 定义了一个名为 U 的类型参数。一般来说,Java 编译器可以推断出泛型方法调用的类型参数。因此,大多数情况下,你不需要指定类型参数。例如,要调用泛型方法 addBox,你可以像下面这样,利用类型见证(type witness)指定类型参数:

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

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

  1. BoxDemo.addBox(Integer.valueOf(20), listOfIntegerBoxes);

类型推断与泛型类的实例化

只要编译器能够从上下文中推断出类型参数,你可以用类型参数的空集(<>)去替换调用泛型类构造方法所需的类型参数。这一对尖括号非正式地称作菱形语法(The Diamond)。例如,考虑以下变量声明:

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

你可以用类型参数的空集(<>)去替换构造函数的参数化类型:

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

注意,要在泛型实例化时利用类型推断的优势,你必须使用菱形语法。在以下例子中,编译器会发出一个未经检查的警告,因为 HashMap() 构造函数指向 HashMap 原始类型,而不是 Map < String, List< String > > 类型:

  1. Map<String, List<String>> myMap = new HashMap(); // unchecked conversion warning

类型推断与泛型类和非泛型类的泛型构造函数

注意,可以在泛型类和非泛型类中泛型化构造方法(也就是说,构造方法可以声明自己的形式类型参数)。考虑以下例子:

  1. class MyClass<X> {
  2. <T> MyClass (T t) {
  3. // ...
  4. }
  5. }

考虑以下 MyClass 类的实例:

  1. new MyClass<Integer>("")

这个语句创建了一个参数化类型 MyClass< Integer > 的实例;这个语句显式地为泛型类 MyClass< X > 的形式类型参数 X 指定了类型 Integer。注意,这个泛型类的构造函数包含了一个形式类型参数 T。编译器为泛型类的构造函数的形式类型参数推断出 String 类型(因为构造函数实际的参数是 String 对象)。

Java SE 7 之前版本的编译器能够像泛型方法一样,推断出泛型构造函数的参数类型。然而,如果你使用菱形语法(<>),在实例化时,Java SE 7 或者更新版本的编译器能够推断出泛型类的实际类型参数。考虑以下例子:

  1. MyClass<Integer> myObject = new MyClass<>("");

在这个例子里,编译器为泛型类 MyClass < X > 的形式类型参数 X 推断出 Integer 类型。同时,它为泛型类的构造函数的形式类型参数 T 推断出 String 类型。

——————————————————————————————————————————————————————————————————————————————————————————————
注意:需要重点强调的一点是,推断算法只使用调用参数,目标类型,还可能是一个可明显预期的返回类型去推断类型。推断算法不会再之后的程序里去使用这个推断结果。
——————————————————————————————————————————————————————————————————————————————————————————————

目标类型

Java 编译器利用目标类型化的优势去推断泛型方法调用的类型参数。表达式的目标类型是Java编译器在表达式所在之处所期望的数据类型。考虑以下声明的 Collections.emptyList 方法:

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

考虑以下赋值语句:

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

这个语句期望一个 List < String > 的实例;这个数据类型就是目标类型。因为 emptyList 方法返回 List < T > 类型的值,所以编译器推断出类型参数 T 必须是 String 类型的值。这个在 Java SE 7 和 8 中都是凑效的。或者,你可以像下面这样使用类型见证去指定 T 的值:

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

然而,在这中场景下是不必要的。但是,在别的场景下却是需要的。考虑以下方法:

  1. void processStringList(List< String > stringList) {
  2. // process stringList
  3. }

假设你想用一个空 list 去调用 processStringList 方法。在 Java SE 7 中,下面的语句是不能通过编译的:

  1. processStringList(Collections.emptyList());

编译器会产生一个类似下面的错误信息:

  1. List<Object> cannot be converted to List<String>

编译器需要一个类型参数 T 的具体值,所以它会以 Object 作为具体值开始。因此,Collections.emptyLisyt 方法的调用返回一个 List < Object > 类型的值,它和方法 processStringList 的入参是不兼容的。所以,在 Java SE 7 里,你必须像下面这样为类型参数的值指定一个值:

  1. processStringList(Collections.<String>emptyList());

这个在 Java SE 8 中已经不是必须的了。目标类型的概念扩展到包含方法参数,比如 processStringList 方法的参数。在这种情况下,processStringList 方法需要一个 List < String > 类型的参数。Collections.emptyLisyt 方法返回一个 List < T > 类型的值,所以,使用 List < String > 目标类型时,编译器推断出类型参数 T 有一个 String 类型的值。所以在 Java SE 8 中,下面的语句可以通过编译:

  1. processStringList(Collections.emptyList());

更多信息请参考Lambda 表达式 中的目标类型化

通配符

在泛型代码中,问号(?)被称作为通配符,表示未知类型。通配符可以用于多种场景:作为一个形式参数,属性,或者本地变量的类型;有时候作为一个返回类型(然而最好在编程更为明确地指定)。通配符不会用来作为泛型方法调用、泛型类实例创建的类型参数,或者一个父类型。

上界通配符

你可以使用上界通配符放宽在一个变量的限制。例如,当你想写一个在 List < Integer >、List < Double > 和 List < Number > 上操作的方法;你可以使用上界通配符达到这个目的。

要声明一个上界通配符,需要使用通配符字符(“?”),后面跟着 extends 关键字,接着后面再跟着它的上界。注意,在这种场景下,extends 关键字是类关系中的 “extends” 的意思,或者是接口关系中的 “implements” 的意思。

要写一个在 Number 和它的子类(比如 Integer,Double,Float)的列表上操作的方法,你需要指定 List 。List < Number > 子句比 List 更为严格,因为前者只匹配 Number 类型的列表,而后者匹配 Number类型或者它的任一子类的列表。

考虑以下 process 方法:

  1. public static void processList(List<? extends Foo> list) { /*...*/ }

上界通配符 ,其中 Foo 是某个类型,匹配 Foo 类以及 Foo 的任一子类。process 方法可以把列表元素当作 Foo 类型来访问:

  1. public static void process(List<? extends Foo> list) {
  2. for (Foo elem : list) {
  3. // ...
  4. }
  5. }

在 foreach 子句中,变量 elem 在列表的每一个元素上遍历。Foo 类中定义的任何方法,都可以在 elem 上使用。

sumOfList 方法返回列表元素的总和:

  1. public static double sumOfList(List<? extends Number> list) {
  2. double s = 0.0;
  3. for (Number n : list)
  4. s += n.doubleValue();
  5. return s;
  6. }

以下代码使用 Integer 对象列表,打印出 sum = 6.0:

  1. List<Integer> li = Array.asList(1, 2, 3);
  2. System.out.println("sum = " + sumOfList(li));

Double 类型值的列表也可以使用 sumOfList 方法。下面的代码打印出 sum = 7.0:

  1. List<Double> ld = Array.asList(1.2, 2.3, 3.5);
  2. System.out.println("sum = " + sumOfList(ld));

无界通配符

无界通配符类型是使用通配符符号(“?”)指定,例如,List < ? >。这个叫未知类型的列表。有两个场景,使用无界通配符是非常有用的方法:

考虑以下 printList 方法:

  1. public static void printList(List<Object> list) {
  2. for (Object elem : list)
  3. System.out.println(elem + " ");
  4. System.out.println();
  5. }

printList 方法的目标是打印任意类型的列表,但是它不能做到这一点 —— 它只能打印 Object 实例的列表;它不能打印 List < Integer >,List < String >,List < Double > 等等,因为他们都不是 List < Object > 的子类型。使用 List < ? > 写一个泛型的 printList 方法:

  1. public static void printList(List<?> list) {
  2. for (Object elem : list)
  3. System.out.println(elem + " ");
  4. System.out.println();
  5. }

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

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

——————————————————————————————————————————————————————————————————————————————————————————————
注意:实例中的ArrayList方法贯穿整个课程。这个静态工厂方法转换指定的数组并且返回一个固定大小的列表。
——————————————————————————————————————————————————————————————————————————————————————————————

需要重点强调的一点是 List < Object > 和 List < ? > 不是一样的。你可以往 List < Object > 里面插入一个 Object 类型的值,或者 Object 的子类型的值。但是你只能往 List < ? > 里面插入 null。 通配符使用指南这个章节会有更多关于如何在给定的场景下确定通配符类型的信息。

下界通配符

上界通配符章节已经介绍了由extends关键字表示的上界通配符限定了未知类型为指定类型或者该类型的子类。同样的,下界通配符限定了未知类型为指定类型或者该类型的父类

下界通配符使用通配符字符(“?”),后面跟着 super 关键字,再紧跟着它的下界来表示:。

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

你想写一个方法,把 Integer 类型的对象放到列表里。为了使得它最为灵活,你可能倾向于让这个方法可以在 List < Integer >、List < Number >、和 List < Object > —— 任一个可以装下 Integer 类型值的列表里。

要写这个在 Integer 类型或者 Integer 的父类型(比如 Integer,Number 和 Object)上操作的方法,你会指定 List 来实现。List < Integer > 子句比 List 更为严格,因为前者只能匹配 Integer 类型的列表,然而后者却可以匹配任一个 Integer 类型的父类型的列表。

以下代码添加数字 1 到 10 到列表的尾部:

  1. public static void addNumbers(List<? super Integer> list) {
  2. for (int i = 1; i <= 10; i++) {
  3. list.add(i);
  4. }
  5. }

通配符使用指南章节会提供关于何时使用上界通配符何时使用下界通配符的指南。

通配符与子类化

就像在泛型,继承与子类型章节描述的那样,泛型类之间或者泛型接口之间毫无关系,只是它们的类型之间存在着一种关系。然而,你可以使用通配符在泛型类之间或者泛型接口之间创造一种关系。

给定以下两个普通的(非泛型)类:

  1. class A { /*...*/ }
  2. class B extends A { /*...*/ }

写下以下代码是合理的:

  1. B b = new B();
  2. A a = b;

这个例子展示了普通类的继承遵循这条规则:如果类 B 继承自类 A,那么 B 是 A 的子类型。这条规则不适用于泛型:

  1. List<B> lb = new ArrayList<>();
  2. List<A> la = lb; // complie-time error

给定 Number 的子类型 Integer 类型,那么 List < Integer > 和 List < Number > 之间是什么关系呢?

WildCardAndSubtyping01

共同父类 List < ? >

即使 Integer 类型是 Number 类型的子类,但是 List < Integer > 不是 List < Number > 类型的子类,事实上,这两个类不相关。List < Integer > 与 List < Number > 的共同父类是 List < ? >。

为了在这些类之间创造一个关系使得代码可以通过 List < Integer > 的元素访问 Number 类的方法,我们使用上届通配符:

  1. List<? extends Integer> intList = new ArrayList<>();
  2. List<? entends Number> numList = intList; // OK. List<? extends Integer> 是 List<? extends Number> 的子类型。

因为 Integer 是 Number 类型的子类,numList 是 Number 对象的列表,那么现在 intList (一个 Integer 对象的列表)与 numList 之间就存在着一种关系。下图将展示若干个使用上届或者下界通配符声明的 List 类之间的关系。

WildCardAndSubtyping02

若干泛型列表类声明的继承层次结构

通配符使用指南章节用更多关于使用上界和下界通配符的影响。

通配符匹配与帮助方法

在有些情况下,编译器会推断通配符的类型。例如,一个可能被定义为 List < ? > 的列表,然而当评估一个表达式时,编译器从代码中推断出一个特定的类型。这个场景被称作通配符匹配

大多数情况下,你无需关心通配符匹配,除非当你看到包含 “capture of” 短语的错误信息。

一个编译后产生匹配错误的通配符错误的例子:

  1. import java.util.List;
  2. public class WildcardError {
  3. void foo(List<?> i) {
  4. i.set(0, i.get(0));
  5. }
  6. }

在这个例子中,编译器把输入参数 i 当作 Object 类型处理。当 foo 方法调用 List.set(int, E) 方法时,编译器无法确认已被插入列表的对象的类型,那么错误就产生了。当这种类型的错误发生时,它通常意味着编译器相信你正在给一个变量赋一个错误的类型。泛型被加到 Java 语言里就是因为这个原因 —— 在编译时刻强制类型安全。

当使用 Oracle 的 JDK 7 的javac 实现来编译这个通配符错误的示例后,将
产生以下错误:

  1. WildcardError.java:6: error: method set in interface List<E> cannot be applied to given types;
  2. i.set(0, i.get(0));
  3. ^
  4. required: int,CAP#1
  5. found: int,Object
  6. reason: actual argument Object cannot be converted to CAP#1 by method invocation conversion
  7. where E is a type-variable:
  8. E extends Object declared in interface List
  9. where CAP#1 is a fresh type-variable:
  10. CAP#1 extends Object from capture of ?
  11. 1 error

在这个例子中,代码尝试去执行一个安全的操作,你怎样去解决这个编译错误呢,你可以通过写一个私有的帮助方法(private helper method)来匹配通配符。在这种情况下,你可以像 WildcardFixed 类所展示的那样,通过创建一个私有的帮助方法来解决这个问题:

  1. public class WildcardFixed {
  2. void foo(List<?> i) {
  3. fooHelper(i);
  4. }
  5. // Helper method created so that the wildcard can be captured
  6. // through type inference.
  7. private <T> void fooHelper(List<T> l) {
  8. l.set(0, l.get(0));
  9. }
  10. }

由于有了帮助方法,编译器使用类型推断将 T 确定为这次调用中的匹配变量 CAP#1。现在这个例子可以成功地通过编译。

按照约定,帮助方法一般命名为 originalMethodNameHelper。

现在考虑一个更加复杂的例子,WildcardErrorBad类:

  1. import java.util.List;
  2. public class WildcardErrorBad {
  3. void swapFirst(List<? extends Number> l1, List<? extends Number> l2) {
  4. Number temp = l1.get(0);
  5. l1.set(0, l2.get(0)); // expected a CAP#1 extends Number,
  6. // got a CAP#2 extends Number;
  7. // same bound, but different types
  8. l2.set(0, temp); // expected a CAP#1 extends Number,
  9. // got a Number
  10. }
  11. }

在这个例子中,代码尝试执行一个不安全的操作。例如,考虑如下 swapFirst 方法的执行:

  1. List<Integer> li = Arrays.asList(1, 2, 3);
  2. List<Double> ld = Arrays.asList(10.10, 20.20, 30.30);
  3. swapFirst(li, ld);

当 List < Integer > 和 List < Double > 两个都满足 List < ? extends Number > 的条件,但是试图将一个 Integer 列表中的项放在 Double 列表中显然是不正确的。

当使用 Oracle 的 JDK 的javac 实现来编译这段代码时,编译器将产生以下错误:

  1. WildcardErrorBad.java:7: error: method set in interface List<E> cannot be applied to given types;
  2. l1.set(0, l2.get(0)); // expected a CAP#1 extends Number,
  3. ^
  4. required: int,CAP#1
  5. found: int,Number
  6. reason: actual argument Number cannot be converted to CAP#1 by method invocation conversion
  7. where E is a type-variable:
  8. E extends Object declared in interface List
  9. where CAP#1 is a fresh type-variable:
  10. CAP#1 extends Number from capture of ? extends Number
  11. WildcardErrorBad.java:10: error: method set in interface List<E> cannot be applied to given types;
  12. l2.set(0, temp); // expected a CAP#1 extends Number,
  13. ^
  14. required: int,CAP#1
  15. found: int,Number
  16. reason: actual argument Number cannot be converted to CAP#1 by method invocation conversion
  17. where E is a type-variable:
  18. E extends Object declared in interface List
  19. where CAP#1 is a fresh type-variable:
  20. CAP#1 extends Number from capture of ? extends Number
  21. WildcardErrorBad.java:15: error: method set in interface List<E> cannot be applied to given types;
  22. i.set(0, i.get(0));
  23. ^
  24. required: int,CAP#1
  25. found: int,Object
  26. reason: actual argument Object cannot be converted to CAP#1 by method invocation conversion
  27. where E is a type-variable:
  28. E extends Object declared in interface List
  29. where CAP#1 is a fresh type-variable:
  30. CAP#1 extends Object from capture of ?
  31. 3 errors

由于这段代码从根本上就是错误的,没有一个 helper 方法用来可以解决这个错误。

通配符使用指南

当使用泛型编程时,确定什么时候使用上界通配符,什么时候使用下界通配符是更使人困惑的一件事。本页将在你设计代码是,提供一些需要遵循的指南。

这个讨论的目的,有助于把变量看作是提供以下两个功能之一:
“In” 变量
“In” 变量给代码提供数据。想象以下有两个参数的复制方法:copy(src, dest)。其中,src 参数提供被复制的数据,所以它被称作 "in" 参数。
“Out” 变量
“Out” 变量持有被在任意地方使用的数据。在复制方法 copy(src, dest) 的样例中,dest 参数接受数据,所以它被称作“out”参数。

当然,有些变量既被当做 “In” 变量来使用,又被当做 “out” 变量来使用,这种场景也在这个指南中有所说明。

当需要确定是否使用通配符和使用什么类型通配符比较合适,你可以使用 “In” 和 “Out” 的原则。以下清单提供了所尊徐的指南:
——————————————————————————————————————————————————————————————————————————————————————————————
通配符指南:
- “In” 变量使用有 extends 关键字的上界通配符来定义。
- “Out” 变量使用有 super 关键字的下界通配符来定义。
- 如果 “In” 变量可以通过 Object 类里的方法来访问,使用无界通配符
- 如果代码既需要访问 “In” 变量,有需要访问 “Out” 变量,则不适用通配符
——————————————————————————————————————————————————————————————————————————————————————————————

这些指南不适用于方法的返回值。应该避免使用通配符作为返回值,因为它强制编程人员编写代码处理通配符。

由 List < ? extends ... > 定义的列表可以非正式地认为是只读的,但是这不是一个严格的保证。假设你有一下两个类:

  1. class NaturalNumber {
  2. private int i;
  3. public NaturalNumber(int i) { this.i = i; }
  4. // ...
  5. }
  6. class EvenNumber extends NaturalNumber {
  7. public EvenNumber(int i) { super(i); }
  8. // ...
  9. }

考虑以下代码:

  1. List<EvenNumber> le = new ArrayList<>();
  2. List<? extends NaturalNumber> ln = le;
  3. ln.add(new NaturalNumber(35)); // compile-time error

因为 List < EvenNumber > 是 List < ? extends NaturalNumber > 的子类,所以你可以将 le 赋值给 ln。但是,你不可以用 ln 把一个自然数添加到一个偶数的列表中。列表上的一下操作时可以的:

你可以看到由 List < ? extends NaturalNumber > 定义的列表在严格意思上不是只读的,但是你可以认为它是,因为你不能在列表上存储一个新的元素或者改变一个已存在的元素。

类型擦除

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

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

泛型类型的擦除

在类型擦除处理过程中,Java 编译器擦除所有的类型,并且如果类型参数是有界的话,使用它的第一个界值来替换类型参数,如果类型参数是无界的话,使用 Object 替换。

考虑以下代表单链表节点的泛型类:

  1. public class Node<T> {
  2. private T data;
  3. private Node<T> next;
  4. public Node(T data, Node<T> next) }
  5. this.data = data;
  6. this.next = next;
  7. }
  8. public T getData() { return data; }
  9. // ...
  10. }

因为类型参数 T 是无界的,Java 编译器使用 Object 替换类型参数 T :

  1. public class Node {
  2. private Object data;
  3. private Node next;
  4. public Node(Object data, Node next) {
  5. this.data = data;
  6. this.next = next;
  7. }
  8. public Object getData() { return data; }
  9. // ...
  10. }

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

  1. public class Node<T extends Comparable<T>> {
  2. private T data;
  3. private Node<T> next;
  4. public Node(T data, Node<T> next) {
  5. this.data = data;
  6. this.next = next;
  7. }
  8. public T getData() { return data; }
  9. // ...
  10. }

Java 编译器将用第一个绑定类 Comparable 来替换有界类型参数:

  1. public class Node {
  2. private Comparable data;
  3. private Node next;
  4. public Node(Comparable data, Node next) {
  5. this.data = data;
  6. this.next = next;
  7. }
  8. public Comparable getData() { return data; }
  9. // ...
  10. }

泛型方法的擦除

Java 编译器同样擦除泛型方法参数中的类型参数。考虑以下泛型方法:

  1. // Counts the number of occurrences of elem in anArray.
  2. //
  3. public static <T> int count(T[] anArray, T elem) {
  4. int cnt = 0;
  5. for (T e : anArray)
  6. if (e.equals(elem))
  7. ++cnt;
  8. return cnt;
  9. }

因为 T 是无界的,所以 Java 编译器使用 Object 来替换它:

  1. public static int count(Object[] anArray, Object elem) {
  2. int cnt = 0;
  3. for (Object e : anArray)
  4. if (e.equals(elem))
  5. ++cnt;
  6. return cnt;
  7. }

假设定义以下类:

  1. class Shape { /* ... */ }
  2. class Circle extends Shape { /* ... */ }
  3. class Rectangle extends Shape { /* ... */ }

你可以使用泛型方法画不同的形状:

  1. public static <T extends Shape> void draw(T shape) { /* ... */ }

Java 编译器使用 Shape 替换参数 T:

  1. public static void draw(Shape shape) { /* ... */ }

类型擦除的影响与桥方法

有时候类型擦除会导致非预期的情况。以下样例展示了这个是怎么发生的。样例(在桥方法中描述)展示了编译器有时候会产生一个合成的方法,叫桥方法(bridge method),作为类型擦除过程的一部分。

给定以下两个类:

  1. public class Node<T> {
  2. public T data;
  3. public Node(T data) { this.data = data; }
  4. public void setData(T data) {
  5. System.out.println("Node.setData");
  6. this.data = data;
  7. }
  8. }
  9. public class MyNode extends Node<Integer> {
  10. public MyNode(Integer data) { super(data); }
  11. public void setData(Integer data) {
  12. System.out.println("MyNode.setData");
  13. super.setData(data);
  14. }
  15. }

考虑以下代码:

  1. MyNode mn = new MyNode(5);
  2. Node n = mn; // A raw type - compiler throws an unchecked warning
  3. n.setData("Hello");
  4. Integer x = mn.data; // Causes a ClassCastException to be thrown.

在类型擦除之后,代码变为了:

  1. MyNode mn = new MyNode(5);
  2. Node n = (MyNode)mn; // A raw type - compiler throws an unchecked warning
  3. n.setData("Hello");
  4. Integer x = (String)mn.data; // Causes a ClassCastException to be thrown.

在代码执行的时候,会发生以下情况:

桥方法

当编译一个继承于参数化类的类,或者一个实现参数化接口的接口的时候,编译器可能需要创建一个叫桥方法的合成方法,作为类型擦除过程的一部分。

类型擦除后,Node 和 MyNode 类变成了:

  1. public class Node {
  2. public Object data;
  3. public Node(Object data) { this.data = data; }
  4. public void setData(Object data) {
  5. System.out.println("Node.setData");
  6. this.data = data;
  7. }
  8. }
  9. public class MyNode extends Node {
  10. public MyNode(Integer data) { super(data); }
  11. public void setData(Integer data) {
  12. System.out.println("MyNode.setData");
  13. super.setData(data);
  14. }
  15. }

类型擦除后,方法的签名不再匹配。Node 类的方法变成 setData(Object),而 MyNode 类的方法变成 setData(Integer)。所以,MyNode 类的 setData 方法不是重写的 Node 类的 setData 方法。

为了解决这个问题,同时保留类型擦除后泛型类型的多态性,Java 编译器产生一个桥方法以保证子类型化如预期那样生效。对于 MyNode 类,编译器为 setData 方法产生一个桥方法:

  1. class MyNode extends Node {
  2. // Bridge method generated by the compiler
  3. //
  4. public void setData(Object data) {
  5. setData((Integer) data);
  6. }
  7. public void setData(Integer data) {
  8. System.out.println("MyNode.setData");
  9. super.setData(data);
  10. }
  11. // ...
  12. }

如你所见,类型擦除后,桥方法拥有与 Node 类中 setData 方法同样的方法签名,它代理原始的 setData 方法。

不可具体化类型

类型擦除章节讨论了编译器移除形式类型参数(type parameters)和实际类型参数(type arguments)相关信息的过程。对于形式参数有不可具体化类型的可变参数方法,类型擦除有着很大的影响。更多关于可变参数方法的信息参考方法或者构造方法传参信息任意数量参数章节。

本页包含以下主题:

不可具体化类型

可具体化类型是运行时类型信息完全可用的类型。这包括基本数据类型,非泛型类型,原始类型和无界通配符的调用。

不可具体化类型是编译时通过类型擦除移除了相关信息的类型——泛型类型的调用不是作为无界通配符定义的。一个不可具体化类型在运行时不是所有它的信息都可用。非具体化类型的样例有 List < String > 和 List < Number >;Java 虚拟机无法在运行时辨别他们的类型。就像在泛型的限制章节所展示的那样,某些场景下不可具体化类型不可以被使用:比如 instanceof 表达式,或者作为数据中的元素。

堆污染

堆污染发生在一个参数化类型变量引用了一个不是那个参数类型的对象时。如果程序执行了一些编译时可能产生未经检查警告的操作时,这种情况就会发生。如果在编译时(在编译时类型检查规则的范围内)或者运行时一个未经检查的警告发生了,涉及到参数化类型的操作(例如一次方法调用或者一次类型转换)的正确性不能被验证。例如,当混淆了原始类型与参数化类型,或者当执行一次未经检查的类型转换时,堆污染就会发生。

在正常情况下,当所有代码同时被编译完时,编译器发出一个未经检查的警告以引起你对潜在的堆污染的注意。如果你分开编译你的代码,将很难发现堆污染的潜在风险。如果你确保你的代码没有警告地通过编译,那么将不会发生堆污染。

拥有不可具体化类型的可变参数方法潜在的漏洞

包含可变传入参数的泛型方法可能导致堆污染。
考虑以下 ArrayBuilder 类:

  1. public class ArrayBuilder {
  2. public static <T> void addToList (List<T> listArg, T... elements) {
  3. for (T x : elements) {
  4. listArg.add(x);
  5. }
  6. }
  7. public static void faultyMethod(List<String>... l) {
  8. Object[] objectArray = l; // Valid
  9. objectArray[0] = Arrays.asList(42);
  10. String s = l[0].get(0); // ClassCastException thrown here
  11. }
  12. }

以下样例中,HeapPollutionExample 类使用 ArrayBuilder 类:

  1. public class HeapPollutionExample {
  2. public static void main(String[] args) {
  3. List<String> stringListA = new ArrayList<String>();
  4. List<String> stringListB = new ArrayList<String>();
  5. ArrayBuilder.addToList(stringListA, "Seven", "Eight", "Nine");
  6. ArrayBuilder.addToList(stringListB, "Ten", "Eleven", "Twelve");
  7. List<List<String>> listOfStringLists =
  8. new ArrayList<List<String>>();
  9. ArrayBuilder.addToList(listOfStringLists,
  10. stringListA, stringListB);
  11. ArrayBuilder.faultyMethod(Arrays.asList("Hello!"), Arrays.asList("World!"));
  12. }
  13. }

当编译完成后,ArrayBuilder.addToList 方法的定义将产生以下的警告:

  1. warning: [varargs] Possible heap pollution from parameterized vararg type T

当编译器遇到可变参数方法时,它把可变参数的形参翻译为数组。然而,Java 语言不允许参数化类型的数组的创建。在 ArrayBuilder.addToList 方法中,编译器把可变参数的形式参数 T...elements 翻译为形式参数 T[] elements 数组。然而,因为类型参数,编译器转换可变形式参数为 Object[] elements。所以,就有堆污染产生的可能。

以下语句将可变参数的形式参数 l 赋值给 Object 数组 objectArgs:

  1. Object[] objectArray = l;

这个语句可以潜在地引入堆污染。一个匹配可变参数形式参数 l 的参数化类型的值可以被赋值给变量 objectArray,所以能够被赋予给 l。然而,编译器不会在这个语句上产生未经检查的警告。编译器已经在它将可变参数的形式参数 List < String > ...l 转换为形式参数 list[] l 产生了一个警告。这个语句是有效的。变量 l 拥有list[] 类型,这个类型是 object[] 的子类型。

结果,编译器不会发出警告或者错误,即便你像以下语句那样,将任何类型的 List 对象赋值给 ObjectArray 数组部分:

  1. objectArray[0] = Array.asList(42);

这个语句将包含一个 Integer 类型的对象的 List 对象赋值给 objectArray 数组部分。假设你像以下语句这样调用了 ArrayBuilder.faultyMethod:

  1. ArrayBuilder.faultyMethod(Array.asList("Hello!"), Array.adList("World!"));

在运行时,虚拟机在以下语句抛出 ClassCastException 异常:

  1. // ClassCastException thrown here
  2. String s = l[0].get(0);

存储在变量 l 的第一个数据部分的对象拥有 List < Integer > 类型,但是这个语句期望一个 List < String > 类型的对象。

防止不可具体化类型的可变参数方法的警告

如果你声明一个可变参数方法,它拥有参数化类型的参数,而且你确保这个方法不会因为可变参数的形式参数的不正确处理,而抛出 ClassCastException 异常或者喜爱那个其他类似的异常,你可以通过添加以下注解给静态方法或者非构造方法的声明去防止编译器产生这一类的可变方法的警告:

  1. @safeVarargs

@safeVarargs 注解时方法约定的已经记录成文档的部分。这个注解断言方法的实现不会不正确地处理可变参数的形式参数。

即便更少需要,但是通过添加以下方法声明去抑制这些警告也是有可能的:

  1. @SuppressWarnings({"unchecked", "varargs"})

然而,这个方法不会从方法调用之处产生的警告。如果你对 @SuppressWarnings 语法不熟悉,参考注解

泛型的限制

要有效地使用 Java 泛型,你需要考虑以下限制。

不能使用基本数据类型实例化泛型类型

考虑以下参数化类型:

  1. class Pair<K, V> {
  2. private K key;
  3. private V value;
  4. public Pair(K key, V value) {
  5. this.key = key;
  6. this.value = value;
  7. }
  8. // ...
  9. }

当创建 Pair 对象时,你不能用基本数据类型替换类型参数 K 或者 V:

  1. Pair<int, char> p = new Pair<>(8, 'a'); // compile-time error

你只可以使用非基本数据类型来替换类型参数 K 或者 V:

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

注意,Java 编译器会将 8 自动装箱成 Integer.valueOf(8),和将 ‘a’ 自动装箱为 Character('a'):

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

更多关于自动装箱的信息,参考数字类与字符类课程中的自动装箱与自动拆箱

不能创建类型变量实例

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

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

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

  1. public static <E> void append(List<E> list, Class<E> cls) throws Exception {
  2. E elem = cls.newInstance(); // OK
  3. list.add(elem);
  4. }

你可以像以下代码这样调用 append 方法:

  1. List<String> ls = new ArrayList<>();
  2. append(ls, String.class);

不能声明类型为类型变量的静态属性

类的静态属性是被类的所有非静态对象共享的类级别的变量。所以,类型参数的静态属性是不允许的。考虑以下类:

  1. public class MobileDevice<T> {
  2. private static T os;
  3. // ...
  4. }

如果类型参数的静态属性是允许的,那么以下代码将会很疑惑:

  1. MobileDevice<Smartphone> phone = new MobileDevice<>();
  2. MobileDevice<Pager> pager = new MobileDevice<>();
  3. MobileDevice<TabletPC> pc = new MobileDevice<>();

因为静态属性 os 是被 phone,pager 和 pc 共享的。那么 os 的实际类型是什么呢?它不可能同时是 Smartphone,Pager 和 TabletPC。所以,你不能创建类型参数的实例。

不能使用参数化类型来做类型转换或者做 instanceof 判断

因为 Java 编译器擦除了所有泛型代码的类型参数,你不能在运行时验证哪个泛型类型的参数化类型正在被使用。

  1. public static <E> void rtti(List<E> list) {
  2. if (list instanceof ArrayList<Integer>) { // compile-time error
  3. // ...
  4. }
  5. }

传给 rtti 方法的参数化类型的结合是:

  1. S = { ArrayList<Integer>, ArrayList<String> LinkedList<Character>, ... }

运行时不会记录类型参数,所以它不能区分 ArrayList < Integer > 和 ArrayList < String > 的不同。你最多可以使用无界通配符去验证这个列表是一个数组列表 ArrayList:

  1. public static void rtti(List<?> list) {
  2. if (list instanceof ArrayList<?>) { // OK; instanceof requires a reifiable type
  3. // ...
  4. }
  5. }

显然,你不能转换成参数化类型除非它是被无界通配符参数化的,比如:

  1. List<Integer> li = new ArrayList<>();
  2. List<Number> ln = (List<Number>)li; // complile-time error

然而,有时候编译器知道类型参数是有效的,并且允许类型转换,比如:、

  1. List<String> l1 = ...;
  2. ArrayList<String> l2 = (ArrayList<String>)l1; // OK

不能创建参数化类型数组

你不能创建参数化类型的数组。例如,以下代码不能通过编译:

  1. List<Integer>[] arrayOfLists = new List<Integer>[2]; // compile-time error

以下代码解释了当不同类型被插入到一个数组中时将会发生什么:

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

如果你用泛型列表做同样的尝试,会有一个问题:

  1. Object[] stringLists = new List<String>[]; // compiler error, but pretend it's allowed
  2. stringLists[0] = new ArrayList<String>(); // OK
  3. stringLists[1] = new ArrayList<Integer>(); // An ArrayStoreException should be thrown,
  4. // but the runtime can't detect it.

如果参数化列表数组是允许的,这段代码将会因抛出 ArrayStoreException 异常而失败。

不能创建,捕获或者抛出参数化类型对象

泛型类不能直接或者间接地继承 Throwable 类。例如,下面的类不会通过编译:

  1. // Extends Throwable indirectly
  2. class MathException<T> extends Exception { /* ... */ } // compile-time error
  3. // Extends Throwable directly
  4. class QueueFullException<T> extends Throwable { /* ... */ // compile-time error

方法不能捕获类型参数的实例:

  1. public static <T extends Exception, J> void execute(List<J> jobs) {
  2. try {
  3. for (J job : jobs)
  4. // ...
  5. } catch (T e) { // compile-time error
  6. // ...
  7. }
  8. }

然而,你可以在 throw 子句中使用类型参数:

  1. class Parser<T extends Exception> {
  2. public void parse(File file) throws T { // OK
  3. // ...
  4. }
  5. }

不能重载形式参数类型擦除后为相同的原始类型的方法

类不能在类型擦除后有相同前面的重载方法:

  1. public class Example {
  2. public void print(Set<String> strSet) { }
  3. public void print(Set<Integer> intSet) { }
  4. }

这些重载都会共享相同的类文件表示,会产生编译时错误。

问题与练习

1.写一个泛型方法计算拥有特定属性的集合的元素个数(例如,奇整数,质数,回文)。

2.以下代码会通过编译么?如果不会,为什么?

  1. public final class Algorithm {
  2. public static <T> T max(T x, T y) {
  3. return x > y ? x : y;
  4. }
  5. }

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

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

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

  1. public class Pair<K, V> {
  2. public Pair(K key, V value) {
  3. this.key = key;
  4. this.value = value;
  5. }
  6. public K getKey(); { return key; }
  7. public V getValue(); { return value; }
  8. public void setKey(K key) { this.key = key; }
  9. public void setValue(V value) { this.value = value; }
  10. private K key;
  11. private V value;
  12. }

6.在类型擦除后,以下代码被转换成什么样?

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

7.以下方法会通过编译么?如果不会,为什么?

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

8.写一个泛型方法找出列表中范围在[begin, end]之间的最大元素。

9.以下类会通过编译么?如果不会,为什么?

  1. public class Singleton<T> {
  2. public static T getInstance() {
  3. if (instance == null)
  4. instance = new Singleton<T>();
  5. return instance;
  6. }
  7. private static T instance = null;
  8. }

10.给定以下类:

  1. class Shape { /* ... */ }
  2. class Circle extends Shape { /* ... */ }
  3. class Rectangle extends Shape { /* ... */ }
  4. class Node<T> { /* ... */ }

以下代码会通过编译么?如果不会,为什么?

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

11.考虑这个类:

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

以下代码会通过编译么?如果不会,为什么?

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

12.你如何调用以下方法找出与特定的整形列表互质的列表中的第一个整数。

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

注意,如果 gcd(a,b)=1,那么 a 和 b 两个整形是互质的,其中 gcd 是最大公约数(greatest common divisor)的简写。

检测你的答案

添加新批注
在作者公开此批注前,只有你和作者可见。
回复批注