Java编程思想第四版 第15章 泛型

  • 第15章 泛型
    • 1 与C的比较
    • 2 简单泛型
      • 21 一个元组类库
      • 22 一个堆栈类
      • 23 RandomList
    • 3 泛型接口
    • 4 泛型方法
      • 41杠杆利用类型参数推断
      • 42 可变参数与泛型方法
      • 43 用于Generator的泛型方法
      • 44 一个通用的Generator
      • 45 简化元组的使用
      • 46 一个Set实用工具
    • 5匿名内部类
    • 7 擦除的神秘之处
      • 71 C的方式
      • 72 迁移兼容性
      • 73 擦除的问题
      • 74 边界处的动作
    • 8 擦除的补偿
      • 81 创建类型实例
      • 82泛型数组
    • 9 边界
    • 10通配符
      • 101 编译器有多聪明
      • 102 逆变
      • 103 无界通配符
      • 104 捕获转换
    • 11问题
      • 111任何基本类型都不能作为类型参数
      • 112实现参数化接口
      • 113转型和警告
      • 114重裁
      • 115 基类劫持了接口

第15章 泛型

一般的类和方法。只能使用具体的类型:要么是基本类型。要么是自定义的类。如果要编写可以应用于多种类型的代码。这种刻板的限制对代码的束缚就会很大。

在面向对象编程语言中,多态算是一种泛化机制。例如,你可以将方法的参数类型设为基类,那么该方法就可以接受从这个基类中导出的任何类作为参数。这样的方法更加通用一些,可应用的地方也多一些。在类的内部也是如此,凡是需要说明类型的地方,如果都使用基类,确实能够良备更好的灵活性。但是,考虑到除了final类不能扩展,其他任何类都可以被扩展,所以这种灵活性大多数时候也会有一些性能损耗。

有时候,拘泥于单继承体系,也会使程序受限太多。如果方法的参数是一个接口,而不是一个类,这种限制就放松了许多。因为任何实现了该接口的类都能够满足该方法,这也包括暂时还不存在的类。这就给予客户端程序员一种选择,他可以通过实现一个接口来满足类或方法。因此,接口允许我们快捷地实现类继承,也使我们有机会创建一个新类来做到这一点。

可是有的时候,即便使用了接口,对程序的约束也还是太强了。因为一旦指明了接口,它就要求你的代码必须使用特定的接口。而我们希望达到的目的是编写更通用的代码,要使代码能够应用于“某种不具体的类型”,而不是一个具体的接口或类。

这就是Java SE5的重大变化之一:泛型的概念。泛型实现了参数化类型的概念,使代码可以应用于多种类型。“泛型“这个术语的意思是:“适用于许多许多的类型’。泛型在编程语言中出现时,其最初的目的是希望类或方法能够具备最广泛的表达能力。如何做到这一点呢,正是通过解耦类或方法与所使用的类型之间的约束。稍后你将看到,Java中的泛型并没有这么高的追求,实际上,你可能会质疑,Java中的术语“泛型”是否适合用来描述这一功能。

如果你从未接触过参数化类型机制,那么,在学习了Java中的泛型之后,你会发现,对这门语言而言,泛型确实是一个很有益的补充。在你创建参数化类型的一个实例时,编译器会为你负责转型操作,并且保证类型的正确性。这应该是一个进步。

然而,如果你了解其他语言(例如C++)中的参数化类型机制,你就会发现。有些以前能做到的事情,使用Java的泛型机制却无法做到。使用别人已经构建好的泛型类型会相当容易,但是如果你要自己创建一个泛型实例,就会遇到许多令你吃惊的事情。在本章中,我的任务之一就是向你解释,Java中的泛型是怎样发展成现在这样的。

这并非是说Java的泛型毫无用处。在很多情况下,它们可以使代码更直接更优雅。不过,如果你具备其他语言的经验,而那种语言实现了更纯粹的泛型,那么,Java可能令你失望了。在本章中,我们会介绍Java泛型的优点与局限,希望这能够帮助你更有效地使用Java的这个新功能。

15.1 与C++的比较

Java的设计者曾说过,设计这门语言的灵感主要来自C++。尽管如此,学习Java时,基本上可以不用参考C++。我也是尽力这样做的,除非,与C++的比较能够加深你的理解。

Java中的泛型就需要与C++进行一番比较,理由有二:
- 首先,丁解C++模板的某些方面,有助于你理解泛型的基础。同时,非常重要的一点是,你可以了解Java泛型的局限是什么,以及为什么会有这些限制。最终的目的是帮助你理解,Java泛型的边界在哪里。理解了边界所在,你才能成为程序高手。因为只有知道了某个技术不能做到什么,你才能更好地做到所能做的(部分原因是,不必浪费时间在死胡同里乱转)。
- 第二个原因是,在Java社区中,人们普遍时C++摸板有一种误解,而这种误解可能会误导你令你在理解泛型的意图时产生偏差。

因此,在本章中会介绍一些C++模板的例子,不过我也会尽量控制它们的篇幅。

15.2 简单泛型

有许多原因促成了泛型的出现,而最引人往目的一个原因,就是为了创造容器类。容器,就是存放要使用的对象的地方.数组也是如此,不过与简单的数组相比,容器类更加灵活,具备更多不同的功能。事实上,所有的程序,在运行时都要求你持有一大堆对象,所以,容器类算得上最具重用性的类库之一。

我们先来看看一个只能持有单个对象的类。当然了,这个类可以明确指定其持有的对象的类型:

//: generics/Holder1.java

class Automobile {}

public class Holder1 {
  private Automobile a;
  public Holder1(Automobile a) { this.a = a; }
  Automobile get() { return a; }
} ///:~

不过,这个类的可重用性就不怎么样了,它无法特有其他类型的任何对象。我们可不希望为碰到的每个类型都编写一个新的类。
在Javaa SE5之前,我们可以让这个类直接持有Object类型的对象:

//: generics/Holder2.java

public class Holder2 {
  private Object a;
  public Holder2(Object a) { this.a = a; }
  public void set(Object a) { this.a = a; }
  public Object get() { return a; }
  public static void main(String[] args) {
    Holder2 h2 = new Holder2(new Automobile());
    Automobile a = (Automobile)h2.get();
    h2.set("Not an Automobile");
    String s = (String)h2.get();
    h2.set(1); // Autoboxes to Integer
    Integer x = (Integer)h2.get();
  }
} ///:~

现在,Holder2可以存储任何类型的对象,在这个例子中,只用了一个Hlolder2对象,却先后三次存储了三种不同类型的对象。

有些情况下,我们确实希望容器能够同时持有多种类型的对象。但是,通常而言,我们只会使用容器来存储一种类型的对象。泛型的主要目的之一就是用来指定容器要持有什么类型的对象。而且由编译器来保证类型的正确性。

因此,与其使用Object,我们更喜欢暂时不指定类型,而是稍后再决定具体使用什么类型。要达到这个目的,需要使用类型参数,用尖括号括住,放在类名后面。然后在使用这个类的时候,再用实际的类型替换此类型参数。在下面的例子中,T就是类型参数:

//: generics/Holder3.java

public class Holder3<T> {
  private T a;
  public Holder3(T a) { this.a = a; }
  public void set(T a) { this.a = a; }
  public T get() { return a; }
  public static void main(String[] args) {
    Holder3 h3 =
      new Holder3(new Automobile());
    Automobile a = h3.get(); // No cast needed
    // h3.set("Not an Automobile"); // Error
    // h3.set(1); // Error
  }
} ///:~

现在,当你创建Holder3对象时,必须指明想持有什么类型的对象,将其置于尖括号内。就像main()中那样。然后,你就只能在Holder3中存入该类型(或其子类,因为多态与泛型不冲突)的对象了。并且,在你从Holder3中取出它持有的对象时,自动地就是正确的类型。
这就是Java泛型的核心概念:告诉编译器想使用什么类型,然后编译器帮你处理一切细节。

一般而言,你可以认为泛型与其他的类型差不多,只不过它们碰巧有类型参数罢了。稍后我们会看到,在使用泛型时,我们只需指定它们的名称以及类型参数列表即可。

15.2.1 一个元组类库

仅一次方法调用就能返回多个对象,你应该经常需要这样的功能吧。可是return语句只允许返回单个对象,因此,解决办法就是创建一个对象,用它来持有想要返回的多个对象。当然,可以在每次需要的时候,专门创建一个类来完成这样的工作。可是有了泛型。我们就能够一次性地解决该问题,以后再也不用在这个问题上浪费时间了。同时,我们在编译期就能确保类型安全。

这个概念称为元组(tuple),它是将一组对象直接打包存储干其中的一个单一对象。这个容器对象允许谈取其中元素,但是不允许向其中存放新的对象。(这个概念也称为数据传这对象或信使。)

通常,元组可以具有任意长度,同时,元组中的对象可以是任意不同的类型。不过,我们希望能够为每一个对象指明其类型,并且从容器中读取出来时,能够得到正确的类型。要处理不同长度的问题,我们需要创建多个不同的元组。下面的程序是一个2维元组,它能够持有两个对象:

//: net/mindview/util/TwoTuple.java
package net.mindview.util;

public class TwoTuple<A,B> {
  public final A first;
  public final B second;
  public TwoTuple(A a, B b) { first = a; second = b; }
  public String toString() {
    return "(" + first + ", " + second + ")";
  }
} ///:~

构造器捕获了要存储的对象,而toString()是一个便利函数,用来显示列表中的值。注意,元组隐含地保持了其中元素的次序。

第一次阅读上面的代码时,你也许会想,这不是违反了Java编程的安全性原则吗?first和second应该声明为private,然后提供getFirst()和getSecond()之类的访问方法才对呀?让我们仔细看看这个例子中的安全性:客户端程序可以读取first和second对象,然后可以随心所欲地使用这两个对象。但是,它们却无法将其他值赋予first或second。因为final声明为你买了相同的安全保险,而且这种格式更简洁明了。

还有另一种设计考虑,即你确实希望允许客户端程序员改变first办second所引用的对象。然而,采用以上的形式无疑是更安全的做法,这样的话,如果程序员想要使用具有不同元素的元组,就强制要求他们另外创建一个新的TwoTuple对象。

我们可以利用继承机制实现长度更长的元组。从下面的例子中可以看到,增加类型参数是件很简单的事情:

//: net/mindview/util/ThreeTuple.java
package net.mindview.util;

public class ThreeTuple<A,B,C> extends TwoTuple<A,B> {
  public final C third;
  public ThreeTuple(A a, B b, C c) {
    super(a, b);
    third = c;
  }
  public String toString() {
    return "(" + first + ", " + second + ", " + third +")";
  }
} ///:~

为了使用元组,你只需定义一个长度适合的元组,将其作为方法的返回值,然后在return语句中创建该元组,井返回即可。

//: generics/TupleTest.java
import net.mindview.util.*;

class Amphibian {}
class Vehicle {}

public class TupleTest {
  static TwoTuple f() {
    // Autoboxing converts the int to Integer:
    return new TwoTuple("hi", 47);
  }
  static ThreeTuple g() {
    return new ThreeTuple(
      new Amphibian(), "hi", 47);
  }
  static
  FourTuple h() {
    return
      new FourTuple(
        new Vehicle(), new Amphibian(), "hi", 47);
  }
  static
  FiveTuple k() {
    return new
      FiveTuple(
        new Vehicle(), new Amphibian(), "hi", 47, 11.1);
  }
  public static void main(String[] args) {
    TwoTuple ttsi = f();
    System.out.println(ttsi);
    // ttsi.first = "there"; // Compile error: final
    System.out.println(g());
    System.out.println(h());
    System.out.println(k());
  }
} /* Output: (80% match)
(hi, 47)
(Amphibian@1f6a7b9, hi, 47)
(Vehicle@35ce36, Amphibian@757aef, hi, 47)
(Vehicle@9cab16, Amphibian@1a46e30, hi, 47, 11.1)
*///:~

由于有了泛型,你可以很容易地创建元组,令其返回一组任意类型的对象。而你所要做的,只是编写表达式而已。

通过ttsi.first=“there”语句的错误,我们可以看出,final明确实能够保护public元素,在对象被构造出来之后,声明为final的元素便不能被再赋予其他值了。

在上面的程序中,new表达式确实有点罗嗦。本章稍后会介绍,如何利用泛型方法简化这样的表达式。

15.2.2 一个堆栈类

接下来我们看一个稍微复杂一点的例子:传统的下推堆栈。在第11章中,我们看到,这个堆栈是作为net.mindview.util.Stack类,用一个LinkedList实现的。在那个例子中,LinkedList本身已经具备了创建堆栈所必需的方法,而Stack也可以通过两个泛型的类StackLinkedList的组合来创建。在那个示例中,我们可以看出,泛型类型也就是另一种类型罢了(稍候我们会一些例外的情况)。

现在我们不用LinkedList,来实现自己的内部链式存储机制:

public class LinkedStack<T> {
  private static class Node<U> {
    U item;
    Node next;
    Node() { item = null; next = null; }
    Node(U item, Node next) {
      this.item = item;
      this.next = next;
    }
    boolean end() { return item == null && next == null; }
  }
  private Node top = new Node(); // End sentinel
  public void push(T item) {
    top = new Node(item, top);
  }    
  public T pop() {
    T result = top.item;
    if(!top.end())
      top = top.next;
    return result;
  }
  public static void main(String[] args) {
    LinkedStack lss = new LinkedStack();
    for(String s : "Phasers on stun!".split(" "))
      lss.push(s);
    String s;
    while((s = lss.pop()) != null)
      System.out.println(s);
  }
}

/* Output:
stun!
on
Phasers
*/

内部类Node也是一个泛型,它拥有自己的类型参数。

这个例子使用了一个末端哨兵(end sentinel)来判断堆栈何时为空。这个末端哨兵是在构造LinkedStack时创建的。然后,每调用一次push()方法,就会创建一个Node对象,并将其链接到前一个Node对象。当你调用pop()方法时,总是返top.item,然后丢弃当前top所指的Node,并将top转移到下一个Node,除非你已经碰到了末端哨兵,这时候就不再移动top了。如果已经到了末端,客户端程序还继续调用pop()方法,它只能得到null,说明堆栈已经空了。

15.2.3 RandomList

作为容器的另一个例子,假设我们需要一个持有特定类型对象的列表,每次调用其上的select()方法时,它可以随机地选取一个元素。如果我们希望以此构建一个可以应用干各种类型的对象的工具,就需要使用泛型:

//: generics/RandomList.java
import java.util.*;

public class RandomList<T> {
  private ArrayList storage = new ArrayList();
  private Random rand = new Random(47);
  public void add(T item) { storage.add(item); }
  public T select() {
    return storage.get(rand.nextInt(storage.size()));
  }
  public static void main(String[] args) {
    RandomList rs = new RandomList();
    for(String s: ("The quick brown fox jumped over " +
        "the lazy brown dog").split(" "))
      rs.add(s);
    for(int i = 0; i < 11; i++)
      System.out.print(rs.select() + " ");
  }
} /* Output:
brown over fox quick quick dog brown The brown lazy brown
*///:~

15.3 泛型接口

泛型也可以应用于接口。例如生成器(generator),这是一种专门负责创建对象的类。实际上,这是工厂方法设计模式的一种应用。不过,当使用生成器创建新的对象时,它不需要任何参数,而工厂方法一般需要参数。也鼓是说,生成器无需额外的信息就知道如何创建新对象。
一般而言,一个生成器只定义一个方法,该方法用以产生新的对象.在这里,就是next方法。

// A generic interface.
public interface Generator<T> { T next(); }

方法next()的返回类型是参数化的T。正如你所见到的,接口使用泛型与类使用泛型没什么区别。

为了演示如何实现Generator接口,我们还需要一些别的类。例如,Coffee类层次结构如下:

public class Coffee {
  private static long counter = 0;
  private final long id = counter++;
  public String toString() {
    return getClass().getSimpleName() + " " + id;
  }
}

public class Latte extends Coffee {} // 拿铁
public class Mocha extends Coffee {}  // 摩卡
public class Cappuccino extends Coffee {} // 卡布奇诺
public class Americano extends Coffee {} // 美式咖啡
public class Breve extends Coffee {} 

现在,我们可以编写一个类,实现Generator接口,它能够随机生成不同类型的Coffee对象。

public class CoffeeGenerator implements Generator<Coffee>, Iterable<Coffee> {
  private Class[] types = { Latte.class, Mocha.class,
    Cappuccino.class, Americano.class, Breve.class, };
  private static Random rand = new Random(47);
  public CoffeeGenerator() {}
  // For iteration:
  private int size = 0;
  public CoffeeGenerator(int sz) { size = sz; } 
  public Coffee next() {
    try {
      return (Coffee)
        types[rand.nextInt(types.length)].newInstance();
      // Report programmer errors at run time:
    } catch(Exception e) {
      throw new RuntimeException(e);
    }
  }
  class CoffeeIterator implements Iterator {
    int count = size;
    public boolean hasNext() { return count > 0; }
    public Coffee next() {
      count--;
      return CoffeeGenerator.this.next();
    }
    public void remove() { // Not implemented
      throw new UnsupportedOperationException();
    }
  };  
  public Iterator iterator() {
    return new CoffeeIterator();
  }
  public static void main(String[] args) {
    CoffeeGenerator gen = new CoffeeGenerator();
    for(int i = 0; i < 5; i++)
      System.out.println(gen.next());
    for(Coffee c : new CoffeeGenerator(5))
      System.out.println(c);
  }
} /* Output:
Americano 0
Latte 1
Americano 2
Mocha 3
Mocha 4
Breve 5
Americano 6
Latte 7
Cappuccino 8
Cappuccino 9
*///:~

参数化的Generator接口确保next()的返回值是参数的类型。CofeeGenerator同时还实现了Iterable接口,所以它可以在循环语句中使用。不过,它还需要一个“末端哨兵”来判断何时停止,这正是第二个构造器的功能。

下面的类是Generetore接口的另一个实现,它负责生成Fibonacci(斐波那契)数列:

public class Fibonacci implements Generator<Integer> {
  private int count = 0;
  public Integer next() { return fib(count++); }
  private int fib(int n) {
    if(n < 2) return 1;
    return fib(n-2) + fib(n-1);
  }
  public static void main(String[] args) {
    Fibonacci gen = new Fibonacci();
    for(int i = 0; i < 18; i++)
      System.out.print(gen.next() + " ");
  }
}
/* Output:
1 1 2 3 5 8 13 21 34 55 89 144 233 377 610 987 1597 2584
*///:~

虽然我们在Fibonacci类的里里外外使用的都是int类型,但是其类型参数却是Integer,这个例子引出了Java泛型的一个局限性:基本类型无法作为类型参数。不过,Java 5具备了自动打包和自动拆包的功能,可以很方便地在基本类型和其相应的包装器类型之间进行转换。通过这个例子中Fibonacci类对int的使用,我们已经看到了这种效果。

如果还想更进一步,编写一个实现了Iterable的Fibonacci生成器。我们的一个选择是重写这个类,令其实现Iterable接口。不过,你并不是总能拥有源代码的控制权,井且,除非必须这么做,否则,我们也不愿意重写一个类。而且我们还有另一种选择,就是创建个过配器(adapter)来实现所需的接口,我们在前面介绍过这个设计模式。

有多种方法可以实现适配器。例如,可以通过继承来创建适配器类:

public class IterableFibonacci extends Fibonacci implements Iterable<Integer> {
  private int n;
  public IterableFibonacci(int count) { n = count; }
  public Iterator iterator() {
    return new Iterator() {
      public boolean hasNext() { return n > 0; }
      public Integer next() {
        n--;
        return IterableFibonacci.this.next();
      }
      public void remove() { // Not implemented
        throw new UnsupportedOperationException();
      }
    };
  } 
  public static void main(String[] args) {
    for(int i : new IterableFibonacci(18))
      System.out.print(i + " ");
  }
} 
/* Output:
1 1 2 3 5 8 13 21 34 55 89 144 233 377 610 987 1597 2584
*///:~

如果要在循环语句中使用IterableFibonacci,必须向IterableFibonacci的构造器提供一个边界值,然后hasNext()方法才能知道何时应该返回false。

15.4 泛型方法

到目前为止,我们看到的泛型,都是应用于整个类上。但同样可以在类中包含参数化方法,而这个方法所在的类可以是泛型类,也可以不是泛型类。也就是说,是否拥有泛型方法,与其所在的类是否是泛型没有关系。

泛型方法使得该方法能够独立于类而产生变化。以下是一个基本的指导原则无论何时,只要你能做到,你就应该尽最使用泛型方法。也就是说,如果使用泛型方法可以取代将整个类泛型化,那么就应该只使用泛型方法,因为它可以使事情更清楚明白。另外,对于一个static的方法而言,无法访向泛型类的类型参数,所以,如果static方法需要使用泛型能力,就必须使其成为泛型方法。

要定义泛型方法,只需将泛型参数列表置于返回值之前,就像下面这样:

public class GenericMethods {
  public  void f(T x) {
    System.out.println(x.getClass().getName());
  }
  public static void main(String[] args) {
    GenericMethods gm = new GenericMethods();
    gm.f("");
    gm.f(1);
    gm.f(1.0);
    gm.f(1.0F);
    gm.f('c');
    gm.f(gm);
  }
} /* Output:
java.lang.String
java.lang.Integer
java.lang.Double
java.lang.Float
java.lang.Character
GenericMethods
*///:~

GenericMethods并不是参数化的,尽管这个类和其内部的方法可以被同时参数化,但是在这个例子中,只有方法f()拥有类型参数。这是由该方法的返回类型前面的类型参数列表指明的。

注意,当使用泛型类时,必须在创建对象的时候指定类型参数的值,而使用泛型方法的时候,通常不必指明参数类型,因为编译器会为我们找出具体的类型。这称为类型参数推断(type argumeut inference)。因此,我们可以像调用普通方法一样调用f(),而且就好像是f()被无限次地重载过。它甚至可以接受GenericMethods作为其类型参数。

如果调用f()时传人基本类型,自动打包机制就会介入其中,将基本类型的值包装为对应的对象。事实上,泛型方法与自动打包避免了许多以前我们不得不自己编写出来的代码。

15.4.1杠杆利用类型参数推断

人们对泛型有一个抱怨,使用泛型有时候需要向程序中加人更多的代码。如果要创建一个持有List的Map,就要像下面这样:

    MapList extends Pet>> petPeople = 
      new HashMapList extends Pet>>}();

(本章稍后会介绍表达式中问号与extends的用法。)看到了吧,你在重复自己做过的事情,编译器本来应该能够从泛型参数列表中的一个参数推断出另一个参数。唉,可惜的是,编译器暂时还做不到。然而,在泛型方法中,类型参数推断可以为我们简化一部分工作。例如,我们可以编写一个工目类,它包含各种各样的static方法,专门用来创建各种常用的容器对象:

public class New {
  public static  Map map() {
    return new HashMap();
  }
  public static  List list() {
    return new ArrayList();
  }
  public static  LinkedList lList() {
    return new LinkedList();
  }
  public static  Set set() {
    return new HashSet();
  } 
  public static  Queue queue() {
    return new LinkedList();
  }
  // Examples:
  public static void main(String[] args) {
    Map> sls = New.map();
    List ls = New.list();
    LinkedList lls = New.lList();
    Set ss = New.set();
    Queue qs = New.queue();
  }
} ///:~

main()方法演示了如何使用这个工具类,类型参数推断避免了重复的泛型参数列表。它同样可以应用于holding/MapOfList.java:

//: generics/SimplerPets.java
import typeinfo.pets.*;
import java.util.*;
import net.mindview.util.*;

public class SimplerPets {
  public static void main(String[] args) {
    Map> petPeople = New.map();
    // Rest of the code is the same...
  }
} ///:~

对于类型参数推断而言,这是一个有趣的例子。不过,很难说它为我们带来了多少好处。

类型推断只对赋值操作有效,其他时候并不起作用。如果你将一个泛型方法调用的结果(例如New.map())作为参数,传递给另一个方法,这时编译器井不会执行类型推断。在这种情况下,编译器认为:调用泛型方法后,其返回值被赋给一个Object类型的变量。下面的例子证明了这一点:

public class LimitsOfInference {
  static void
  f(Map> petPeople) {}
  public static void main(String[] args) {
    // f(New.map()); // Does not compile
  }
} ///:~
  • 显式的类型说明
    在泛型方法中,可以显式地指明类型,不过这种语法很少使用。要显式地指明类型,必须在点操作符与方法名之间擂人尖括号,然后把A型置于尖括号内。如果是在定义该方法的类的内部,必须在点操作符之前使用this关键字,如果是使用static的方法,必须在点操作符之前加上类名。使用这种语法,可以解决LimitsOfInference.java中的问题:
public class ExplicitTypeSpecification {
  static void f(Map> petPeople) {}
  public static void main(String[] args) {
    f(New.>map());
  }
} ///:~

当然,这种语法抵消了New类为我们带来的好处(即省去了大量的类型说明),不过,只有在编写非赋值语句时,我们才需要这样的额外说明。

15.4.2 可变参数与泛型方法

泛型方法与可变参数列表能够很好地共存:

public class GenericVarargs {
  public static  List makeList(T... args) {
    List result = new ArrayList();
    for(T item : args)
      result.add(item);
    return result;
  }
  public static void main(String[] args) {
    List ls = makeList("A");
    System.out.println(ls);
    ls = makeList("A", "B", "C");
    System.out.println(ls);
    ls = makeList("ABCDEFFHIJKLMNOPQRSTUVWXYZ".split(""));
    System.out.println(ls);
  }
} /* Output:
[A]
[A, B, C]
[, A, B, C, D, E, F, F, H, I, J, K, L, M, N, O, P, Q, R, S, T, U, V, W, X, Y, Z]
*///:~

makeList()方法展示了与标淮类库中java.util.Arrays.asList()方法相同的功能。

15.4.3 用于Generator的泛型方法

利用生成器,我们可以很方便地填充一个Collection,而泛型化这种操作是具有实际意义的:

public class Generators {
  public static  Collection
  fill(Collection coll, Generator gen, int n) {
    for(int i = 0; i < n; i++)
      coll.add(gen.next());
    return coll;
  } 
  public static void main(String[] args) {
    Collection coffee = fill(
      new ArrayList(), new CoffeeGenerator(), 4);
    for(Coffee c : coffee)
      System.out.println(c);
    Collection fnumbers = fill(
      new ArrayList(), new Fibonacci(), 12);
    for(int i : fnumbers)
      System.out.print(i + ", ");
  }
} /* Output:
Americano 0
Latte 1
Americano 2
Mocha 3
1, 1, 2, 3, 5, 8, 13, 21, 34, 55, 89, 144,
*///:~

请注意,fill()方法是如何透明地应用于Coffee和lnteger的容器和生成器。

15.4.4 一个通用的Generator

下面的程序可以为任何类构造一个Generator,只要该类具有默认的构造器。为了减少类型声明,它提供了一个泛型方法。用以生成BasicGenerator:

public class BasicGenerator<T> implements Generator<T> {
  private Class type;
  public BasicGenerator(Class type){ this.type = type; }
  public T next() {
    try {
      // Assumes type is a public class:
      return type.newInstance();
    } catch(Exception e) {
      throw new RuntimeException(e);
    }
  }
  // Produce a Default generator given a type token:
  public static  Generator create(Class type) {
    return new BasicGenerator(type);
  }
}

这个类提供了一个基本实现,用以生成某个类的对象。这个类必需具备两个特点:
(1)它必须声明为public,(因为BasicGenerator与要处理的类在不同的包中,所以该类必须声明为public,并且不只具有包内访问权限.)
(2)它必须具备默认的构造器(无参数的构造器)。要创建这样的BasicGenerator对象,只需调用create()方法,并传入想要生成的类型。泛型化的create()方法允许执行BasicGenerator.create(MyType.class),而不必执行麻烦的new BasicGenerator(MyType.class)。

例如,下面是一个具有默认构造器的简单的类:

//: generics/CountedObject.java

public class CountedObject {
  private static long counter = 0;
  private final long id = counter++;
  public long id() { return id; }
  public String toString() { return "CountedObject " + id;}
} ///:~

使用BasicGenerator,你可以很容易地为CountedObject创建一个Generator:

//: generics/BasicGeneratorDemo.java
import net.mindview.util.*;

public class BasicGeneratorDemo {
  public static void main(String[] args) {
    Generator gen =
      BasicGenerator.create(CountedObject.class);
    for(int i = 0; i < 5; i++)
      System.out.println(gen.next());
  }
} /* Output:
CountedObject 0
CountedObject 1
CountedObject 2
CountedObject 3
CountedObject 4
*///:~

可以看到,使用泛型方法创建Generator对象,大大减少了我们要编写的代码。Java泛型要求传人Class对象,以便也可以在create()方法中用它进行类型推断。

CountedObject类能够记录下它创建了多少个CountedObject实例,井通过tostring()方法告诉我们其编号。

15.4.5 简化元组的使用

有了类型参数推断。再加上static方法.我们可以重新编写之前看到的元组工具,使其成为更通用的工具类库。在这个类中,我们通过重载static方法创建元组:

//: net/mindview/util/Tuple.java
// Tuple library using type argument inference.
package net.mindview.util;

public class Tuple {
  public static  TwoTuple tuple(A a, B b) {
    return new TwoTuple(a, b);
  }
  public static  ThreeTuple
  tuple(A a, B b, C c) {
    return new ThreeTuple(a, b, c);
  }
  public static  FourTuple
  tuple(A a, B b, C c, D d) {
    return new FourTuple(a, b, c, d);
  }
  public static 
  FiveTuple tuple(A a, B b, C c, D d, E e) {
    return new FiveTuple(a, b, c, d, e);
  }
} ///:~

下面是修改后的TupleTest.java,用来测试Tuple.java:

//: generics/TupleTest2.java
import net.mindview.util.*;
import static net.mindview.util.Tuple.*;

public class TupleTest2 {
  static TwoTuple f() {
    return tuple("hi", 47);
  }
  static TwoTuple f2() { return tuple("hi", 47); }
  static ThreeTuple g() {
    return tuple(new Amphibian(), "hi", 47);
  }
  static
  FourTuple h() {
    return tuple(new Vehicle(), new Amphibian(), "hi", 47);
  }
  static
  FiveTuple k() {
    return tuple(new Vehicle(), new Amphibian(),
      "hi", 47, 11.1);
  }
  public static void main(String[] args) {
    TwoTuple ttsi = f();
    System.out.println(ttsi);
    System.out.println(f2());
    System.out.println(g());
    System.out.println(h());
    System.out.println(k());
  }
} /* Output: (80% match)
(hi, 47)
(hi, 47)
(Amphibian@7d772e, hi, 47)
(Vehicle@757aef, Amphibian@d9f9c3, hi, 47)
(Vehicle@1a46e30, Amphibian@3e25a5, hi, 47, 11.1)
*///:~

注意,方法f()返回一个参数化的TwoTuple对象,而f2()返回的是非参数化的TwoTuple对象。在这个例子中,编译器井没有关于f2()的警告信息,因为我们井没有将其返回值作为参数化对象使用。在某种意义上,它被“向上转型’为一个非参数化的TwoTuple.然而,如果试图将f2()的返回值转型为参数化的TwoTuple,编译器就会发出警告。

15.4.6 一个Set实用工具

作为泛型方法的另一个示例,我们看看如何用Set来表达数学中的关系式。通过使用泛型方法,可以很方便地做到这一点,而且可以应用于多种类型:

//: net/mindview/util/Sets.java
package net.mindview.util;
import java.util.*;

public class Sets {
  public static  Set union(Set a, Set b) {
    Set result = new HashSet(a);
    result.addAll(b);
    return result;
  }
  public static 
  Set intersection(Set a, Set b) {
    Set result = new HashSet(a);
    result.retainAll(b);
    return result;
  } 
  // Subtract subset from superset:
  public static  Set
  difference(Set superset, Set subset) {
    Set result = new HashSet(superset);
    result.removeAll(subset);
    return result;
  }
  // Reflexive--everything not in the intersection:
  public static  Set complement(Set a, Set b) {
    return difference(union(a, b), intersection(a, b));
  }
} ///:~

在前三个方法中,都将第一个参数Set复制了一份,将Set中的所有引用都存人一个新的HashSet对象中,因此,我们并未直接修改参数中的Set,返回的值是一个全新的set对象。
这四个方法表达了如下的数学集合操作:union()返回一个Set,它将两个参数合并在一起;intersection()返回的Set只包含两个参数共有的部分。difference()方法从superset中移除subset包含的元素;complement()返回的Set包含除了交集之外的所有元素。下面提供了一个enum,它包
含各种水彩画的颜色。我们将用它来演示以上这些方法的功能和效果。

//: generics/watercolors/Watercolors.java
package generics.watercolors;

public enum Watercolors {
  ZINC, LEMON_YELLOW, MEDIUM_YELLOW, DEEP_YELLOW, ORANGE,
  BRILLIANT_RED, CRIMSON, MAGENTA, ROSE_MADDER, VIOLET,
  CERULEAN_BLUE_HUE, PHTHALO_BLUE, ULTRAMARINE,
  COBALT_BLUE_HUE, PERMANENT_GREEN, VIRIDIAN_HUE,
  SAP_GREEN, YELLOW_OCHRE, BURNT_SIENNA, RAW_UMBER,
  BURNT_UMBER, PAYNES_GRAY, IVORY_BLACK
} ///:~

为了方便起见(可以直接使用enum中的元素名),下面的示例以static的方式引人Watercolors,这是Java SE5中的新工具,用来从enum直接创建Set。在这里,我们向static方法EnumSet.range()传入某个范围的第一个元素与最后一个元素,然后它将返回一个Set,其中包含该范围内的所有元素:

//: generics/WatercolorSets.java
import generics.watercolors.*;
import java.util.*;
import static net.mindview.util.Print.*;
import static net.mindview.util.Sets.*;
import static generics.watercolors.Watercolors.*;

public class WatercolorSets {
  public static void main(String[] args) {
    Set set1 =
      EnumSet.range(BRILLIANT_RED, VIRIDIAN_HUE);
    Set set2 =
      EnumSet.range(CERULEAN_BLUE_HUE, BURNT_UMBER);
    print("set1: " + set1);
    print("set2: " + set2);
    print("union(set1, set2): " + union(set1, set2));
    Set subset = intersection(set1, set2);
    print("intersection(set1, set2): " + subset);
    print("difference(set1, subset): " +
      difference(set1, subset));    
    print("difference(set2, subset): " +
      difference(set2, subset));
    print("complement(set1, set2): " +
      complement(set1, set2));
  }    
} /* Output: (Sample)
set1: [BRILLIANT_RED, CRIMSON, MAGENTA, ROSE_MADDER, VIOLET, CERULEAN_BLUE_HUE, PHTHALO_BLUE, ULTRAMARINE, COBALT_BLUE_HUE, PERMANENT_GREEN, VIRIDIAN_HUE]
set2: [CERULEAN_BLUE_HUE, PHTHALO_BLUE, ULTRAMARINE, COBALT_BLUE_HUE, PERMANENT_GREEN, VIRIDIAN_HUE, SAP_GREEN, YELLOW_OCHRE, BURNT_SIENNA, RAW_UMBER, BURNT_UMBER]
union(set1, set2): [SAP_GREEN, ROSE_MADDER, YELLOW_OCHRE, PERMANENT_GREEN, BURNT_UMBER, COBALT_BLUE_HUE, VIOLET, BRILLIANT_RED, RAW_UMBER, ULTRAMARINE, BURNT_SIENNA, CRIMSON, CERULEAN_BLUE_HUE, PHTHALO_BLUE, MAGENTA, VIRIDIAN_HUE]
intersection(set1, set2): [ULTRAMARINE, PERMANENT_GREEN, COBALT_BLUE_HUE, PHTHALO_BLUE, CERULEAN_BLUE_HUE, VIRIDIAN_HUE]
difference(set1, subset): [ROSE_MADDER, CRIMSON, VIOLET, MAGENTA, BRILLIANT_RED]
difference(set2, subset): [RAW_UMBER, SAP_GREEN, YELLOW_OCHRE, BURNT_SIENNA, BURNT_UMBER]
complement(set1, set2): [SAP_GREEN, ROSE_MADDER, YELLOW_OCHRE, BURNT_UMBER, VIOLET, BRILLIANT_RED, RAW_UMBER, BURNT_SIENNA, CRIMSON, MAGENTA]
*///:~

我们可以从输出中看到各种关系运算的结果。
下面的示例使用Sets.difference()打印出java.util包中各种Collection类与Map类之间的方法差异:

//: net/mindview/util/ContainerMethodDifferences.java
package net.mindview.util;
import java.lang.reflect.*;
import java.util.*;

public class ContainerMethodDifferences {
  static Set methodSet(Class type) {
    Set result = new TreeSet();
    for(Method m : type.getMethods())
      result.add(m.getName());
    return result;
  }
  static void interfaces(Class type) {
    System.out.print("Interfaces in " +
      type.getSimpleName() + ": ");
    List result = new ArrayList();
    for(Class c : type.getInterfaces())
      result.add(c.getSimpleName());
    System.out.println(result);
  }
  static Set object = methodSet(Object.class);
  static { object.add("clone"); }
  static void
  difference(Class superset, Class subset) {
    System.out.print(superset.getSimpleName() +
      " extends " + subset.getSimpleName() + ", adds: ");
    Set comp = Sets.difference(
      methodSet(superset), methodSet(subset));
    comp.removeAll(object); // Don't show 'Object' methods
    System.out.println(comp);
    interfaces(superset);
  }
  public static void main(String[] args) {
    System.out.println("Collection: " +
      methodSet(Collection.class));
    interfaces(Collection.class);
    difference(Set.class, Collection.class);
    difference(HashSet.class, Set.class);
    difference(LinkedHashSet.class, HashSet.class);
    difference(TreeSet.class, Set.class);
    difference(List.class, Collection.class);
    difference(ArrayList.class, List.class);
    difference(LinkedList.class, List.class);
    difference(Queue.class, Collection.class);
    difference(PriorityQueue.class, Queue.class);
    System.out.println("Map: " + methodSet(Map.class));
    difference(HashMap.class, Map.class);
    difference(LinkedHashMap.class, HashMap.class);
    difference(SortedMap.class, Map.class);
    difference(TreeMap.class, Map.class);
  }
} ///:~

在第11章的“总结”中,我们使用了这个程序的输出结果。

15.5匿名内部类

泛型还可以应用于内部类以及匿名内部类,下面的示例使用匿名内部类实现了Generator接口。

//: generics/BankTeller.java
// A very simple bank teller simulation.
import java.util.*;
import net.mindview.util.*;

class Customer {
  private static long counter = 1;
  private final long id = counter++;
  private Customer() {}
  public String toString() { return "Customer " + id; }
  // A method to produce Generator objects:
  public static Generator generator() {
    return new Generator() {
      public Customer next() { return new Customer(); }
    };
  }
}   

class Teller {
  private static long counter = 1;
  private final long id = counter++;
  private Teller() {}
  public String toString() { return "Teller " + id; }
  // A single Generator object:
  public static Generator generator =
    new Generator() {
      public Teller next() { return new Teller(); }
    };
}   

public class BankTeller {
  public static void serve(Teller t, Customer c) {
    System.out.println(t + " serves " + c);
  }
  public static void main(String[] args) {
    Random rand = new Random(47);
    Queue line = new LinkedList();
    Generators.fill(line, Customer.generator(), 15);
    List tellers = new ArrayList();
    Generators.fill(tellers, Teller.generator, 4);
    for(Customer c : line)
      serve(tellers.get(rand.nextInt(tellers.size())), c);
  } 
} /* Output:
Teller 3 serves Customer 1
Teller 2 serves Customer 2
Teller 3 serves Customer 3
Teller 1 serves Customer 4
Teller 1 serves Customer 5
Teller 3 serves Customer 6
Teller 1 serves Customer 7
Teller 2 serves Customer 8
Teller 3 serves Customer 9
Teller 3 serves Customer 10
Teller 2 serves Customer 11
Teller 4 serves Customer 12
Teller 2 serves Customer 13
Teller 1 serves Customer 14
Teller 1 serves Customer 15
*///:~

Customer和Teller类都只有private的构造器,这可以强制你必须使用Generator对象。Customer有一个generator()方法,每次执行它都会生成一个新的Generate对象。我们其实不需要多个Generator对象,Teller就只创建了一个public的generator时象。在main()方法中可以看到,这两种创建Generator的方式都在fill()中用到了。
由于Customer中的generator()方法,以及Teller中的Generator对象都声明成了static的,所以它们无法作为接口的一部分,因此无法用接口这种特定的惯用法来泛化这二者。尽管如此,它们在fill()方法中都工作得很好。

在第21章中,我们还会看到关干这个排队问题的另一个版本。

泛型的一个重要好处是能够简单而安全地创建复杂的模型。例如,我们可以很容易地创建List元组:

//: generics/TupleList.java
// Combining generic types to make complex generic types.
import java.util.*;
import net.mindview.util.*;

public class TupleList<A,B,C,D>
extends ArrayList<FourTuple<A,B,C,D>> {
  public static void main(String[] args) {
    TupleList tl =
      new TupleList();
    tl.add(TupleTest.h());
    tl.add(TupleTest.h());
    for(FourTuple i: tl)
      System.out.println(i);
  }
} /* Output: (75% match)
(Vehicle@11b86e7, Amphibian@35ce36, hi, 47)
(Vehicle@757aef, Amphibian@d9f9c3, hi, 47)
*///:~

尽管这看上去有些冗长(特别是迭代器的创建),但最终还是没有用过多的代码就得到了一个相当强大的数据结构。

下面是另一个示例,它展示了使用泛型类型来构建复杂模型是多么的简单。即使每个类部是作为一个构建块创建的,但是其整个还是包含许多部分。在本例中,构建的模型是一个零售店,它包含走廊、货架和商品:

//: generics/Store.java
// Building up a complex model using generic containers.
import java.util.*;
import net.mindview.util.*;

class Product {
  private final int id;
  private String description;
  private double price;
  public Product(int IDnumber, String descr, double price){
    id = IDnumber;
    description = descr;
    this.price = price;
    System.out.println(toString());
  }
  public String toString() {
    return id + ": " + description + ", price: $" + price;
  }
  public void priceChange(double change) {
    price += change;
  }
  public static Generator generator =
    new Generator() {
      private Random rand = new Random(47);
      public Product next() {
        return new Product(rand.nextInt(1000), "Test",
          Math.round(rand.nextDouble() * 1000.0) + 0.99);
      }
    };
}

class Shelf extends ArrayList {
  public Shelf(int nProducts) {
    Generators.fill(this, Product.generator, nProducts);
  }
}    

class Aisle extends ArrayList {
  public Aisle(int nShelves, int nProducts) {
    for(int i = 0; i < nShelves; i++)
      add(new Shelf(nProducts));
  }
}

class CheckoutStand {}
class Office {}

public class Store extends ArrayList<Aisle> {
  private ArrayList checkouts =
    new ArrayList();
  private Office office = new Office();
  public Store(int nAisles, int nShelves, int nProducts) {
    for(int i = 0; i < nAisles; i++)
      add(new Aisle(nShelves, nProducts));
  }
  public String toString() {
    StringBuilder result = new StringBuilder();
    for(Aisle a : this)
      for(Shelf s : a)
        for(Product p : s) {
          result.append(p);
          result.append("\\n");
        }
    return result.toString();
  }
  public static void main(String[] args) {
    System.out.println(new Store(14, 5, 10));
  }
} /* Output:
258: Test, price: $400.99
861: Test, price: $160.99
868: Test, price: $417.99
207: Test, price: $268.99
551: Test, price: $114.99
278: Test, price: $804.99
520: Test, price: $554.99
140: Test, price: $530.99
...
*///:~

正如我们在Store.tostring()中看到的,其结果是许多层容器,但是它们是类型安全且可管理的。令人印象深刻之处是组装这个的模型十分容易,并不会成为智力挑战。

15.7 擦除的神秘之处

当你开始更深人地钻研泛型时,会发现有大量的东西初看起来是没有意义的。例如,尽管可以声明ArrayList.class,但是不能声明ArrayList.class,请考虑下面的情况:

//: generics/ErasedTypeEquivalence.java
import java.util.*;

public class ErasedTypeEquivalence {
  public static void main(String[] args) {
    Class c1 = new ArrayList().getClass();
    Class c2 = new ArrayList().getClass();
    System.out.println(c1 == c2);
  }
} /* Output:
true
*///:~

ArrayListArrayList很容易被认为是不同的类型。不同的类型在行为方面肯定不同,例如,如果尝试着将一个Integer放入ArrayList,所得到的行为(将失败)与把一个Integer放入ArrayList(将成功)所得到的行为完全不同。但是上面的程序会认为它们是相同的类型。

下面是的示例是对这个谜题的一个补充:

//: generics/LostInformation.java
import java.util.*;

class Frob {}
class Fnorkle {}
class Quark {}
class Particle {}

public class LostInformation {
  public static void main(String[] args) {
    List list = new ArrayList();
    Map map = new HashMap();
    Quark quark = new Quark();
    Particle p = new Particle();
    System.out.println(Arrays.toString(
      list.getClass().getTypeParameters()));
    System.out.println(Arrays.toString(
      map.getClass().getTypeParameters()));
    System.out.println(Arrays.toString(
      quark.getClass().getTypeParameters()));
    System.out.println(Arrays.toString(
      p.getClass().getTypeParameters()));
  }
} /* Output:
[E]
[K, V]
[Q]
[POSITION, MOMENTUM]
*///:~

根据JDK文档的描述,Class.getTypeParameters()将“返回一个TypeVariable对象数组,表示有泛型声明所声明的类型参数…”。这好像是在暗示你可能发现参数类型的信息,但是,正如你从输出中所看到的,你能够发现的只是用作参数占位符的标识符,这并非有用的信息。

因此,残酷的现实是:
在泛型代码内部,无法获得任何有关泛型参数类型的信息。

因此,你可以知道诸如类型参数标识符和泛型类型边界这类的信息——你却无法知道用来创建某个特定实例的实际的类型参数。如果你曾经是C++程序员,那么这个事实肯定让你觉得很沮丧,在使用Java泛型工作时它是必须处理的最基本的间题。

Java泛型是使用擦除来实现的,这意味着当你在使用泛型时,任何具体的类型信息都被擦除了,你唯一知遒的就是你在使用一个对象。因此ListList在运行时事实上是相同的类型。这两种形式都被擦除成它们的“原生”类型,即List。理解擦除以及应该如何处理它,是你在学习Java泛型时面临的最大障碍。这也是我们在本节将要探讨的内容。

15.7.1 C++的方式

下面是使用模版的C++示例,你将注意到用于参数化类型的语法十分相似,因为Java是受C++的启发:

//: generics/Templates.cpp
#include 
using namespace std;

template<class T> class Manipulator {
  T obj;
public:
  Manipulator(T x) { obj = x; }
  void manipulate() { obj.f(); }
};

class HasF {
public:
  void f() { cout << "HasF::f()" << endl; }
};

int main() {
  HasF hf;
  Manipulator manipulator(hf);
  manipulator.manipulate();
} /* Output:
HasF::f()
///:~

Manipulator类存储了一个类型T的对象,有意思的地方是manipulate()方法,它在obj上调用方f()。它怎么能知道f()方法是为类型参数T而存在的呢?当你实例化这个模版时,C++编译器将进行检查,因此在Manipulalor被实例化的这一刻,它看到HasF拥有一个方法f()。如果情况井非如此,就会得到一个编译期错误,这样类型安全就得到了保障。

用C++编写这种代码很简单,因为当模版被实例化时,模版代码知道其模版参数的类型。Java泛型就不同了。下面是HasF的Java版本:

//: generics/HasF.java

public class HasF {
  public void f() { System.out.println("HasF.f()"); }
} ///:~

如果我们将这个示例的其余代码都翻译成Java,那么这些代码将不能编译:

//: generics/Manipulation.java
// {CompileTimeError} (Won't compile)

class Manipulator {
  private T obj;
  public Manipulator(T x) { obj = x; }
  // Error: cannot find symbol: method f():
  public void manipulate() { obj.f(); }
}

public class Manipulation {
  public static void main(String[] args) {
    HasF hf = new HasF();
    Manipulator manipulator =
      new Manipulator(hf);
    manipulator.manipulate();
  }
} ///:~

由于有了擦除,Java编译器无法将maaipulate()必须能够在obj上调用f()这一需求映射到HasF有f()这一事实上。为了调用f(),我们必须协助泛型类,给定泛型类的边界,以此告知编译器只能接受遵循这个边界的类型。这里重用了extends关键字。由于有了边界,下面的代码就可以编译了:

//: generics/Manipulator2.java

class Manipulator2 {
  private T obj;
  public Manipulator2(T x) { obj = x; }
  public void manipulate() { obj.f(); }
} ///:~

边界声明T必须具有类型HasF或者从HasF导出的类型。如果情况确实如此,那么就可以安全地在obj上调用f()了。

我们说泛型类型参数将擦除到它的第一个边界(它可能会有多个边界,稍候你就会看到),我们还捉到了类型参数的擦除。编译器实际上会把类型参数替换为它的擦除,就像上面的示例一样。T擦除到了HasF,就好像在类的声明中用HasF替换了T一样。

你可能已经正确地观察到,在Manipulation2.java中,泛型没有贡献任何好处。只需很容易地自己去执行擦除,就可以创建出没有泛型的类:

//: generics/Manipulator3.java

class Manipulator3 {
  private HasF obj;
  public Manipulator3(HasF x) { obj = x; }
  public void manipulate() { obj.f(); }
} ///:~

这提出了很重要的一点:只有当你希望使用的类型参数比某个具体类型(以及它的所有子类型)更加“泛化”时——也就是说,当你希望代码能够跨多个类工作时,使用泛型才有所帮助。因此,类型参数和它们在有用的泛型代码中的应用,通常比简单的类替换要更复杂。但是,不能因此而认为形式的任何东西而都是有缺陷的。例如,如果某个类有一个返回T的方法,那么泛型就有所帮助,因为它们之后将返回确切的类型:

//: generics/ReturnGenericType.java

class ReturnGenericType {
  private T obj;
  public ReturnGenericType(T x) { obj = x; }
  public T get() { return obj; }
} ///:~

必须查看所有的代码,并确定它是否“足够复杂”到必须作用泛型的程度。
我们将在本章稍后介绍有关边界的更多细节。

15.7.2 迁移兼容性

为了减少潜在的关于擦除的混淆,你必须清楚地认识到这不是一个语言特性。它是Java的泛型实现中的一种折中,因为泛型不是Java语言出现时就有的组成部分,所以这种折中是必需的。这种折中会使你痛苦,因此你需要习惯它并了解为什么它会是这样。

如果泛型在Java 1.0中就已经是其一部分了,那么这个特性将不会使用擦除来实现——它将使用县体化,使类型参数保持为第一类实体,因此你就能够在类型参数上执行基于类型的语言操作和反射操作。你将在本章稍后看到,擦除减少了泛型的泛化性。泛型在Java中仍归是有用的,只是不如它们本来设想的那么有用,而原因就是擦除。

在基于擦除的实现中,泛型类型被当作第二类类型处理,即不能在某些重要的上下文环境中使用的类型。泛型类型只有在静态类型检查期间才出现,在此之后,程序中的所有泛型类型都将被擦除,替换为它们的非泛型上界。例如,诸如List这样的类型注解将被擦除为List,而普通的类型变量在未指定边界的情况下将被擦除为Object。

擦除的核心动机是它使得泛化的客户端可以用非泛化的类库来使用,反之亦然,这经常被称为“迁移兼容性”。在理想情况下,当所有事物都可以同时被泛化时,我们就可以专注于此。在现实中,即使程序员只编写泛型代码,他们也必须处理在Java SE5之前编写的非泛型类库。那些类库的作者可能从没有想过要泛化它们的代码,或者可能刚刚开始接触泛型。

因此Java泛型不仅必须支特向后兼容性,即现有的代码和类文件仍旧合法,井且继续保持其之前的含义而且还要支持迁移兼容性,使得类库接照它们自己的步调变为泛型的,并且当某个类库变为泛型时,不会破坏依赖于它的代码和应用程序。在决定这就是目标之后,Java设计者们和从事此问题相关工作的各个团队决策认为擦除是唯一可行的解决方案。通过允许非泛型代码与泛型代码共存,擦除使得这种向着泛型的迁移成为可能。

例如,假设某个应用程序具有两个类库X和Y,并且Y还要使用类库Z。随着Java SE5的出现,这个应用程序和这些类库的创建者最终可能希望迁移到泛型上。但是,当进行这种迁移时,他们有着不同动机和限制。为了实现迁移兼容性,每个类库和应用程序都必须与其他所有的部分是否使用了泛型无关。这样,它们必须不具备探测其他类库是否使用了泛型的能力。因此,某个特定的类库使用了泛型这样的证据必须被“擦除”。

如果没有某种类型的迁移途径,所有已经构建了很长时间的类库就需要与希望迁移到Java泛型上的开发者们说再见了。但是,类库是编程语言无可争议的一部分,它们对生产效率会产生最重要的影响,因此这不是一种可以接受的代价。擦除是否是最佳的或者唯一的迁移途径,还需要时间来证明。

15.7.3 擦除的问题

因此,擦除主要的正当理由是从非泛化代码到泛化代码的转变过程,以及在不破坏现有类库的情况下,将泛型融入Java语言。擦除使得现有的非泛型客户端代码能够在不改变的情况下继续使用,直至客户端准备好用泛型重写这些代码。这是一个崇高的动机,因为它不会突然间破坏所有现有的代码。

擦除的代价是显著的。泛型不能用于显式地引用运行时类型的操作之中,例如转型、instanceof操作和new表达式。因为所有关于参数的类型信息都丢失了。无论何时,当你在编写泛型代码时,必须时刻提醒自己,你只是看起来好像拥有有关参数的类型信息而已。因此,如果你编写了下面这样的代码段:

class Foo {
  T var;
}

那么,看起来当你在创建Foo实例时:

Foo f = new Foo();

class Foo中的代码应该知道现在工作于Cat之上,而泛型语法也在强烈暗示:在整个类中的各个地方,类型T都在被替换。但是事实并非如此,无论何时,当你在编写这个类的代码时,必须提醒自己:“不,它只是一个Object。”

另外,擦除和迁移兼容性意味着,使用泛型并不是强制的,尽管你可能希望这样:

//: generics/ErasureAndInheritance.java

class GenericBase {
  private T element;
  public void set(T arg) { arg = element; }
  public T get() { return element; }
}

class Derived1 extends GenericBase {}

class Derived2 extends GenericBase {} // No warning

// class Derived3 extends GenericBase {}
// Strange error:
//   unexpected type found : ?
//   required: class or interface without bounds    

public class ErasureAndInheritance {
  @SuppressWarnings("unchecked")
  public static void main(String[] args) {
    Derived2 d2 = new Derived2();
    Object obj = d2.get();
    d2.set(obj); // Warning here!
  }
} ///:~

Derived2继承自GenericBase,但是没有任何泛型参数,而编译器不会发出任何警告。警告在Set()被调用时才会出现。

为了关闭警告,Java提供了一个注解,我们可以在列表中看到它(这个注解在Java SE5之前的版本中不支持):

    @suppressWarnings("unchecked")

注意,这个往解被放置在可以产生这类警告的方法之上,而不是整个类上。当你要关闭警告时,最好是尽量地“聚焦”,这样就不会因为过于宽泛地关闭警告,而导致意外地遮蔽掉真正的问题。

可以推断,Derived3产生的错误意味着编译器期望得到一个原生基类。

当你希望将类型参数不要仅仅当作Object处理时,就需要付出额外努力来管理边界,并且与在C++, Ada和Eiffel这样的语言中获得参数化类型相比,你需要付出多得多的努力来获得少得多的回报。这并不是说,对于大多数的编程问题而言,这些语言通常都会比Java更得心应手,这只是说,它们的参数化类型机制比Java的更灵活、更强大。

15.7.4 边界处的动作

正是因为有了擦除,我发现泛型最令人困惑的方面源自这样一个事实,即可以表示没有任何意义的事物。例如:

//: generics/ArrayMaker.java
import java.lang.reflect.*;
import java.util.*;

public class ArrayMaker<T> {
  private Class kind;
  public ArrayMaker(Class kind) { this.kind = kind; }
  @SuppressWarnings("unchecked")
  T[] create(int size) {
    return (T[])Array.newInstance(kind, size);
  }
  public static void main(String[] args) {
    ArrayMaker stringMaker =
      new ArrayMaker(String.class);
    String[] stringArray = stringMaker.create(9);
    System.out.println(Arrays.toString(stringArray));
  }
} /* Output:
[null, null, null, null, null, null, null, null, null]
*///:~

即使kind被存储为Class,擦除也意味着它实际将被存储为Class,没有任何参数。因此,当你在使用它时,例如在创建数组时,Array.newInstance()实际上并未拥有kind所蕴含的类型信息,因此这不会产生具体的结果。所以必须转型,这将产生一条令你无法满意的警告。

注意,对于在泛型中创建数组,使用Array.newInstance()是推荐的方式。

如果我们要创建一个容器而不是数组,情况就有些不同了:

//: generics/ListMaker.java
import java.util.*;

public class ListMaker<T> {
  List create() { return new ArrayList(); }
  public static void main(String[] args) {
    ListMaker stringMaker= new ListMaker();
    List stringList = stringMaker.create();
  }
} ///:~

编译器不会给出任何警告,尽管我们(从擦除中)知道在create()内部的new ArrayList中的被移除了——在运行时,这个类的内部没有任何,因此这看起来毫无意义。但是如果你遵从这种思路,并将这个表达式改为new ArrayList(),编译器就会给出警告。

在本例中,这是否真的毫无意义呢?如果返回list之前,将某些对象放入其中。就像下面这样,情况又会如何呢?

//: generics/FilledListMaker.java
import java.util.*;

public class FilledListMaker<T> {
  List create(T t, int n) {
    List result = new ArrayList();
    for(int i = 0; i < n; i++)
      result.add(t);
    return result;
  }
  public static void main(String[] args) {
    FilledListMaker stringMaker =
      new FilledListMaker();
    List list = stringMaker.create("Hello", 4);
    System.out.println(list);
  }
} /* Output:
[Hello, Hello, Hello, Hello]
*///:~

即使编译器无法知道有关create()中的T的任何信息,但是它仍旧可以在编译期确保你放置到result中的对象具有T类型,使共适合ArrayList。因此,即使擦除在方法或类内部移除了有关实际类型的信息,编译器仍旧可以确保在方法或类中使用的类型的内部一致性。

因为擦除在方法体中移除了类型信息,所以在运行时的问题就是边界:即对象进入和离开方法的地点。这些正是编译器在编译期执行类型检查并插入转型代码的地点。请考虑下面的非泛型示例:

//: generics/SimpleHolder.java

public class SimpleHolder {
  private Object obj;
  public void set(Object obj) { this.obj = obj; }
  public Object get() { return obj; }
  public static void main(String[] args) {
    SimpleHolder holder = new SimpleHolder();
    holder.set("Item");
    String s = (String)holder.get();
  }
} ///:~

如果用Javap -c SimpleHolder反编译这个类,就可以得到下面的(经过编辑的)内容:

public void set (java.lang.Object):
0: aload_0
1: aload_1
2: putfield #2; //Field obj:Object
5: return

public java.lang.Object get():
0: aload_0
1: getfield #2; //Field obj:Object
4: areturn

public static void main(java.lang.String[]):
0: new #3; //class SimpleHolder
3: dup
4: invokespecial #4; //Method ":()v
7: a-store_1
8: aload_ 1
9: ldc #5; //String Item;
11: invokevitrual #6; //Method set:(Object;)V
14: aload_1
15: invokevirtual #7: //Method get:()Object;
18: checkcast #8; //class java/lang/String
21: astore_2
22: return

get()和set()方法将直接存储和产生值,而转型是在调用get()的时候接受检查的。

现在将泛型合并到上面的代码中:

//: generics/GenericHolder.java

public class GenericHolder<T> {
  private T obj;
  public void set(T obj) { this.obj = obj; }
  public T get() { return obj; }
  public static void main(String[] args) {
    GenericHolder holder =
      new GenericHolder();
    holder.set("Item");
    String s = holder.get();
  }
} ///:~

从get()返回之后的转型消失了,但是我们还知道传递给set()的值在编译期会接受检查。下面是相关的字节码:

public void set (java.lang.Object):
0: aload_0
1: aload_1
2: putfield #2; //Field obj:Object
5: return

public java.lang.Object get():
0: aload_0
1: getfield #2; //Field obj:Object
4: areturn

public static void main(java.lang.String[]):
0: new #3; //class GenericHolder
3: dup
4: invokespecial #4; //Method ":()v
7: a-store_1
8: aload_ 1
9: ldc #5; //String Item;
11: invokevitrual #6; //Method set:(Object;)V
14: aload_1
15: invokevirtual #7: //Method get:()Object;
18: checkcast #8; //class java/lang/String
21: astore_2
22: return

所产生的字节码是相同的。对进入set()的类型进行检查是不需要的,因为这将由编译器执行。而对从get()返回的值进行转型仍旧是需要的,但这与你自己必须执行的操作是一样的——此处它将由编译器自动插入,因此你写人(和读取)的代码的噪声将更小,

由于所产生的get()和set()的字节码相同,所以在泛型中的所有动作都发生在边界处——对传递进来的值进行额外的编译期检查,并插入对传递出去的值的转型。这有助于澄清对擦除的混淆,记住,“边界就是发生动作的地方”。

15.8 擦除的补偿

正如我们看到的,擦除丢失了在泛型代码中执行某些操作的能力。任何在运行时需要知道确切类型信息的操作都将无法工作:

//: generics/Erased.java
// {CompileTimeError} (Won't compile)

public class Erased<T> {
  private final int SIZE = 100;
  public static void f(Object arg) {
    if(arg instanceof T) {}          // Error
    T var = new T();                 // Error
    T[] array = new T[SIZE];         // Error
    T[] array = (T)new Object[SIZE]; // Unchecked warning
  }
} ///:~

偶尔可以绕过这些问题来编程,但是有时必须通过引入类型标签来对擦除进行补偿。这意味着你需要显式地传递你的类型的Class对象,以便你可以在类型表达式中使用它。

例如,在前面示例中对使用instanceof的尝试最终失败了,因为其类型信息已经被擦除了。如果引入类型标签,就可以转而使用动态的isInstance():

//: generics/ClassTypeCapture.java

class Building {}
class House extends Building {}

public class ClassTypeCapture<T> {
  Class kind;
  public ClassTypeCapture(Class kind) {
    this.kind = kind;
  }
  public boolean f(Object arg) {
    return kind.isInstance(arg);
  }    
  public static void main(String[] args) {
    ClassTypeCapture ctt1 =
      new ClassTypeCapture(Building.class);
    System.out.println(ctt1.f(new Building()));
    System.out.println(ctt1.f(new House()));
    ClassTypeCapture ctt2 =
      new ClassTypeCapture(House.class);
    System.out.println(ctt2.f(new Building()));
    System.out.println(ctt2.f(new House()));
  }
} /* Output:
true
true
false
true
*///:~

编译器将确保类型标签可以匹配泛型参数。

15.8.1 创建类型实例

在Erased.java中对创建一个new T()的尝试将无法实现,部分原因是因为擦除,而另一部分原因是因为编译器不能验证T具有默认(无参)构造器。但是在C++中,这种操作很自然、很直观,并且很安全(它是在编译期受到检查的):

//: generics/InstantiateGenericType.cpp
// C++, not Java!

template<class T> class Foo {
  T x; // Create a field of type T
  T* y; // Pointer to T
public:
  // Initialize the pointer:
  Foo() { y = new T(); }
};

class Bar {};

int main() {
  Foo fb;
  Foo<int> fi; // ... and it works with primitives
} ///:~

Jaya中的解决方案是传递一个工厂对象,并使用它来创建新的实例。最便利的工厂对象就是Class对象,因此如果使用类型标签,那么你就可以使用newInstance来创建这个类型的新对象:

//: generics/InstantiateGenericType.java
import static net.mindview.util.Print.*;

class ClassAsFactory {
  T x;
  public ClassAsFactory(Class kind) {
    try {
      x = kind.newInstance();
    } catch(Exception e) {
      throw new RuntimeException(e);
    }
  }
}

class Employee {}    

public class InstantiateGenericType {
  public static void main(String[] args) {
    ClassAsFactory fe =
      new ClassAsFactory(Employee.class);
    print("ClassAsFactory succeeded");
    try {
      ClassAsFactory fi =
        new ClassAsFactory(Integer.class);
    } catch(Exception e) {
      print("ClassAsFactory failed");
    }
  }
} /* Output:
ClassAsFactory succeeded
ClassAsFactory failed
*///:~

这可以编译,但是会因ClassAsFactory而失败,因为Integer没有任何默认的构造器。因为这个错误不是在编译期捕获的,所以Sun的伙计们对这种方式井不赞成,他们建议使用显式的工厂,井将限制其类型,使得只能接受实现了这个工厂的类:

//: generics/FactoryConstraint.java

interface FactoryI {
  T create();
}

class Foo2 {
  private T x;
  public > Foo2(F factory) {
    x = factory.create();
  }
  // ...
}

class IntegerFactory implements FactoryI {
  public Integer create() {
    return new Integer(0);
  }
}    

class Widget {
  public static class Factory implements FactoryI<Widget> {
    public Widget create() {
      return new Widget();
    }
  }
}

public class FactoryConstraint {
  public static void main(String[] args) {
    new Foo2(new IntegerFactory());
    new Foo2(new Widget.Factory());
  }
} ///:~

注意,这确实只是传递Class的一种变体。两种方式都传递了工厂对象,Class碰巧是内建的工厂对象,而上面的方式创建了一个显式的工厂对象,但是你却获得了编译期检查。

另一种方式是模板方法设计模式。在下面的示例中,get()是模板方法,而create()是在子类中定义的、用来产生子类类型的对象:

//: generics/CreatorGeneric.java

abstract class GenericWithCreate {
  final T element;
  GenericWithCreate() { element = create(); }
  abstract T create();
}

class X {}

class Creator extends GenericWithCreate {
  X create() { return new X(); }
  void f() {
    System.out.println(element.getClass().getSimpleName());
  }
}    

public class CreatorGeneric {
  public static void main(String[] args) {
    Creator c = new Creator();
    c.f();
  }
} /* Output:
X
*///:~

15.8.2泛型数组

正如你在Erased.java中所见,不能创建泛型数组。一般的解决方案是在任何想要创建泛型数组的地方都使用ArrayList:

//: generics/ListOfGenerics.java
import java.util.*;

public class ListOfGenerics<T> {
  private List array = new ArrayList();
  public void add(T item) { array.add(item); }
  public T get(int index) { return array.get(index); }
} ///:~

这里你将获得数组的行为,以及由泛型提供的编译期的类型安全。

有时,你仍旧希璧创建泛型类型的数组(例如,ArrayList内部使用的是数组)。有趣的是可以按照编译器喜欢的方式来定义一个引用,例如:

//: generics/ArrayOfGenericReference.java

class Generic {}

public class ArrayOfGenericReference {
  static Generic[] gia;
} ///:~

编译器将接受这个程序,而不会产生任何警告。但是,永远都不能创建这个确切类型的数组(包括类型参数),因此这有一点令人困惑。既然所有数组无论它们持有的类型如何,都具有相同的结构(每个数组槽位的尺寸和数组的布局),那么看起来你应该能够创建一个Object数组,并将其转型为所希望的数组类型。事实上这可以编译,但是不能运行,它将产生ClassCaseException:

//: generics/ArrayOfGeneric.java

public class ArrayOfGeneric {
  static final int SIZE = 100;
  static Generic[] gia;
  @SuppressWarnings("unchecked")
  public static void main(String[] args) {
    // Compiles; produces ClassCastException:
    //! gia = (Generic[])new Object[SIZE];
    // Runtime type is the raw (erased) type:
    gia = (Generic[])new Generic[SIZE];
    System.out.println(gia.getClass().getSimpleName());
    gia[0] = new Generic();
    //! gia[1] = new Object(); // Compile-time error
    // Discovers type mismatch at compile time:
    //! gia[2] = new Generic();
  }
} /* Output:
Generic[]
*///:~

问题在于数组将跟踪它们的实际类型,而这个类型是在数组被创建时确定的,因此,即使gia已经被转型为Generic[],但是这个信息只存在干编译期(如果没有@Suppress Warnings注解,你将得到有关这个转型的警告)。在运行时,它仍将引发问题。成功创建泛型数组的唯一方式就是创建一个被擦除类型的新数组,然后对其转型。

让我们看一个更复杂的示例。考虑一个简单的泛型数组包装器:

//: generics/GenericArray.java

public class GenericArray<T> {
  private T[] array;
  @SuppressWarnings("unchecked")
  public GenericArray(int sz) {
    array = (T[])new Object[sz];
  }
  public void put(int index, T item) {
    array[index] = item;
  }
  public T get(int index) { return array[index]; }
  // Method that exposes the underlying representation:
  public T[] rep() { return array; }    
  public static void main(String[] args) {
    GenericArray gai =
      new GenericArray(10);
    // This causes a ClassCastException:
    //! Integer[] ia = gai.rep();
    // This is OK:
    Object[] oa = gai.rep();
  }
} ///:~

与前面相同,我们井不能声明T[] array=new T[sz],因此我们创建了一个对象数组,然后将其转型。
rep()方法将返回T[],它在main()中将用于gai,因此应该是Integer[],但是如果调用它,并尝试着将结果作为Integer[]引用来捕获,就会得到ClassCastException,这还是因为实际的运行时类型是Objet[]。

如果在注释掉@Suppresswarniugs注解之后再编译GenericArray.java ,编译器就会产生警告:

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

在这种情况下,我们将只获得单个的警告,并且相信这事关转型。但是如果真的想要确定是否是这么回事,就应该用.Xlint:unchecked来编译:

GenerieArray.java:7: warning:[unchecked] unchecked cast
found : java.lang.Object[]
required: T[]
array = (T[])new Object[sz];
                  ^                      
1 warning

这确实是对转型的抱怨。因为警告会变得令人迷惑,所以一旦我们验证某个特定警告是可预期的,那么我们的上策就是用@SuppressWarnings关闭它。通过这种方式,当警告确实出现时,我们就可以真正地展开对它的调查了。

因为有了擦除,数组的运行时类型就只能是Object[]。如果我们立即将其转型为T[],那么在编译期该数组的实际类型就将丢失,而编译器可能会错过某些潜在的错误检查。正因为这样,最好是在集合内部使用Object[],然后当你使用数组元素时,添加一个对T的转型。让我们看着这是如何作用于GenericArray.java示例的:

//: generics/GenericArray2.java

public class GenericArray2<T> {
  private Object[] array;
  public GenericArray2(int sz) {
    array = new Object[sz];
  }
  public void put(int index, T item) {
    array[index] = item;
  }
  @SuppressWarnings("unchecked")
  public T get(int index) { return (T)array[index]; }
  @SuppressWarnings("unchecked")
  public T[] rep() {
    return (T[])array; // Warning: unchecked cast
  }    
  public static void main(String[] args) {
    GenericArray2 gai =
      new GenericArray2(10);
    for(int i = 0; i < 10; i ++)
      gai.put(i, i);
    for(int i = 0; i < 10; i ++)
      System.out.print(gai.get(i) + " ");
    System.out.println();
    try {
      Integer[] ia = gai.rep();
    } catch(Exception e) { System.out.println(e); }
  }
} /* Output: (Sample)
0 1 2 3 4 5 6 7 8 9
java.lang.ClassCastException: [Ljava.lang.Object; cannot be cast to [Ljava.lang.Integer;
*///:~

初看起来,这好像没多大变化,只是转型挪了地方。如果没有@Suppresswarnings注解,你仍旧会得到unchecked告。但是,现在的内部表示是Object[]而不是T[]。当get()被调用时,它将对象转型为T,这实际上是正确的类型,因此这是安全的。然而,如果你调用rep() ,它还是尝试着将Object[]转型为T[],这仍旧是不正确的,将在编译期产生警告,在运行时产生异常。因此,没有任何方式可以推翻底层的数组类型,它只能是Object[]。在内部将array当作Object[]而不是T[]处理的优势是:我们不太可能忘记这个数组的运行时类型,从而意外地引入缺陷(尽管大多数也可能是所有这类缺陷都可以在运行时快速地探测到)。

对于新代码,应该传递一个类型标记。在这种情况下,GenericArray看起来会像下面这样:

//: generics/GenericArrayWithTypeToken.java
import java.lang.reflect.*;

public class GenericArrayWithTypeToken<T> {
  private T[] array;
  @SuppressWarnings("unchecked")
  public GenericArrayWithTypeToken(Class type, int sz) {
    array = (T[])Array.newInstance(type, sz);
  }
  public void put(int index, T item) {
    array[index] = item;
  }
  public T get(int index) { return array[index]; }
  // Expose the underlying representation:
  public T[] rep() { return array; }    
  public static void main(String[] args) {
    GenericArrayWithTypeToken gai =
      new GenericArrayWithTypeToken(
        Integer.class, 10);
    // This now works:
    Integer[] ia = gai.rep();
  }
} ///:~

类型标记Class被传递到构造器中,以便从擦除中恢复,使得我们可以创建需要的实际类型的数组,尽管从转型中产生的警告必须用@Suppresswarnings压制住。一旦我们获得了实际类型。就可以返回它,并获得想要的结果,就像在main()中看到的那样。该数组的运行时类型是确切类型T[]。

遗憾的是,如果查看java SE5标准类库中的源代码,你就会看到从Object数组到参数化类型的转型遍及各处。例如,下面是经过整理和简化之后的从Collection中复制ArrayList的构造器:

public ArrayList(Collection c) {
    size = c.size();
    elementData = (E[])new Object[size];
    c.toArray(elementData);
}

如果你通读ArrayList.java,就会发现它充满了这种转型。如果我们编译它,又会发生什么呢?

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

可以十分肯定,标准类库会产生大量的警告。如果你曾经用过C++,特别是ANSI C之前的版本,你就会记得警告的特殊效果:当你发现可以忽略它们时,你就可以忽略。正是因为这个原因,最好是从编译器中不要发出任何消息,除非程序员必须对其进行响应。

Neal Gafter (Java SE5的领导开发者之一)在他的博客中指出,在重写Java库时,他十分懒散,而我们不应该像他那样。Neal还指出,在不破坏现有接口的情况下,他将无法修改某些Java类库代码。因此,即使在Java类库源代码中出现了某些惯用法,也不能表示这就是正确的解决之道。当查看类库代码时,你不能认为它就是应该在自己的代码中遵循的示例。

15.9 边界

本章前面简单地介绍过边界。边界使得你可以在用于泛型的参数类型上设置限制条件。尽管这使得你可以强制规定泛型可以应用的类型,但是其潜在的一个更重要的效果是你可以按照自己的边界类型来调用方法。

因为擦出移除了类型信息,所以,可以用无界泛型参数调用的方法只是哪些可以用Object调用的方法。但是,如果能够将这个参数限制为某个类型子集,那么你就可以用这些类型子集来调用方法。为了执行这种限制,Java泛型重用了extends关键字。对你来说有一点很重要,即要理解extends关键字在泛型边界上下文环境中和在普通情况下所具有的意义是完全不同的。下面的示例展示了边界的基本要素:

//: generics/BasicBounds.java

interface HasColor { java.awt.Color getColor(); }

class Colored {
  T item;
  Colored(T item) { this.item = item; }
  T getItem() { return item; }
  // The bound allows you to call a method:
  java.awt.Color color() { return item.getColor(); }
}

class Dimension { public int x, y, z; }

// This won't work -- class must be first, then interfaces:
// class ColoredDimension {

// Multiple bounds:
class ColoredDimension {
  T item;
  ColoredDimension(T item) { this.item = item; }
  T getItem() { return item; }
  java.awt.Color color() { return item.getColor(); }
  int getX() { return item.x; }
  int getY() { return item.y; }
  int getZ() { return item.z; }
}

interface Weight { int weight(); }    

// As with inheritance, you can have only one
// concrete class but multiple interfaces:
class Solid {
  T item;
  Solid(T item) { this.item = item; }
  T getItem() { return item; }
  java.awt.Color color() { return item.getColor(); }
  int getX() { return item.x; }
  int getY() { return item.y; }
  int getZ() { return item.z; }
  int weight() { return item.weight(); }
}

class Bounded
extends Dimension implements HasColor, Weight {
  public java.awt.Color getColor() { return null; }
  public int weight() { return 0; }
}    

public class BasicBounds {
  public static void main(String[] args) {
    Solid solid =
      new Solid(new Bounded());
    solid.color();
    solid.getY();
    solid.weight();
  }
} ///:~

你可能已经观察到了,BasiicBounds.java看上去包含可以通过继承消除的冗余。下面,可以看到如何在继承的每个层次上添加边界限制:

//: generics/InheritBounds.java

class HoldItem {
  T item;
  HoldItem(T item) { this.item = item; }
  T getItem() { return item; }
}

class Colored2 extends HoldItem {
  Colored2(T item) { super(item); }
  java.awt.Color color() { return item.getColor(); }
}

class ColoredDimension2
extends Colored2 {
  ColoredDimension2(T item) {  super(item); }
  int getX() { return item.x; }
  int getY() { return item.y; }
  int getZ() { return item.z; }
}

class Solid2
extends ColoredDimension2 {
  Solid2(T item) {  super(item); }
  int weight() { return item.weight(); }
}

public class InheritBounds {
  public static void main(String[] args) {
    Solid2 solid2 =
      new Solid2(new Bounded());
    solid2.color();
    solid2.getY();
    solid2.weight();
  }
} ///:~

HoldItem直接持有一个对象,因此这种行为被继承到了Colored2中,它也要求其参数与HasColor一致。ColoredDimension2和Solid2进一步扩展了这个层次结构,并在每个层次上都添加了边界。现在这些方法被继承,因而不必在每个类中重复。

下面是具有更多层次的示例:

//: generics/EpicBattle.java
// Demonstrating bounds in Java generics.
import java.util.*;

interface SuperPower {}
interface XRayVision extends SuperPower {
  void seeThroughWalls();
}
interface SuperHearing extends SuperPower {
  void hearSubtleNoises();
}
interface SuperSmell extends SuperPower {
  void trackBySmell();
}

class SuperHero {
  POWER power;
  SuperHero(POWER power) { this.power = power; }
  POWER getPower() { return power; }
}

class SuperSleuth
extends SuperHero {
  SuperSleuth(POWER power) { super(power); }
  void see() { power.seeThroughWalls(); }
}

class CanineHero
extends SuperHero {
  CanineHero(POWER power) { super(power); }
  void hear() { power.hearSubtleNoises(); }
  void smell() { power.trackBySmell(); }
}

class SuperHearSmell implements SuperHearing, SuperSmell {
  public void hearSubtleNoises() {}
  public void trackBySmell() {}
}

class DogBoy extends CanineHero {
  DogBoy() { super(new SuperHearSmell()); }
}

public class EpicBattle {
  // Bounds in generic methods:
  static 
  void useSuperHearing(SuperHero hero) {
    hero.getPower().hearSubtleNoises();
  }
  static 
  void superFind(SuperHero hero) {
    hero.getPower().hearSubtleNoises();
    hero.getPower().trackBySmell();
  }
  public static void main(String[] args) {
    DogBoy dogBoy = new DogBoy();
    useSuperHearing(dogBoy);
    superFind(dogBoy);
    // You can do this:
    List audioBoys;
    // But you can't do this:
    // List dogBoys;
  }
} ///:~

注意,通配符〔我们下面将要学习)被限制为单一边界。

15.10通配符

你已经在第11章中看到了一些使用通配符的示例——在泛型参数表达式中的问号,在第14章中这种示例更多。本节将更深人地探讨这个问题。

我们开始入手的示例要展示数组的一种特殊行为:可以向导出类型的数组赋予基类型的数组引用:

//: generics/CovariantArrays.java

class Fruit {}
class Apple extends Fruit {}
class Jonathan extends Apple {}
class Orange extends Fruit {}

public class CovariantArrays {
  public static void main(String[] args) {
    Fruit[] fruit = new Apple[10];
    fruit[0] = new Apple(); // OK
    fruit[1] = new Jonathan(); // OK
    // Runtime type is Apple[], not Fruit[] or Orange[]:
    try {
      // Compiler allows you to add Fruit:
      fruit[0] = new Fruit(); // ArrayStoreException
    } catch(Exception e) { System.out.println(e); }
    try {
      // Compiler allows you to add Oranges:
      fruit[0] = new Orange(); // ArrayStoreException
    } catch(Exception e) { System.out.println(e); }
  }
} /* Output:
java.lang.ArrayStoreException: Fruit
java.lang.ArrayStoreException: Orange
*///:~

main()中的第一行创建了一个Apple数组,井将其赋值给一个Fruit数组引用。这是有意义的,因为Apple也是一种Fruit,因此Apple数组应该也是一个Fruit数组。

但是,如果实际的数组类型是Apple[],你应该只能在其中放置Apple或Apple的子类型,这在编译期和运行时都可以工作。但是请注意,编译器允许你将Fruit放置到这个数组中,这对于编译器来说是有意义的,因为它有一个Fruit[]引用——它有什么理由不允许将Fruit对象或名任何从Fruit继承出来的对象(例如Orange),放置到这个数组中呢?因此,在编译期,这是允许的。但是,运行时的数组机制知道它处理的是Apple[],因此会在向数组中放置异构类型时抛出异常。

实际上,向上转型不合适用在这里。你真正做的是将一个数组赋值给另一个数组。数组的行为应该是它可以持有其他对象,这里只是因为我们能够向上转型而已,所以很明显,数组对象可以保留有关它们包含的对象类型的规则。就好像数组对它们持有的对象是有意识的,因此在编译期检查和运行时检查之间,你不能滥用它们。

对数组的这种赋值并不是那么可怕,因为在运行时可以发现你已经插入了不正确的类型。但是泛型的主要目标之一是将这种错误检测移入到编译期。因此当我们试图使用泛型容器来代替数组时,会发生什么呢?

//: generics/NonCovariantGenerics.java
// {CompileTimeError} (Won't compile)
import java.util.*;

public class NonCovariantGenerics {
  // Compile Error: incompatible types:
  List flist = new ArrayList();
} ///:~

尽管你在第一次阅读这段代码时会认为:“不能将一个Apple容器赋值给一个Fruit容器”。别忘了,泛型不仅和容器相关。正确的说法是:“不能把一个涉及Apple的泛型赋给一个涉及Fruit的泛型”。如果就像在数组的情况中一样,编译器对代码的了解足够多,可以确定所涉及到的容器,那么它可能会留下一些余地。但是它不知透任何有关这方面的信息,因此它拒绝向上转型。然而实际上这根本不是向上转型——Apple的List不是Fruit的List。Apple的List将持有Apple和Apple的子类型,而Fruit的List将持有任何类型的Fruit,诚然,这包括Apple在内,但是它不是一个Apple的List,它仍旧是Fruit的List。Apple的List在类型上不等价于Fruit的List,即使Apple是一种Fruit类型。

真正的问题是我们在谈论容器的类型。而不是容器持有的类型。与数组不同,泛型没有内建的协变类型。这是因为数组在语言中是完全定义的,因此可以内建了编译期和运行时的检查,但是在使用泛型时,编译器和运行时系统都不知道你想用类型做些什么,以及该采用什么样的规则。

但是,有时你想要在两个类型之间建立某种类型的向上转型关系,这正是通配符所允许的:

//: generics/GenericsAndCovariance.java
import java.util.*;

public class GenericsAndCovariance {
  public static void main(String[] args) {
    // Wildcards allow covariance:
    List flist = new ArrayList();
    // Compile Error: can't add any type of object:
    // flist.add(new Apple());
    // flist.add(new Fruit());
    // flist.add(new Object());
    flist.add(null); // Legal but uninteresting
    // We know that it returns at least Fruit:
    Fruit f = flist.get(0);
  }
} ///:~

flist类型现在是List,你可以将其读作“具有任何从Fruit继承的类型的列表”。但是,这实际上并不意味着这个List将持有任何类型的Fruit。通配符引用的是明确的类型,因此它意味着“某种first引用没有指定的具体类型”。因此这个被赋值的List必须持有诸如Fruit或Apple这样的某种指定类型,但是为了向上转型为flist,这个类型是什么并没有人关心。

如果唯一的限制是这个List要持有某种具体的Fruit或Fruit的子类型,但是你实际上并不关心它是什么,那么你能用这样的List做什么呢?如果不知道List持有什么类型,那么你怎样才能安全地向其中添加对象呢?就像在CovariantArrays.java中向上转型数组一样,你不能,除非编译器而不是运行时系统可以阻止这种操作的发生。你很快就会发现这一问题。

你可能会认为,事情变得有点走极端了,因为现在你甚至不能向刚刚声明过将持有Apple对象的List中放置一个Apple对象了。是的,但是编译器并不知道这一点。List可以合法地指向一个List。一旦执行这种类型的向上转型,你就将丢失掉向其中传递任何对象的能力,甚至是传递Object也不行。

另一方面,如果你调用一个返回Fruit的方法,则是安全的,因为你知道在这个List中的任何对象至少具有Fruit类型,因此编译器将允许这么做。

15.10.1 编译器有多聪明

现在,你可能会猜想自己被阻止去调用任何接受参数的方法,但是请考虑下面的程序:

//: generics/CompilerIntelligence.java
import java.util.*;

public class CompilerIntelligence {
  public static void main(String[] args) {
    List flist =
      Arrays.asList(new Apple());
    Apple a = (Apple)flist.get(0); // No warning
    flist.contains(new Apple()); // Argument is 'Object'
    flist.indexOf(new Apple()); // Argument is 'Object'
  }
} ///:~

你可以看到,对contains()和indexOf()的调用,这两个方法都接受Apple对象作为参数,而这些调用都可以正常执行。这是否意味着编译器实际上将检查代码,以查着是否有某个特定的方法修改了它的对象?

通过查看ArrsyList的文档,我们可以发现,编译器并没有这么聪明。尽管add()将接受一个具有泛型参数类型的参数,但是contains()和indexOf()将接受Object类型的参数。因此当你指定一个ArrayList时,add()的参数就变成了“? Extends Fruit”。从这个描述中,编译器并不能了解这里需要Fruit的哪个具体子类型,因此它不会接受任何类型的Fruit。如果先将Apple向上转型为Fruit,也无关紧要——编译器将直接拒绝对参数列表中涉及通配符的方法(例如add())的调用。

在使用contains()和indexOf()时,参数类型是Object,因此不涉及任何通配符,而编译器也将允许这个调用。这意味着将由泛型类的设计者来决定哪些调用是“安全的”,并使用Object类型作为其参数类型。为了在类型中使用了通配符的情况下禁止这类调用,我们需要在参数列表中使用类型参数。

可以在一个非常简单的Holder类中看到这一点:

//: generics/Holder.java

public class Holder<T> {
  private T value;
  public Holder() {}
  public Holder(T val) { value = val; }
  public void set(T val) { value = val; }
  public T get() { return value; }
  public boolean equals(Object obj) {
    return value.equals(obj);
  }    
  public static void main(String[] args) {
    Holder Apple = new Holder(new Apple());
    Apple d = Apple.get();
    Apple.set(d);
    // Holder Fruit = Apple; // Cannot upcast
    Holder fruit = Apple; // OK
    Fruit p = fruit.get();
    d = (Apple)fruit.get(); // Returns 'Object'
    try {
      Orange c = (Orange)fruit.get(); // No warning
    } catch(Exception e) { System.out.println(e); }
    // fruit.set(new Apple()); // Cannot call set()
    // fruit.set(new Fruit()); // Cannot call set()
    System.out.println(fruit.equals(d)); // OK
  }
} /* Output: (Sample)
java.lang.ClassCastException: Apple cannot be cast to Orange
true
*///:~

Holder有一个接受T类型对象的set()方法,一个get()方法,以及一个接受Object对象的equals()方法。正如你已经看到的,如果创建了一个Holder,不能将其向上转型为Holder,但是可以将其向上转型为Holder,如果调用get(),它只会返回一个Fruit——这就是在给定“任何扩展自Fruit的对象”这一边界之后,它所能知道的一切了。
如果能够了解更多的信息,那么你可以转型到某种具体的Fruit类型,而这不会导致任何警告,但是你存在着得到ClassCastException的风险。set()方法不能工作于Apple或Fruit,因为set()的参数也是”? Extends Fruit”,这意味着它可以是任何事物,而编译器无法验证“任何事物”的类型安全性。
但是,equals()方法工作良好,因为它将接受Object类型而井非T类型的参数。因此,编译器只关注传递进来和要返回的对象类型,它井不会分析代码,以查看是否执行了任何实际的写入和读取操作。

15.10.2 逆变

还可以走另外一条路,即使用超类型通配符。这里,可以声明通配符是由某个特定类的任何基类来界定的,方法是指定,甚至或者使用类型参数:(尽管你不能对泛型参数给出一个超类型边界;即不能声明)。这使得你可以安全地传递一个类型对象到泛型类型中。因此,有了超类型通配符,就可以向Collection写入了:

//: generics/SuperTypeWildcards.java
import java.util.*;

public class SuperTypeWildcards {
  static void writeTo(Listsuper Apple> apples) {
    apples.add(new Apple());
    apples.add(new Jonathan());
    // apples.add(new Fruit()); // Error
  }
} ///:~

参数Apple是Apple的某种基类型的List,这样你就知道向其中添加Apple或Apple的子类型是安全的。但是,既然Apple是下界,那么你可以知道向这样的List中添加Fruit是不安全的,因为这将使这个List敞开口子,从而可以向其中添加非Apple类型的对象,而这是违反静态类型安全的。

因此你可能会根据如何能够向一个泛型类型“写人”(传递给一个方法),以及如何能够从一个泛型类型中“读取”(从一个方法中返回),来着手思考子类型和超类型边界。
超类型边界放松了在可以向方法传递的参数上所作的限制:

//: generics/GenericWriting.java
import java.util.*;

public class GenericWriting {
  static  void writeExact(List list, T item) {
    list.add(item);
  }
  static List apples = new ArrayList();
  static List fruit = new ArrayList();
  static void f1() {
    writeExact(apples, new Apple());
    // writeExact(fruit, new Apple()); // Error:
    // Incompatible types: found Fruit, required Apple
  }
  static  void
  writeWithWildcard(Listsuper T> list, T item) {
    list.add(item);
  }    
  static void f2() {
    writeWithWildcard(apples, new Apple());
    writeWithWildcard(fruit, new Apple());
  }
  public static void main(String[] args) { f1(); f2(); }
} ///:~

writeExact()方法使用了一个确切参数类型(无通配符)。在f1()中,可以看到这工作良好——只要你只向List中放置Apple。但是,writeExact()不允许将Apple放置到List中,即使知道这应该是可以的。

在writeWithWildcard()中,其参数现在是List,因此这个List将持有从T导出的某种具体类型,这样就可以安全地将一个T类型的对象或者从T导出的任何对象作为参数传递给List的方法。在f2()中可以看到这一点,在这个方法中我们仍旧可以像前面那样,将Apple放置到List中,但是现在我们还可以如你所期望的那样,将Apple放置到List中。

我们可以执行下面这个相同的类型分析。作为对协变和通配符的一个复习:

//: generics/GenericReading.java
import java.util.*;

public class GenericReading {
  static  T readExact(List list) {
    return list.get(0);
  }
  static List apples = Arrays.asList(new Apple());
  static List fruit = Arrays.asList(new Fruit());
  // A static method adapts to each call:
  static void f1() {
    Apple a = readExact(apples);
    Fruit f = readExact(fruit);
    f = readExact(apples);
  }
  // If, however, you have a class, then its type is
  // established when the class is instantiated:
  static class Reader {
    T readExact(List list) { return list.get(0); }
  }    
  static void f2() {
    Reader fruitReader = new Reader();
    Fruit f = fruitReader.readExact(fruit);
    // Fruit a = fruitReader.readExact(apples); // Error:
    // readExact(List) cannot be
    // applied to (List).
  }
  static class CovariantReader {
    T readCovariant(List list) {
      return list.get(0);
    }
  }
  static void f3() {
    CovariantReader fruitReader =
      new CovariantReader();
    Fruit f = fruitReader.readCovariant(fruit);
    Fruit a = fruitReader.readCovariant(apples);
  }    
  public static void main(String[] args) {
    f1(); f2(); f3();
  }
} ///:~

与前面一样,第一个方法readExact()使用了精确的类型。因此如果使用这个没有任何通配符的精确类型,就可以向List中写人和读取这个精确类型。另外,对于返回值,静态的泛型方法readExact()可以有效地“适应”每个方法调用,并能够从List中返回一个Apple,从List中返回一个Fruit,就像在f1()中看到的那样。因此,如果可以摆脱静态泛型方法,那么当只是读取时,就不需要协变类型了。

但是,如果有一个泛型类,那么当你创建这个类的实例时,要为这个类确定参数。就像在f2()中看到的,fruitReader实例可以从List中读取一个Fruit,因为这就是它的确切类型。但是List还应该产生Fruit对象,而fruitReader不允许这么做。

为了修正这个问题,CovariantReader.readCovcariant()方法将接受List,因此,从这个列表中读取一个T是安全的(你知道在这个列表中的所有对象至少是一个T,并且可能是从T导出的某种对象)。在f3()中,你可以看到现在可以从List中读取Fruit了。

15.10.3 无界通配符

无界通配符看起来意味着“任何事物”,因此使用无界通配符好像等价于使用原生类型。

事实上,编译器初看起来是支持这种判断的:

//: generics/UnboundedWildcards1.java
import java.util.*;

public class UnboundedWildcards1 {
  static List list1;
  static List list2;
  static List list3;
  static void assign1(List list) {
    list1 = list;
    list2 = list;
    // list3 = list; // Warning: unchecked conversion
    // Found: List, Required: List
  }
  static void assign2(List list) {
    list1 = list;
    list2 = list;
    list3 = list;
  }    
  static void assign3(List list) {
    list1 = list;
    list2 = list;
    list3 = list;
  }
  public static void main(String[] args) {
    assign1(new ArrayList());
    assign2(new ArrayList());
    // assign3(new ArrayList()); // Warning:
    // Unchecked conversion. Found: ArrayList
    // Required: List
    assign1(new ArrayList());
    assign2(new ArrayList());
    assign3(new ArrayList());
    // Both forms are acceptable as List:
    List wildList = new ArrayList();
    wildList = new ArrayList();
    assign1(wildList);
    assign2(wildList);
    assign3(wildList);
  }
} ///:~

有很多情况都和你在这里看到的情况类似,即编译器很少关心使用的是原生类型还是。在这些情况中,可以被认为是一种装饰,但是它仍旧是很有价值的,因为,实际上,它是在声明:“我是想用Java的泛型来编写这段代码,我在这里并不是要用原生类型,但是在当前这种情况下,泛型参数可以持有任何类型。”

第二个示例展示了无界通配符的一个重要应用。当你在处理多个泛型参数时,有时允许一个参数可以是任何类型,同时为其他参数确定某种特定类型的这种能力会显得很重要:

//: generics/UnboundedWildcards2.java
import java.util.*;

public class UnboundedWildcards2 {
  static Map map1;
  static Map map2;
  static Map map3;
  static void assign1(Map map) { map1 = map; }
  static void assign2(Map map) { map2 = map; }
  static void assign3(Map map) { map3 = map; }
  public static void main(String[] args) {
    assign1(new HashMap());
    assign2(new HashMap());
    // assign3(new HashMap()); // Warning:
    // Unchecked conversion. Found: HashMap
    // Required: Map
    assign1(new HashMap());
    assign2(new HashMap());
    assign3(new HashMap());
  }
} ///:~

但是,当你拥有的全都是无界通配符时,就像在Map中看到的那样,编译器看起来就无法将其与原生Map区分开了。另外,UnboundedWildcards.java展示了编译器处理ListList时是不同的。

令人困惑的是,编译器并非总是关注像List和List之间的这种差异,因此它们看起来就像是相同的事物。因为,事实上,由干泛型参数将擦除到它的第一个边界,因此List看起来等价于List,而List实际上也是List——除非这些语句都不为真。List实际上表示“持有任何Object类型的原生List”,而List表示“具有某种特定类型的非原生List,只是我们不知道那种类型是什么。”

编译器何时才会关注原生类型和涉及无界通配符的类型之间的差异呢?下面的示例使用了前面定义的Holder类,它包含接受Holder作为参数的各种方法,但是它们具有不同的形式作为原生类型,具有具体的类型参数以及具有无界通配符参数:

//: generics/Wildcards.java
// Exploring the meaning of wildcards.

public class Wildcards {
   // Raw argument:
  static void rawArgs(Holder holder, Object arg) {
    // holder.set(arg); // Warning:
    //   Unchecked call to set(T) as a
    //   member of the raw type Holder
    // holder.set(new Wildcards()); // Same warning

    // Can't do this; don't have any 'T':
    // T t = holder.get();

    // OK, but type information has been lost:
    Object obj = holder.get();
  }    
  // Similar to rawArgs(), but errors instead of warnings:
  static void unboundedArg(Holder holder, Object arg) {
    // holder.set(arg); // Error:
    //   set(capture of ?) in Holder
    //   cannot be applied to (Object)
    // holder.set(new Wildcards()); // Same error

    // Can't do this; don't have any 'T':
    // T t = holder.get();

    // OK, but type information has been lost:
    Object obj = holder.get();
  }    
  static  T exact1(Holder holder) {
    T t = holder.get();
    return t;
  }
  static  T exact2(Holder holder, T arg) {
    holder.set(arg);
    T t = holder.get();
    return t;
  }
  static 
  T wildSubtype(Holder holder, T arg) {
    // holder.set(arg); // Error:
    //   set(capture of ? extends T) in
    //   Holder
    //   cannot be applied to (T)
    T t = holder.get();
    return t;
  }    
  static 
  void wildSupertype(Holdersuper T> holder, T arg) {
    holder.set(arg);
    // T t = holder.get();  // Error:
    //   Incompatible types: found Object, required T

    // OK, but type information has been lost:
    Object obj = holder.get();
  }
  public static void main(String[] args) {
    Holder raw = new Holder();
    // Or:
    raw = new Holder();
    Holder qualified = new Holder();
    Holder unbounded = new Holder();
    Holder bounded = new Holder();
    Long lng = 1L;

    rawArgs(raw, lng);
    rawArgs(qualified, lng);
    rawArgs(unbounded, lng);
    rawArgs(bounded, lng);

    unboundedArg(raw, lng);
    unboundedArg(qualified, lng);
    unboundedArg(unbounded, lng);
    unboundedArg(bounded, lng);

    // Object r1 = exact1(raw); // Warnings:
    //   Unchecked conversion from Holder to Holder
    //   Unchecked method invocation: exact1(Holder)
    //   is applied to (Holder)
    Long r2 = exact1(qualified);
    Object r3 = exact1(unbounded); // Must return Object
    Long r4 = exact1(bounded);

    // Long r5 = exact2(raw, lng); // Warnings:
    //   Unchecked conversion from Holder to Holder
    //   Unchecked method invocation: exact2(Holder,T)
    //   is applied to (Holder,Long)
    Long r6 = exact2(qualified, lng);
    // Long r7 = exact2(unbounded, lng); // Error:
    //   exact2(Holder,T) cannot be applied to
    //   (Holder,Long)
    // Long r8 = exact2(bounded, lng); // Error:
    //   exact2(Holder,T) cannot be applied
    //   to (Holder,Long)

    // Long r9 = wildSubtype(raw, lng); // Warnings:
    //   Unchecked conversion from Holder
    //   to Holder
    //   Unchecked method invocation:
    //   wildSubtype(Holder,T) is
    //   applied to (Holder,Long)
    Long r10 = wildSubtype(qualified, lng);
    // OK, but can only return Object:
    Object r11 = wildSubtype(unbounded, lng);
    Long r12 = wildSubtype(bounded, lng);

    // wildSupertype(raw, lng); // Warnings:
    //   Unchecked conversion from Holder
    //   to Holder
    //   Unchecked method invocation:
    //   wildSupertype(Holder,T)
    //   is applied to (Holder,Long)
    wildSupertype(qualified, lng);
    // wildSupertype(unbounded, lng); // Error:
    //   wildSupertype(Holder,T) cannot be
    //   applied to (Holder,Long)
    // wildSupertype(bounded, lng); // Error:
    //   wildSupertype(Holder,T) cannot be
    //  applied to (Holder,Long)
  }
} ///:~

在rawArgs()中,编译器知道Holder是一个泛型类型,因此即使它在这里被表示成一个原生类型,编译器仍旧知道向set()传递一个Object是不安全的。由于它是原生类型,你可以将任何类型的对象传递给set(),而这个对象将被向上转型为Object。因此,无论何时,只要使用了原生类型,都会放弃编译期检查。对get()的调用说明了相同的问题:没有任何T类型的对象,因此结果只能是一个Object。

人们很自然地会开始考虑原生Holder与Holder是大致相同的事物。但是unboundedArg()强调它们是不同的——它揭示了相同的问题,但是它将这些问题作为错误而不是警告报告,因为原生Holder将持有任何类型的组合,而Holder将持有具有某种具体类型的同构集合,因此不能只是向其中传递Object。

在exact1()和exact2()中,你可以看到使用了确切的泛型参数没有任何通配符。你将看到,exact2()与exact1()具有不同的限制,因为它有额外的参数。

在wildSubtype()中,在Holder类型上的限制被放松为包括持有任何扩展自T的对象的Holder。这还是意味着如果T是Fruit,那么Holder可以是Holder,这是合法的。为了防止将Orange放置到Holder中,对set()的调用(或者对任何接受这个类型参数为参数的方法的调用)都是不允许的。但是,你仍旧知道任何来自Holder的对象至少是Fruit,因此get()(或者任何将产生具有这个类型参数的返回值的方法)都是允许的。

wildSupertype()展示了超类型通配符,这个方法展示了与wildSubtype()相反的行为:holder可以是持有任何T的基类型的容器。因此,set()可以接受T,因为任何可以工作于基类的对象都可以多态地作用于导出类(这里就是T)。但是,尝试着调用get()是没有用的,因为由holde持有的类型可以是任何超类型,因此唯一安全的类型就是Object。

这个示例还展示了对于在unbounded()中使用无界通配符能够做什么不能做什么所做出的限制。对于迁移兼容性,rawArgs()将接受所有Holder的不同变体,而不会产生警告。unbounded-Args()方法也可以接受相同的所有类型,尽管如前所述,它在方法体内部处理这些类型的方式并不相同。

如果向接受“确切”泛型类型(没有通配符)的方法传递一个原生Holder引用,就会得到一个警告,因为确切的参数期望得到在原生类型中井不存在的信息。如果向exact1()传递一个无界引用,就不会有任何可以确定返回类型的类型信息。

可以看到,exact2()具有最多的限制,因为它希望精确地得到一个Holder,以及一个具有类型T的参数,正由于此,它将产生错误或警告,除非提供确切的参数。有时,这样做很好,但是如果它过于受限,那么就可以使用通配符,这取决于是否想要从泛型参数中返回类型确定的返回值(就像在wildSubtype()中看到的那样),或者是否想要向泛型参数传递类型确定的参数(就像在wildSupertype()中看到的那样)。

因此,使用确切类型来替代通配符类型的好处是,可以用泛型参数来做更多的事,但是使用通配符使得你必须接受范围更宽的参数化类型作为参数。因此,必须逐个情况地权衡利弊,找到更适合你的需求的方法。

15.10.4 捕获转换

有一种情况特别需要使用而不是原生类型。如果向一个使用的方法传递原生类型,那么对编译器来说,可能会推断出实际的类型参数,使得这个方法可以回转并调用另一个使用这个确切类型的方法。下面的示例演示了这种技术,它被称为捕获转换,因为未指定的通配符类型被捕获,并被转换为确切类型。这里,有关警告的注释只有在@SuppressWarnings注解被移除之后才能起作用:

//: generics/CaptureConversion.java

public class CaptureConversion {
  static  void f1(Holder holder) {
    T t = holder.get();
    System.out.println(t.getClass().getSimpleName());
  }
  static void f2(Holder holder) {
    f1(holder); // Call with captured type
  }    
  @SuppressWarnings("unchecked")
  public static void main(String[] args) {
    Holder raw = new Holder(1);
    // f1(raw); // Produces warnings
    f2(raw); // No warnings
    Holder rawBasic = new Holder();
    rawBasic.set(new Object()); // Warning
    f2(rawBasic); // No warnings
    // Upcast to Holder, still figures it out:
    Holder wildcarded = new Holder(1.0);
    f2(wildcarded);
  }
} /* Output:
Integer
Object
Double
*///:~

f1()中的类型参数都是确切的,没有通配符或边界。在f2()中,Holder()参数是一个无界通配符,因此它看起来是未知的。但是,在f2()中,f1()被调用,而f1()需要一个已知参数。这里所发生的是:参数类型在调用f2()的过程中被捕获,因此它可以在对f1()的调用中被使用。

你可能想知道,这项技术是否可以用于写入,但是这要求要在传递Holder时同时传递一个具体类型。捕获转换只有在这样的情况下可以工作:即在方法内部,你需要使用确切的类型。注意,不能从f2()中返回T,因为T对于f2()来说是未知的。捕获转换十分有趣,但是非常受限。

15.11问题

本节将阐述在使用Java泛型时会出现的各类问题。

15.11.1任何基本类型都不能作为类型参数

正如本章早先提到过的,你将在Java泛型中发现的限制之一是,不能将基本类型用作类型参数。因此不能创建ArrayList之类的东西。
解决之道是使用甚本类型的包装器类以及Java SE5的自动包装机制。如果创建一个ArrayList,并将基本类型int应用于这个容器,那么你将发现自动包装机制将自动地实现int到Integer的双向转换——因此,这几乎就像是有一个ArrayList一样:

//: generics/ListOfInt.java
// Autoboxing compensates for the inability to use
// primitives in generics.
import java.util.*;

public class ListOfInt {
  public static void main(String[] args) {
    List li = new ArrayList();
    for(int i = 0; i < 5; i++)
      li.add(i);
    for(int i : li)
      System.out.print(i + " ");
  }
} /* Output:
0 1 2 3 4
*///:~

注意,自动包装机制甚至允许用foreach语法来产生int。

通常,这种解决方案工作得很好—能够成功地存储和读取int,有一些转换碰巧在发生的同时会对你屏蔽掉。但是,如果性能成为了问题,就需要使用专门适配基本类型的容器版本。

//: generics/ByteSet.java
import java.util.*;

public class ByteSet {
  Byte[] possibles = { 1,2,3,4,5,6,7,8,9 };
  Set mySet =
    new HashSet(Arrays.asList(possibles));
  // But you can't do this:
  // Set mySet2 = new HashSet(
  //   Arrays.asList(1,2,3,4,5,6,7,8,9));
} ///:~

注意,自动包装机制解决了一些问题,但并不是解决了所有问题。下面的示例展示了一个泛型的Generator接口,它指定next()方法返回一个具有其参数类型的对象。FArray类包含一个泛型方法,它通过使用生成器在数组中填充对象(这使得类泛型在本例中无法工作,因为这个方法是静态的)。Generator实现来自第16章,并且在main()中,可以看到FArray.fill()使用它在数组中填充对象:

//: generics/PrimitiveGenericTest.java
import net.mindview.util.*;

// Fill an array using a generator:
class FArray {
  public static  T[] fill(T[] a, Generator gen) {
    for(int i = 0; i < a.length; i++)
      a[i] = gen.next();
    return a;
  }
}   

public class PrimitiveGenericTest {
  public static void main(String[] args) {
    String[] strings = FArray.fill(
      new String[7], new RandomGenerator.String(10));
    for(String s : strings)
      System.out.println(s);
    Integer[] integers = FArray.fill(
      new Integer[7], new RandomGenerator.Integer());
    for(int i: integers)
      System.out.println(i);
    // Autoboxing won't save you here. This won't compile:
    // int[] b =
    //   FArray.fill(new int[7], new RandIntGenerator());
  }
} /* Output:
YNzbrnyGcF
OWZnTcQrGs
eGZMmJMRoE
suEcUOneOE
dLsmwHLGEa
hKcxrEqUCB
bkInaMesbt
7052
6665
2654
3909
5202
2209
5458
*///:~

由于RandomGenerator.Integer实现了Generator,所以我的希望是自动包装机制可以自动地将next()的值从Integer转换为int。但是,自动包装机制不能应用于数组,因此这无法工作。

15.11.2实现参数化接口

一个类不能实现同一个泛型接口的两种变体,由于擦除的原因,这两个变体会成为相同的接口。下面是产生这种冲突的情况:

//: generics/MultipleInterfaceVariants.java
// {CompileTimeError} (Won't compile)

interface Payable {}

class Employee implements Payable {}
class Hourly extends Employee
  implements Payable {} ///:~

Hourly不能编译,因为擦除会将PayablePayable简化为相同的类Payable,这样,上面的代码就意味着在重复两次地实现相同的接口。十分有趣的是,如果从Payable的两种用法中都移除掉泛型参数〔就像编译器在擦出阶段所做的那样)这段代码就可以编译。
在使用某些更基本的Java接口,例如Comparable时,这个问题可能会变得十分令人恼火,就像你在本节稍后就会看到的那样。

15.11.3转型和警告

使用带有泛型类型参数的转型或instanceof不会有任何效果。下面的容器在内部将各个值存储为Object,并在获取这些值时,耳将它们转型回T:

//: generics/GenericCast.java

class FixedSizeStack {
  private int index = 0;
  private Object[] storage;
  public FixedSizeStack(int size) {
    storage = new Object[size];
  }
  public void push(T item) { storage[index++] = item; }
  @SuppressWarnings("unchecked")
  public T pop() { return (T)storage[--index]; }
}   

public class GenericCast {
  public static final int SIZE = 10;
  public static void main(String[] args) {
    FixedSizeStack strings =
      new FixedSizeStack(SIZE);
    for(String s : "A B C D E F G H I J".split(" "))
      strings.push(s);
    for(int i = 0; i < SIZE; i++) {
      String s = strings.pop();
      System.out.print(s + " ");
    }
  }
} /* Output:
J I H G F E D C B A
*///:~

如果没有@SuppressWarnings注解,编译器将对pop()产生“unchecked cast’警告。由于擦除的原因,编译器无法知道这个转型是否是安全的,并且pop()方法实际上并没有执行任何转型。这是因为,T被擦除到它的第一个边界,默认情况下是Object, 因此pop()实际上只是将Object转型为Object。

有时,泛型没有消除对转型的需要,这就会由编译器产生警告,而这个警告是不恰当的。例如:

//: generics/NeedCasting.java
import java.io.*;
import java.util.*;

public class NeedCasting {
  @SuppressWarnings("unchecked")
  public void f(String[] args) throws Exception {
    ObjectInputStream in = new ObjectInputStream(
      new FileInputStream(args[0]));
    List shapes = (List)in.readObject();
  }
} ///:~

正如你将在下一章学到的那样,readObject()无法知道它正在读取的是什么,因此它返的是必须转型的对象。但是当注释掉@SuppressWarnings注解,并编译这个程序时,就会得到下面的警告:

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

如果遵循这条指示,使用-Xliint: unchecked来重新编译:

NeedCasting.java:12: warning: [unchecked] unchecked cast found:java.lang.Object
required: java.util.List
    List shapes = (List)in.readObjlect():

你会被强制要求转型,但是又被告知不应该转型。为了解决这个问题,必须使用在Java SE5中引入的新的转型形式,既通过泛型类来转型:

//: generics/ClassCasting.java
import java.io.*;
import java.util.*;

public class ClassCasting {
  @SuppressWarnings("unchecked")
  public void f(String[] args) throws Exception {
    ObjectInputStream in = new ObjectInputStream(
      new FileInputStream(args[0]));
      // Won't Compile:
//    List lw1 =
//    List.class.cast(in.readObject());
    List lw2 = List.class.cast(in.readObject());
  }
} ///:~

但是,不能转型到实际类型(List)。也就是说,不能声明:

    List.class.cast(in.readObject())

甚至当你添加一个像下面这样的另一个转型时:

    (List)List.class.cast(in.readObject())

仍旧会得到一个警告。

15.11.4重裁

下面的程序是不能编译的,即使编译它是一种合理的尝试:

//: generics/UseList.java
// {CompileTimeError} (Won't compile)
import java.util.*;

public class UseList<W,T> {
  void f(List v) {}
  void f(List v) {}
} ///:~

由于擦除的原因,重载方法将产生相同的类型签名。
与此不同的是,当被擦除的参数不能产生唯一的参数列表时。必须提供明显有区别的方法名:

//: generics/UseList2.java
import java.util.*;

public class UseList2<W,T> {
  void f1(List v) {}
  void f2(List v) {}
} ///:~

幸运的是,这类问题可以由编译器探测到。

15.11.5 基类劫持了接口

假设你有一个Pet类,它可以与其他的Pet对象进行比较(实现了Comparable接口):

//: generics/ComparablePet.java

public class ComparablePet
implements Comparable<ComparablePet> {
  public int compareTo(ComparablePet arg) { return 0; }
} ///:~

对可以与ComparablePet的子类比较的类型进行窄化是有意义的,例如,一个Cat对象就只能与其他的Cat对象比较:

//: generics/HijackedInterface.java
// {CompileTimeError} (Won't compile)

class Cat extends ComparablePet implements Comparable{
  // Error: Comparable cannot be inherited with
  // different arguments:  and 
  public int compareTo(Cat arg) { return 0; }
} ///:~

遗憾的是,这不能工作。一旦为Comparable确定了ComparablePet参数,那么其他任何实现类都不能与ComparablePet之外的任何对象比较:

//: generics/RestrictedComparablePets.java

class Hamster extends ComparablePet
implements Comparable {
  public int compareTo(ComparablePet arg) { return 0; }
}

// Or just:

class Gecko extends ComparablePet {
  public int compareTo(ComparablePet arg) { return 0; }
} ///:~

Hamster说明再次实现ComparablePet中的相同接口是可能的,只要她们精确地相同,包括参数类型在内。但是,这只是与覆盖基类中的方法相同,就像在Geeko中看到的那样。

你可能感兴趣的:(技术)