一般的类和方法,只能使用具体的类型:要么是基本类型,要么是自定义的类。如果要编写可以应用于多种类型的代码,这种刻板的限制对代码的束缚就会很大。
在面向对象编程语言中,多态算是一种泛化机制。例如,你可以将方法的参数类型设为基类,那么该方法就可以接受从这个基类中导出的任何类作为参数。这样的方法更加通用一些,可应用的地方也多一些。在类的内部也是如此,凡是需要说明类型的地方,如果都使用基类,确实能够具备更好的灵活性。但是,考虑到除了final类不能扩展,其他任何类都可以被扩展,所以这种灵活性大多数时候也会有一些性能损耗。
有是有,拘泥于单继承体系,也会使程序受限太多。如果方法的参数是一个接口,而不是一个类,这种限制就放松了许多。因为任何实现了该接口的类都能够满足该方法,这也包括展示还不存在的类。这就基于客户端程序员一种选择,他可以通过实现一个接口来满足类或方法。因此,接口允许我们快捷地实现类继承,也使我们有机会创建一个新类来做到这一点。
可是有的时候,即便使用了接口,对程序的约束也还是太强了。因为一旦指明了接口,它就要求你的代码必须使用特定的接口。而我们希望达到的目的是编写更通用的代码,要使代码能够应用于“某种不具体的类型”,而不是一个具体的接口或类。
这就是Java SE5的重大变化之一:泛型的概念。泛型实现了参数化类型的概念,使代码可以应用于多种类型。“泛型”这个属于的意思是:“适用于许多许多的类型”。泛型在编程语言中出现时,其最初的目的是希望类或方法能够具备最广泛的表达能力。如何做到这一点呢,正式通过解耦类或方法与所使用的类型之间的约束。稍后你将看到,Java中的泛型并没有这么高的追求,实际上,你可能会质疑,Java中的术语“泛型”是否适合用来描述这一功能。
如果你从未接触过参数化类型机制,那么,在学习了Java中的泛型之后,你会发现,对这门语言而言,泛型确实是一个很有益的补充。在你创建参数化类型的一个实例时,编译器会为你负责转型操作,并且保证类型的正确性。这应该是一个进步。
然而,如果你了解其他语言(例如C++)中的参数化类型机制,你就会发现,有些以前能做到的事情,使用Java的泛型机制却无法做到。使用别人已经构建好的泛型类型会相当容易。但是如果你要自己创建一个泛型实例,就会遇到许多令你吃惊的事情。
这并非是说Java的泛型毫无用处。在很多情况下,它们可以使代码更直接更优雅。不过,如果你具备其他语言的经验,而那种语言实现了更纯粹的泛型,那么Java可能令你失望了。在本文中,我们会介绍Java泛型的优点与局限,希望这能够帮助你更有效地使用Java的这个新功能。
1.与C++的比较
Java的设计者曾说过,设计者们语言的灵感主要来自C++。尽管如此,学习Java时,基本上可以不用参考C++。我也是尽力这样做的,除非,与C++的比较能够加深理解。
Java中的泛型就需要与C++进行一番比较,理由有二:首先,了解C++模板的某些方面,有助于你理解泛型的基础。同时,非常重要的一点是,你可以了解Java泛型的局限是什么,以及为什么会有这些限制。最终的目的是帮助你理解,Java泛型的边界在哪里。根据我的经验,理解了边界所在,你才能成为程序高手。因为只有知道了某个技术不能做到什么,你才能更好地做到所能做的(部分原因是,不必浪费时间在死胡同里乱转)。
第二个原因是,在Java社区中,人们普遍对C++模板有一种误解,而这种误解可能会误导你,令你在理解泛型的意图时产生偏差。
因此,在本文中会介绍一些C++模板的例子,不过我也会尽量控制它们的篇幅。
2.简单泛型
有许多原因促成了泛型的出现,而最引人注目的一个原因,就是为了创造容器类。容器,就是存放要使用的对象的地方。数组也是如此,不过与简单的数组相比,容器类更加灵活,具备更多不同的功能。事实上,所有的程序,在运行时都要求你持有一大堆对象,所以,容器类算得上最具有重用性的类库之一。
我们先来看看一个只能持有单个对象的类。当然了,这个类可以明确指定其持有的对象的类型:
package com.zjwave.thinkinjava.generics;
public class Holder1 {
private Automobile a;
public Holder1(Automobile a) {
this.a = a;
}
Automobile get(){
return a;
}
}
class Automobile {}
不过,这个类的可重用性就不怎么样了,它无法持有其他类型的任何对象。我们可不希望为碰到的每个类型都编写一个新的类。
在Java SE5之前,我们可以让这个类直接持有Object类型的对象:
package com.zjwave.thinkinjava.generics;
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();
System.out.println(a);
h2.set("Not an Automobile");
String s = (String) h2.get();
System.out.println(s);
h2.set(1);
Integer x = (Integer) h2.get();
System.out.println(x);
}
}
现在,Holder2可以存储任何类型的对象,在这个例子中,只用了一个Holder2对象,却先后三次存储了三种不同类型的对象。
有些情况下,我们确实希望容器能够同时持有多种类型的对象。但是,通常而言,我们只会使用容器来存储一种类型的对象。泛型的主要目的之一就是用来指定容器要持有什么类型的对象,而且由编译器来保证类型的正确性。
因此,与其使用Object,我们更喜欢暂时不指定类型,而是稍后再决定具体使用什么类型。要达到这个目的,需要使用类型参数,用尖括号括住,放在类型后面。然后在使用这个类的时候,再用实际的类型替换此参数。在下面的例子中,T就是类型参数:
package com.zjwave.thinkinjava.generics;
public class Holder3<T> {
private T a;
public Holder3(T a) {
this.a = a;
}
public T get() {
return a;
}
public void set(T a) {
this.a = a;
}
public static void main(String[] args) {
Holder3<Automobile> h3 = new Holder3<>(new Automobile());
Automobile a = h3.get();
//h3.set("Not an Automobile"); // Error
//h3.set(1); //Error
}
}
现在,当你创建Holder3对象时,必须指明想持有什么类型的对象,将其置于尖括号内。就像main()中那样。然后,你就只能在Holder3中存入该类型(或其子类,因为多态与泛型不冲突)的对象了。并且,在你从Holder3中取出它持有的对象时,自动地就是正确的类型。
这就是Java泛型的核心概念:告诉编译器想使用什么类型,然后编译器帮你处理一切细节。
一般而言,你可以认为泛型与其他的类型差不多,只不过它们碰巧有类型参数罢了。稍后我们会看到,在使用泛型时,我们只需指定它们的名称以及类型参数列表即可。
2.1 一个元组类库
仅一次方法嗲用就能返回多个对象,你应该经常需要这样的功能吧。可是return语句只允许返回单个对象,因此,解决办法就是创建一个对象,用它来持有想要返回的多个对象。当然,可以在每次需要的时候,专门创建一个类来完成这样的工作。可是有了泛型,我们就能够一次性地解决该问题,以后再也不用在这个问题上浪费时间了。同时,我们在编译期就能确保类型安全。
这个概念称为元组(tuple),它是将一组对象直接打包存储于之中的一个单一对象。这个容器对象允许读取其中元素,但是不允许向其中存放新的对象。(这个概念也成为数据传送对象,或信使。)
通常,元组可以具有任意长度,同时,元组中的对象可以是任意不同的类型。不过,我们希望能够为每一个对象指明其类型,并且从容器中读取出来时,能够得到正确的类型。要处理不同长度的问题,我们需要创建多个不同的元组。下面的程序是一个二维元组,它能够持有两个对象:
package com.zjwave.thinkinjava.generics;
public class TwoTuple<A,B> {
public final A first;
public final B second;
public TwoTuple(A first, B second) {
this.first = first;
this.second = second;
}
@Override
public String toString() {
return "TwoTuple{" +
"first=" + first +
", second=" + second +
'}';
}
}
构造器捕获了要存储的对象,而toString()是一个便利函数,用来显示列表中的值。注意,元组隐含地保持了其中元素的次序。
第一次阅读上面的代码,你也许会想,这不是违反了Java编程的安全性原则吗?first和second应该声明为private,然后提供getFirst()和getSecond()之类的访问方法才对呀?让我们自己看看这个例子中的安全性:客户端程序可以读取first和second对象,然后可以随心所欲地使用这两个对象。但是,它们却无法将其他值赋予first或second。因为final声明为你买了相同的安全保险,而且这种格式更简洁明了。
还有另一种设计考虑,即使你确实希望允许客户端程序员改变first或second所引用的对象。然而,采用以上的形式无疑是更安全的做法,这样的话,如果程序员想要使用具有不同元素的元组,就强制要求它们另外创建一个新的TwoTuple对象。
我们可以利用继承机制实现长度更长的元组。从下面的例子中可以看到,增加类型参数是很简单的事情:
package com.zjwave.thinkinjava.generics;
public class ThreeTuple<A,B,C> extends TwoTuple<A,B> {
public final C third;
public ThreeTuple(A first, B second, C third) {
super(first, second);
this.third = third;
}
@Override
public String toString() {
return "ThreeTuple{" +
"third=" + third +
", first=" + first +
", second=" + second +
'}';
}
}
package com.zjwave.thinkinjava.generics;
public class FourTuple<A,B,C,D> extends ThreeTuple<A,B,C> {
public final D fourth;
public FourTuple(A first, B second, C third, D fourth) {
super(first, second, third);
this.fourth = fourth;
}
@Override
public String toString() {
return "FourTuple{" +
"fourth=" + fourth +
", third=" + third +
", first=" + first +
", second=" + second +
'}';
}
}
package com.zjwave.thinkinjava.generics;
public class FiveTuple<A,B,C,D,E> extends FourTuple<A,B,C,D> {
public final E fifth;
public FiveTuple(A first, B second, C third, D fourth, E fifth) {
super(first, second, third, fourth);
this.fifth = fifth;
}
@Override
public String toString() {
return "FiveTuple{" +
"fifth=" + fifth +
", fourth=" + fourth +
", third=" + third +
", first=" + first +
", second=" + second +
'}';
}
}
为了使用元组,你只需定义一个长度适合的元组,将其作为方法的返回值,然后在return语句中创建该元组,并返回即可。
package com.zjwave.thinkinjava.generics;
public class TupleTest {
static TwoTuple<String,Integer> f(){
// Autoboxing converts the int to Integer:
return new TwoTuple<>("hi",47);
}
static ThreeTuple<Amphbian,String,Integer> g(){
return new ThreeTuple<>(new Amphbian(),"hi",47);
}
static FourTuple<Vehicle,Amphbian,String,Integer> h(){
return new FourTuple<>(new Vehicle(),new Amphbian(),"hi",47);
}
static FiveTuple<Vehicle,Amphbian,String,Integer,Double> k(){
return new FiveTuple<>(new Vehicle(),new Amphbian(),"hi",47,11.1);
}
public static void main(String[] args) {
TwoTuple<String, Integer> ttsi = f();
System.out.println(ttsi);
//ttsi.first = "there";//Compile error: Cannot assign a value to final variable 'first'
System.out.println(g());
System.out.println(h());
System.out.println(k());
}
}
class Amphbian{}
class Vehicle{}
由于有了泛型,你可以很容易地创建元组,令其返回一组任意类型的对象。而你所要做的,只是编写表达式而已。
通过ttsi.first = "there"语句的错误,我们可以看出,final声明确实能够保护public元素,在对象被构造出来之后,声明为final的元素遍不能被再赋予其他值了。
在上面的程序中,new表达式确实有点啰嗦。本文稍后会介绍,如何利用泛型方法简化这样的表达式。
2.2 一个堆栈类
接下来我们看一个稍微复杂一点的例子:传统的下推堆栈:
package com.zjwave.thinkinjava.generics;
public class LinkedStack<T> {
private static class Node<U> {
U item;
Node<U> next;
public Node() {
}
public Node(U item, Node<U> next) {
this.item = item;
this.next = next;
}
boolean end() {
return item == null && next == null;
}
}
private Node<T> 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<String> lss = new LinkedStack<>();
for (String s : "Phasers or stun!".split(" ")) {
lss.push(s);
}
String s;
while ((s = lss.pop()) != null){
System.out.println(s);
}
}
}
内部类Node也是一个泛型,它拥有自己的类型参数。
这个例子使用了一个末端哨兵(end sentinel)来判断堆栈何时为空。这个末端哨兵是在构造LinkedStack时创建的。然后,每调用一次push()方法,就会创建一个Node<T>对象,并将其链接到前一个Node<T>对象。当你调用pop()方法时,总是返回top.item,然后丢弃当前top所指的Node<T>,并将top转移到下一个Node<T>,除非你已经碰到了末端哨兵,这时候就不再移动top了。如果已经到了末端,客户端程序还继续调用pop()方法,它只能得到null,说明堆栈已经空了。
2.3 RandomList
作为容器的另一个例子,假设我们需要一个持有特定类型对象的列表,每次调用其上的select()方法时,它可以随机地选取一个元素。如果我们希望以此构件一个可以应用于各种类型的对象的工具,就需要使用泛型:
package com.zjwave.thinkinjava.generics;
import java.util.ArrayList;
import java.util.Random;
public class RandomList<T> {
private ArrayList<T> 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<String> 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.println(rs.select());
}
}
}
3. 泛型接口
泛型也可以应用于接口。例如生成器(generator),这是一种专门负责创建对象的类。实际上,这是工厂方法设计模式的一种应用。不过,当使用生成器创建新的对象时,它不需要任何参数,而工厂方法一般需要参数。也就是说,生成器无需额外的信息就知道如何创建新对象。
一般而言,一个生成器只定义一个方法,该方法用以产生新的对象。在这里,就是next()方法:
package com.zjwave.thinkinjava.generics;
public interface Generator<T> {
T next();
}
方法next()的返回类型是参数化的T。正如你所见到的,接口使用泛型与类使用泛型没什么区别。
为了演示如何实现Generator接口,我们还需要一些别的类。例如,Coffee类层次接口如下:
package com.zjwave.thinkinjava.generics.coffee;
public class Coffee {
private static long counter;
private final long id = counter++;
@Override
public String toString() {
return getClass().getSimpleName() + " " + id;
}
}
package com.zjwave.thinkinjava.generics.coffee;
public class Latte extends Coffee { }
package com.zjwave.thinkinjava.generics.coffee;
public class Mocha extends Coffee{ }
package com.zjwave.thinkinjava.generics.coffee;
public class Cappuccino extends Coffee { }
package com.zjwave.thinkinjava.generics.coffee;
public class Americano extends Coffee{ }
package com.zjwave.thinkinjava.generics.coffee;
public class Breve extends Coffee{ }
现在,我们可以编写一个类,实现Generator<Coffee>接口,它能够随机生成不同类型的Coffee对象:
package com.zjwave.thinkinjava.generics.coffee;
import com.zjwave.thinkinjava.generics.Generator;
import java.util.Iterator;
import java.util.Random;
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);
private int size = 0;
public CoffeeGenerator() {
}
public CoffeeGenerator(int size) {
this.size = size;
}
@Override
public Coffee next() {
try {
return (Coffee) types[rand.nextInt(types.length)].newInstance();
} catch (Exception e) {
throw new RuntimeException(e);
}
}
@Override
public Iterator<Coffee> iterator() {
return new CoffeeIterator();
}
class CoffeeIterator implements Iterator<Coffee>{
int count = size;
@Override
public boolean hasNext() {
return count > 0;
}
@Override
public Coffee next() {
count--;
return CoffeeGenerator.this.next();
}
}
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);
}
}
}
参数化的Generator接口确保next()的返回值是参数的类型。CoffeeGenerator同时还实现了Iterable接口,所以它可以在循环语句中使用。不过,它还需要一个“末端哨兵”来判断何时停止,这正是第二个构造器的功能。
下面是类是Generator<T>接口的另一个实现,它负责生成Fibonacci数列:
package com.zjwave.thinkinjava.generics;
public class Fibonacci implements Generator<Integer>{
private int count = 0;
@Override
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() + " ");
}
}
}
虽然我们在Fibonacci类的里里外外使用的都是int类型,但是其类型参数确是Integer。这个例子引出了Java泛型的一个局限性:基本类型无法作为类型参数。不过,Java SE5具备了自动打包和自动拆包功能,可以很方便地在基本类型和其相应的包装器类型之间进行转换。通过这个例子中Fibonacci类对int的使用,我们已经看到了这种效果。
如果还想更进一步,编写一个实现了Iterable的Fibonacci生成器。我们的一个选择是重写这个类,令其实现Iterable接口。不过,你并不是总能拥有源代码的控制权,并且,除非必须这么做,否则我们也不愿意重写一个类。而且我们还有另一种选择,就是创建一个适配器(adapter)来实现所需的接口。
有多重方法可以实现适配器。例如,可以通过继承来创建适配器类:
package com.zjwave.thinkinjava.generics;
import java.util.Iterator;
public class IterableFibonacci extends Fibonacci implements Iterable<Integer> {
private int n;
public IterableFibonacci(int count) {
this.n = count;
}
@Override
public Iterator<Integer> iterator() {
return new Iterator<Integer>() {
@Override
public boolean hasNext() {
return n > 0;
}
@Override
public Integer next() {
n--;
return IterableFibonacci.this.next();
}
};
}
public static void main(String[] args) {
for (Integer i : new IterableFibonacci(18)) {
System.out.print(i + " ");
}
}
}
如果要在循环语句中使用IterableFibonacci,必须向IterableFibonacci的构造器提供一个边界值,然后hasNext()方法才能知道何时应该返回false。
4.泛型方法
到目前为止,我们看到的泛型,都是应用于整个类上。但同样可以在类中包含参数化方法,而这个方法所在的类可以是泛型类,也可以不是。也就是说,是否拥有泛型方法,与其所在的类是否是泛型没有关系。
泛型方法使得该方法能够独立于类而产生变化。以下是一个基本的指导原则:无论何时,只要你能做到,你就应该尽量使用泛型方法。也就是说,如果使用泛型方法可以取代将整个类泛型化,那么就应该只使用泛型方法,因为它可以使事情更清楚明白。另外,对于一个static的方法而言,无法访问泛型类的类型参数,所以,如果static方法需要使用泛型能力,就必须使其成为泛型方法。
要定义泛型方法,只需将泛型参数列表置于返回值之前,就像下面这样:
package com.zjwave.thinkinjava.generics;
public class GenericMethods {
public <T> 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);
}
}
GenericMethods并不是参数化的,尽管这个类和其内部的方法可以被同时参数化,但是在这个例子中,只有方法f()拥有类型参数。这是由该方法的返回类型前面的类型参数列表指明的。
注意,当使用泛型类时,必须在创建对象的时候指定类型参数的值,而使用泛型方法的时候,通常不必指明参数类型,因为编译器会为我们找出具体的类型。这称为类型参数推断(type argument inference)。因此,我们可以像调用普通方法一样调用f(),而且就好像f()被无限次地重载过。它甚至可以接受GenericMethods作为其类型参数。
如果调用f()时传入基本类型,自动打包机制就会介入其中,将基本类型的值包装为对应的对象。事实上,泛型方法与自动打包避免了许多以前我们不得不自己编写出来的代码。
4.1 杠杆利用类型参数推断
本段是Java 8以前才会出现的问题,在Java 8中编译器已经可以无需再声明两次泛型。
人们对泛型有一个抱怨,使用泛型有时候需要向程序中加入更多的代码。如果要创建一个持有List的Map,就要像下面这样:
Map<Person,List<? extends Pet>> petPeople = new HashMap<Person,List<? extends Pet>>();
看到了吧,你在重复自己做过的事情,编译器本来应该能够从泛型参数列表中的一个参数推断出另一个参数。可惜的是,JDK8以前的编译器暂时还做不到。然而,在泛型方法中,类型参数推断可以为我们简化一部分工作。例如,我们可以编写一个工具类,它包含各种各样的static方法,专门用来创建各种常用的容器对象:
package com.zjwave.thinkinjava.generics;
import java.util.*;
public class New {
public static <K,V> Map<K,V> map(){
return new HashMap<K,V>();
}
public static <T> List<T> list(){
return new ArrayList<T>();
}
public static <T> LinkedList<T> lList(){
return new LinkedList<T>();
}
public static <T> Set<T> set(){
return new HashSet<T>();
}
public static <T> Queue<T> queue(){
return new LinkedList<T>();
}
public static void main(String[] args) {
Map<String, List<String>> sls = New.map();
List<String> ls = New.list();
LinkedList<String> lls = New.lList();
Set<String> ss = New.set();
Queue<String> qs = New.queue();
}
}
main()方法演示了如何使用这个工具类,类型参数推断避免了重复的泛型参数列表。不过,很难说它为我们带来了多少好处。如果某人阅读以上代码,他必须分析理解工具类New,以及New所隐含的功能。而这似乎与不使用New时(具有重复的类型参数列表的定义)的工作效率差不多。这真够讽刺的,要知道,我们引入New工具类的目的,正式为了使代码简单易读。不过,如果标准Java类库钥匙能添加类似New.java这样的工具类的话,我们还是应该使用这样的工具类。
类型推断只对赋值操作有效,其他时候并不起作用。如果你将一个泛型方法调用的结果(例如New.map())作为参数,传递给另一个方法,这是,编译器并不会执行类型推断。在这种情况下,编译器认为:调用泛型方法之后,其返回值被赋给一个Object类型的变量。下面的例子证明了这一点:
package com.zjwave.thinkinjava.generics;
import com.zjwave.thinkinjava.typeinfo.pets.Person;
import com.zjwave.thinkinjava.typeinfo.pets.Pet;
import java.util.List;
import java.util.Map;
public class LimitsOfInference {
static void f(Map<Person, List<? extends Pet>> petPerple){}
public static void main(String[] args) {
//f(New.map());// Does not compile
}
}
显式的类型说明
在泛型方法中,可以显式地指明类型,不过这种语法很少使用。要显式地指明类型,必须在点操作符与方法名之间插入尖括号,然后把类型置于尖括号内。如果是在定义该方法的类的内部。必须在点操作符之前使用this关键字,如果是使用static的方法,必须在点操作符之前加上类名。使用这种语法,可以解决LimitsOfInference.java中的问题:
package com.zjwave.thinkinjava.generics;
import com.zjwave.thinkinjava.typeinfo.pets.Person;
import com.zjwave.thinkinjava.typeinfo.pets.Pet;
import java.util.List;
import java.util.Map;
public class ExplicitTypeSpecification {
static void f(Map<Person, List<Pet>> petPerple){}
public static void main(String[] args) {
f(New.<Person,List<Pet>>map());
}
}
当然,这种语法抵消了New类为我们带来的好处(即省去了大量的类型说明),不过,只有在编写非赋值语句时,我们才需要这样的额外说明。
4.2 可变参数与泛型方法
泛型方法与可变参数列表能够很好地共存:
package com.zjwave.thinkinjava.generics;
import java.util.ArrayList;
import java.util.List;
public class GenericVarargs {
public static <T> List<T> makeList(T... args){
List<T> result = new ArrayList<>();
for (T item : args) {
result.add(item);
}
return result;
}
public static void main(String[] args) {
List<String> ls = makeList("A");
System.out.println(ls);
ls = makeList("A", "B", "C");
System.out.println(ls);
ls = makeList("ABCDEFGHIJKLMNOPQRSTUVWXYZ".split(""));
System.out.println(ls);
}
}
makeList()方法展示了与标准类库中java.util.Arrays.asList()方法相同的功能。
4.3 用于Generator的泛型方法
利用生成器,我们可以很方便地填充一个Collection,而泛型化这种操作是具有实际意义的:
package com.zjwave.thinkinjava.generics;
import com.zjwave.thinkinjava.generics.coffee.Coffee;
import com.zjwave.thinkinjava.generics.coffee.CoffeeGenerator;
import java.util.ArrayList;
import java.util.Collection;
public class Generators {
public static <T> Collection<T> fill(Collection<T> coll, Generator<T> gen, int n){
for (int i = 0; i < n; i++) {
coll.add(gen.next());
}
return coll;
}
public static void main(String[] args) {
Collection<Coffee> coffees = fill(new ArrayList<>(), new CoffeeGenerator(), 4);
for (Coffee c : coffees) {
System.out.println(c);
}
Collection<Integer> fnumbers = fill(new ArrayList<>(), new Fibonacci(), 12);
for (Integer i : fnumbers) {
System.out.print(i + ", ");
}
}
}
请注意,fill()方法是如何透明地应用于Coffee和Integer的容器和生成器。
4.4 一个通用的Generator
下面的程序可以为任何类构造一个Generator,只要该类具有默认的构造器。为了减少类型声明,它提供了一个泛型方法,用以生成BasicGenerator:
package com.zjwave.thinkinjava.generics;
public class BasicGenerator<T> implements Generator<T> {
private Class<T> type;
public BasicGenerator(Class<T> type) {
this.type = type;
}
@Override
public T next() {
try {
return type.newInstance();
} catch (Exception e) {
throw new RuntimeException(e);
}
}
public static <T> Generator<T> create(Class<T> type){
return new BasicGenerator<>(type);
}
}
这个类提供了一个基本实现,用以生成某个类的对象。这个类必须具备两个特点:
- 它必须声明为public。(因为BasicGenerator与要处理的类在不同的包中,所以该类必须声明为public,并且不知具有包内访问权限。)
- 它必须具备默认的构造器(无参数的构造器)。要创建这样的BasicGenerator对象,只需调用create()方法,并传入想要生成的类型。泛型化的create()方法允许执行BasicGenerator.create(MyType.class),而不必执行麻烦的new BasicGenerator<MyType>(MyType.class)。
例如,下面是一个具有默认构造器的简单的类:
package com.zjwave.thinkinjava.generics;
public class CountedObject {
private static long counter;
private final long id = counter++;
public long id(){
return id;
}
@Override
public String toString() {
return "CountedObject " + id;
}
}
CountedObject类能够记录下它创建了多少个CountedObject实例,并通过toString()方法告诉我们其编号。
使用BasicGenerator,你可以很容易地为CountedObject创建一个Generator:
package com.zjwave.thinkinjava.generics;
public class BasicGeneratorDemo {
public static void main(String[] args) {
Generator<CountedObject> gen = BasicGenerator.create(CountedObject.class);
for (int i = 0; i < 5; i++) {
System.out.println(gen.next());
}
}
}
可以看到,使用泛型方法创建Generator对象,大大减少了我们要编写的代码。Java泛型要求传入Class对象,以便也可以在create()方法中用它进行类型推断。
4.5 简化元组的使用
有了类型参数推断,再加上static方法,我们可以重新编写之前看到的元组工具,使其成为更通用的工具类库。在这个类中,我们通过重载static方法创建元组:
package com.zjwave.thinkinjava.generics;
public class Tuple {
public static <A,B> TwoTuple<A,B> tuple(A a,B b){
return new TwoTuple<>(a,b);
}
public static <A,B,C> ThreeTuple<A,B,C> tuple(A a,B b,C c){
return new ThreeTuple<>(a,b,c);
}
public static <A,B,C,D> FourTuple<A,B,C,D> tuple(A a,B b,C c,D d){
return new FourTuple<>(a,b,c,d);
}
public static <A,B,C,D,E> FiveTuple<A,B,C,D,E> tuple(A a,B b,C c,D d,E e){
return new FiveTuple<>(a,b,c,d,e);
}
}
下面是修改后的TupleTest.java,用来测试Tuple.java:
package com.zjwave.thinkinjava.generics;
import static com.zjwave.thinkinjava.generics.Tuple.*;
public class TupleTest2 {
static TwoTuple<String,Integer> f(){
return tuple("hi",47);
}
static TwoTuple f2(){
return tuple("hi",47);
}
static ThreeTuple<Amphbian,String,Integer> g(){
return tuple(new Amphbian(),"hi",47);
}
static FourTuple<Vehicle,Amphbian,String,Integer> h(){
return tuple(new Vehicle(),new Amphbian(),"hi",47);
}
static FiveTuple<Vehicle,Amphbian,String,Integer,Double> k(){
return tuple(new Vehicle(),new Amphbian(),"hi",47,11.1);
}
public static void main(String[] args) {
TwoTuple<String, Integer> ttsi = f();
System.out.println(ttsi);
System.out.println(f2());
System.out.println(g());
System.out.println(h());
System.out.println(k());
}
}
注意,方法f()返回一个参数化的TwoTuple对象,而f2()返回的是非参数化的TwoTuple对象。在这个例子中,编译器并没有关于f2()的警告信息,因为我们并没有将其返回值作为参数化对象使用。在某种意义上,它被“向上转型”为一个非参数化的TwoTuple。然而,如果试图将f2()的返回值转型为参数化的TwoTuple,编译器就会发出警告。
4.6 一个Set实用工具
作为泛型方法的另一个示例,我们看看如何用Set来表达数学中的关系式。通过使用泛型方法,可以很方便地做到这一点,而且可以应用于多种类型:
package com.zjwave.thinkinjava.generics;
import java.util.HashSet;
import java.util.Set;
public class Sets {
public static <T> Set<T> union(Set<T> a, Set<T> b){
Set<T> result = new HashSet<>(a);
result.addAll(b);
return result;
}
public static <T> Set<T> intersection(Set<T> a ,Set<T> b){
Set<T> result = new HashSet<>(a);
result.retainAll(b);
return result;
}
public static <T> Set<T> difference(Set<T> superset,Set<T> subset){
Set<T> result = new HashSet<>(superset);
result.removeAll(subset);
return result;
}
public static <T> Set<T> complement(Set<T> a , Set<T> b){
return difference(union(a,b),intersection(a,b));
}
}
在前三个方法中,都将第一个参数Set复制了一份,将Set中的所有引用都存入一个新的HashSet对象中,因此,我们并未直接修改参数中的Set。返回的值是一个全新的Set对象。
这四个方法表达了如下的数学集合操作:
- union()返回一个Set,它将两个参数合并在一起。
- intersection()返回的Set只包含两个参数的公共部分。
- difference()方法从superset中移除subset包含的元素
- complement()返回的Set包含除了交集之外的所有元素。
下面提供了一个enum,它包含各种水彩画的颜色。我们将用它来演示以上这些方法的功能和效果。
package com.zjwave.thinkinjava.generics;
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;
}
为了方便起见,下面的示例以static的方式引入Watercolors。这个示例使用了EnumSet,这是Java SE5中的新工具,用来从enum直接创建Set。在这里,我们向static方法EnumSet.range()传入某个范围的第一个元素与最后一个元素,然后它将返回一个Set,其中包含该范围内的所有元素:
package com.zjwave.thinkinjava.generics;
import java.util.EnumSet;
import java.util.Set;
import static com.zjwave.thinkinjava.generics.Watercolors.*;
import static com.zjwave.thinkinjava.generics.Sets.*;
public class WatercolorSets {
public static void main(String[] args) {
EnumSet<Watercolors> set1 = EnumSet.range(BRILLIANT_RED, VIRIDIAN_HUE);
EnumSet<Watercolors> set2 = EnumSet.range(CERULEAN_BLUE_HUE, BURNT_UMBER);
System.out.println("set1: " + set1);
System.out.println("set1: " + set2);
System.out.println("union(set1,set2): " + union(set1,set2));
Set<Watercolors> subset = intersection(set1, set2);
System.out.println("intersection(set1,set2): " + subset);
System.out.println("difference(set1,subset): " + difference(set1,subset));
System.out.println("difference(set2,subset): " + difference(set2,subset));
System.out.println("complement(set1,set2)" + complement(set1,set2));
}
}
我们可以从输出中看到各种关系运算的结果。
下面是示例使用Sets.difference()打印出java.util包中各种Collection类与Map类之间的方法差异:
package com.zjwave.thinkinjava.generics;
import java.lang.reflect.Method;
import java.util.*;
public class ContainerMethodDifferences {
static Set<String> methodSet(Class<?> type){
Set<String> result = new TreeSet<String>();
for (Method m : type.getMethods()) {
result.add(m.getName());
}
return result;
}
static void interfaces(Class<?> type){
System.out.print("interfaces in " + type.getSimpleName() + ": ");
List<String> result = new ArrayList<>();
for (Class<?> c : type.getInterfaces()) {
result.add(c.getSimpleName());
}
System.out.println(result);
}
static Set<String> object = methodSet(Object.class);
static {
object.add("clone");
}
static void difference(Class<?> subset,Class<?> superset){
System.out.print(subset.getSimpleName() + " extends " + superset.getSimpleName() + ", adds: ");
Set<String> comp = Sets.difference(methodSet(subset), methodSet(superset));
comp.removeAll(object);//Don't show 'Object' methods
System.out.println(comp);
interfaces(subset);
}
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,Collections.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);
}
}
5.匿名内部类
泛型还可以应用于内部类以及匿名内部类。下面的示例使用匿名内部类实现了Generator接口:
package com.zjwave.thinkinjava.generics;
import java.util.*;
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<Customer> line = new LinkedList<>();
Generators.fill(line,Customer.generator(),15);
List<Teller> tellers = new ArrayList<>();
Generators.fill(tellers,Teller.generator(),4);
for (Customer c : line) {
serve(tellers.get(rand.nextInt(tellers.size())),c);
}
}
}
class Customer{
private static long counter = 1;
private final long id = counter++;
private Customer() {}
@Override
public String toString() {
return "Customer " + id;
}
public static Generator<Customer> generator(){
return new Generator<Customer>() {
@Override
public Customer next() {
return new Customer();
}
};
}
}
class Teller{
private static long counter = 1;
private final long id = counter++;
private Teller() {}
@Override
public String toString() {
return "Teller " + id;
}
public static Generator<Teller> generator(){
return new Generator<Teller>() {
@Override
public Teller next() {
return new Teller();
}
};
}
}
Customer和Teller类都只有private的构造器,这可以强制你必须使用Generator对象。Customer有一个generator()方法,每次执行它都会生成一个新的Generator<Customer>对象。我们其实不需要多个Generator对象,Teller就只创建了一个Generator对象。在main()方法中可以看到,这两种创建Generator的方式都在fill()中用到了。
由于Customer中的generator()方法,以及Teller中的Generator对象都声明成了static的,所以它们无法作为接口的一部分,因此无法用接口这种特定的惯用法来泛化这二者。尽管如此,它们在fill()方法中都工作得很好。
6.构建复杂模型
泛型的一个重要好处是能够简单而安全地创建复杂的模型。例如,我们可以很容易地创建List元组:
package com.zjwave.thinkinjava.generics;
import java.util.ArrayList;
public class TupleList<A,B,C,D> extends ArrayList<FourTuple<A,B,C,D>> {
public static void main(String[] args) {
TupleList<Vehicle,Amphbian,String,Integer> tl = new TupleList<>();
tl.add(TupleTest.h());
tl.add(TupleTest.h());
for (FourTuple<Vehicle, Amphbian, String, Integer> i : tl) {
System.out.println(i);
}
}
}
尽管这看上去有些冗长(特别是迭代器的创建),但最终还是没有用过多的代码就得到了一个相当强大的数据结构。
下面是另一个示例,它展示了使用泛型类型来构建复杂模型是多么的简单。即使每个类都是作为一个构建块创建的,但是其整个还是包含许多部分。在本例中,构件的模型是一个零售店,它包含走廊、货架和商品:
package com.zjwave.thinkinjava.generics;
import java.lang.reflect.Array;
import java.util.ArrayList;
import java.util.Random;
public class Store extends ArrayList<Aisle>{
private ArrayList<CheckoutStand> 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));
}
}
@Override
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));
}
}
class Product{
private final int id;
private String description;
private double price;
public Product(int IDnumber, String description, double price) {
this.id = IDnumber;
this.description = description;
this.price = price;
System.out.println(toString());
}
@Override
public String toString() {
return "Product{" +
"id=" + id +
", description='" + description + '\'' +
", price=" + price +
'}';
}
public void priceChange(double change){
price += change;
}
public static Generator<Product> generator = new Generator<Product>() {
private Random rand = new Random(47);
@Override
public Product next() {
return new Product(rand.nextInt(1000),"Test",Math.round(rand.nextDouble() * 1000 + 0.99));
}
};
}
class Shelf extends ArrayList<Product>{
public Shelf(int nProducts){
Generators.fill(this,Product.generator,nProducts);
}
}
class Aisle extends ArrayList<Shelf> {
public Aisle(int nShelves,int nProducts){
for (int i = 0; i < nShelves; i++) {
add(new Shelf(nProducts));
}
}
}
class CheckoutStand{}
class Office{}
正如我们在Store.toString()中看到的,其结果是许多层容器,但是它们是类型安全且可管理的。令人印象深刻之处是组装这个的模型十分容易,并不会成为智力挑战。
7.擦除的神秘之处
当你开始更深入地钻研泛型时,会发现有大量的东西初看起来是没有意义的。例如,尽管可以声明ArrayList.class,但是不能声明ArrayList<Integer>.class。请考虑下面的情况:
package com.zjwave.thinkinjava.generics;
import java.util.ArrayList;
public class ErasedTypeEquivalence {
public static void main(String[] args) {
Class c1 = new ArrayList<String>().getClass();
Class c2 = new ArrayList<Integer>().getClass();
System.out.println(c1 == c2);
}
}
ArrayList<String>和ArrayList<Integer>很容易被认为是不同的类型。不同的类型在行为方面肯定不同,例如,如果尝试着将一个Integer放入ArrayList<String>,所得到的行为(将失败)与把一个Integer放入ArrayList<Integer>(将成功)所得到的行为完全不同。但是上面的程序会认为它们是相同的类型。
下面的示例是对这个问题的一个补充:
package com.zjwave.thinkinjava.generics;
import java.util.*;
public class LostInfomation {
public static void main(String[] args) {
List<Frob> list = new ArrayList<>();
Map<Frob,Fnorkle> map = new HashMap<>();
Quark<Fnorkle> quark = new Quark<>();
Particle<Long,Double> 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()));
}
}
class Frob{}
class Fnorkle{}
class Quark<Q>{}
class Particle<POSITION,MOMENTUM>{}
根据JDK文档的描述,Class.getTypeParameters()将“返回一个TypeVariable对象数组,表示有反省声明所声明的类型参数…”这好像是在暗示你可能发现参数类型的信息,但是正如你从输出中所看到的,你能够发现的只是用作参数占位符的标识符,这并非有用的信息。
因此,残酷的现实是:
在泛型代码内部,无法获得任何有关泛型参数类型的信息。
因此,你可以知道注入类型参数标识符和泛型类型边界这类的信息——你却无法知道用来创建某个特定实例的实际的类型参数。如果你曾经是C++程序员,那么这个事实肯定让你觉得很沮丧,在使用Java泛型工作时它是必须处理的最基本的问题。
Java泛型是使用擦除来实现的,这意味着当你在使用泛型时,任何具体的类型信息都被擦除了,你唯一知道的就是你在使用一个对象。因此List<String>和List<Integer>在运行时事实上是相同的类型。这两种形式都被擦除成它们的“原生”类型,即List。理解擦除以及应该如何处理它,是你在学习Java泛型时面临的最大障碍。
7.1 C++的方式
下面是使用模板的C++示例,你将注意到用于参数化类型的语法十分相似,因为Java是受C++的启发:
//: generics/Templates.cpp
#include <iostream>
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<HasF> manipulator(hf);
manipulator.manipulate();
}
/*Output:
HasF::f()
*/
Manipulator类存储了一个类型T的对象,有意思的地方是manipulate()方法,它在obj上调用方法f()。它怎么能知道f()方法是为类型参数T而存在的呢?当你实例化这个模板时,C++编译器将进行检查,因此Manipulator<HasF>被实例化的这一刻,它看到HasF拥有一个方法f()。如果情况并非如此,就会得到一个编译期错误,这样类型安全就得到了保障。
用C++编写这种代码很简单,因为当模板被实例化时,模板代码知道其模板参数的类型。Java泛型就不同了。下面是HasF的Java版本:
package com.zjwave.thinkinjava.generics;
public class HasF {
public void f(){
System.out.println("HasF.f()");
}
}
package com.zjwave.thinkinjava.generics;
public class Manipulation {
public static void main(String[] args) {
HasF hf = new HasF();
Manipulator<HasF> manipulator = new Manipulator<>(hf);
manipulator.manipulate();
}
}
class Manipulator<T>{
private T obj;
public Manipulator(T obj) {
this.obj = obj;
}
//Error: Cannot resolve method f()
public void manipulate(){
obj.f();
}
}
由于有了擦除,Java编译器无法将manipulate()必须能够在obj上调用f()这一需求映射到Hasf拥有f()这一事实上。为了调用f(),我们必须协助泛型类,给定泛型类的边界,以此告知编译器只能接受遵循这个边界的类型。这里重用了extends关键字。由于有了边界,下面的代码就可以编译了:
package com.zjwave.thinkinjava.generics;
class Manipulator2<T extends HasF> {
private T obj;
public Manipulator2(T obj) {
this.obj = obj;
}
public void manipulate(){
obj.f();
}
}
边界<T extends HasF>声明T必须具有类型HasF或者从HasF导出的类型。如果情况确实如此,那么就可以安全地在obj上调用f()了。
我们说泛型类型参数将擦除到它的第一个边界(它可能会有多个边界),我们还提到了类型参数的擦除。编译器实际上会把类型参数替换为它的擦除,就像上面的示例一样。T擦除到了HasF,就好像在类的声明中用HasF替换了T一样。
你可能已经正确地观察到,在Manipulator2.java中,泛型没有贡献任何好处。只需很容易地自己去执行擦除,就可以创建出没有泛型的类:
package com.zjwave.thinkinjava.generics;
class Manipulator3 {
private HasF obj;
public Manipulator3(HasF obj) {
this.obj = obj;
}
public void manipulate(){
obj.f();
}
}
这提出了很重要的一点:只有当你希望使用的类型参数比某个具体类型(以及它的所有子类型)更加“泛化”时——也就是说,当你希望代码能够跨多个类工作时,使用泛型才有所帮助。因此,类型参数和它们在有用的泛型代码中的应用,通常比简单的类替换要复杂。但是不能因此而认为<T extends HasF>形式的任何东西都是有缺陷的。例如,如果某个类有一个返回T的方法,那么泛型就有所帮助,因为它们之后将返回确切的类型:
package com.zjwave.thinkinjava.generics;
class ReturnGenericType<T extends HasF> {
private T obj;
public ReturnGenericType(T obj) {
this.obj = obj;
}
public T get(){
return obj;
}
}
必须查看所有的代码,并确定它是否“足够复杂”到必须使用泛型的程度。
7.2 迁移兼容性
为了减少潜在的关于擦除的混淆,你必须清楚地认识到这不是一个语言特性。它是Java的泛型实现中的一种折中,因为泛型不是Java语言出现时就有的组成部分,所以这种折中是必须的。这种折中会使你痛苦,因此你需要习惯它并了解为什么它是这样。
如果泛型在Java 1.0中就已经是其一部分了,那么这个特性将不会使用擦除来实现——它将使用具体换,使类型参数保持为一个类实体,因此你就能够在类型参数上执行基于类型的语言操作和反射操作。擦除减少了泛型的泛化性。泛型在Java中仍旧是有用的,只是不如它们本来设想的那么有用,而原因就是擦除。
在基于擦除的实现中,泛型类型被当做第二类类型处理,即不能在某些重要的上下文环境中使用的类型。泛型类型只有在静态类型检查期间才出现,在此之后,程序中的所有泛型类型都将被擦除,替换为它们的非泛型上界。例如,诸如List<T>这样的类型注解将被擦除为List,而普通的类型变量在未指定边界的情况下将被擦除为Object。
擦除的核心动机是它使得泛化的客户端可以用非泛化的类库来使用,反之亦然,这经常被称为“迁移兼容性”。在理想情况下,当所有事物都可以同时被泛化时,我们就可以专注于此。在现实中,即使程序员只编写泛型代码,他们也必须处理在Java SE5之前编写的非泛型类库。那些类库的作者可能从没有想过要泛化他们的代码,或者可能刚刚开始接触泛型。
因此Java泛型不仅必须支持向后兼容性,即现有的代码和类文件仍旧合法,并且继续保持其之前的含义;而且还要支持迁移兼容性,使得类库按照它们自己的步调变为泛型的,并且当某个类库变为泛型时,不会破坏依赖于它的代码和应用程序。在决定这就是目标之后,Java设计者们和从事此问题相关工作的各个团队决策认为擦除是唯一可行的解决方案。通过允许非泛型代码与泛型代码共存,擦除使得这种向着泛型的迁移称为可能。
例如,假设某个应用程序具有两个类库X和Y,并且Y还要使用类库Z。随着Java SE5的出现,这个应用程序和这些类库的创建者最终可能希望迁移到泛型上。但是,当进行这种迁移时,他们有着不同动机和限制。为了实现迁移兼容性,每个类库和应用程序都必须与其他所有的部分是否使用了泛型无关。这样,它们必须不具备探测其他类库是否使用了泛型的能力。因此,某个特定的类库使用了泛型这样的证据必须被“擦除”。
如果没有某种类型的迁移途径,所有已经构建了很长时间的类库就需要与希望迁移到Java泛型上的开发者们说再见了。但是,类库是编程语言无可争议的一部分,它们对生产效率会产生最重要的影响,因此这不是一种可以接受的代价。擦除是否是最佳的或者唯一的迁移途径,还需要时间来证明。
7.3 擦除的问题
因此,擦除主要的正当理由是从非泛化代码到泛化代码的转变过程,以及在不破坏现有类库的情况下,将泛型融入Java语言。擦除使得现有的非泛型客户端代码能够在不改变的情况下继续使用,直至客户端准备好用泛型重写这些代码。这是一个崇高的动机,因为它不会突然间破坏所有现有的代码。
擦除的代价是显著的。反省不能用于显式地引用运行时类型的操作之中,例如转型、instanceof操作和new表达式。因为所有关于参数的类型信息都丢失了,无论何时,当你在编写泛型代码时,必须时刻提醒自己,你只是看起来好像拥有有关参数的类型信息而已。因此,如果你编写了下面这样的代码段:
class Foo<T>{
T var;
}
那么,看起来当你在创建Foo的实例时:
Foo<Cat> f = new Foo<Cat>();
class Foo中的代码应该知道现在工作于Cat之上,而反省语法也在强烈暗示:在整个类中的各个地方,类型T都在被替换。但是事实并非如此,无论何时,当你在编写这个类的代码时,必须提醒自己:“它只是一个Object。”
另外,擦除和迁移兼容性意味着,使用泛型并不是强制的,尽管你可能希望这样:
package com.zjwave.thinkinjava.generics;
public class ErasureAndInheritance {
@SuppressWarnings("unchecked")
public static void main(String[] args) {
Derived2 d2 = new Derived2();
Object obj = d2.get();
d2.set(obj);//Warning here!
}
}
class GenericBase<T>{
private T element;
public void set(T arg){
element = arg;
}
public T get(){
return element;
}
}
class Derived1<T> extends GenericBase<T>{}
class Derived2<T> extends GenericBase{}// No warning
//class Derived3 extends GenericBase<?>{}
//unexpected type found : ?
//required: class or interface without bounds
Derived2继承自GenericBase,但是没有任何泛型参数,而编译器不会发出任何警告。警告在set()被调用时才会出现。
为了关闭警告,Java提供了一个注解,我们可以在列表中看到它(这个注解在Java SE5之前的版本中不支持):
@SuppressWarnings("unchecked")
注意,这个注解被放置在可以产生这类警告的方法之上,而不是整个类上。当你要关闭警告时,最好是尽量地“聚焦”,这样就不会因为过于宽泛地关闭警告,而导致意外地遮蔽掉真正的问题。
可以推断,Derived3产生的错误意味着编译器期望得到一个原生基类。
当你希望将类型参数不要仅仅当做Object处理时,就需要付出额外努力来管理边界,并且与在C++、Ada和Eiffel这样的语言中获得参数化类型相比,你需要付出多得多的努力来获得少得多的回报。这并不是说,对于大多数的编程问题而言,这些语言通常都会比Java更得心应手;这只是说,它们的参数化类型机制比Java的更灵活、更强大。
7.4 边界处的动作
正式因为有了擦除,我发现泛型最令人困惑的方面源自这样一个事实,即表示没有任何意义的事物。例如:
package com.zjwave.thinkinjava.generics;
import java.lang.reflect.Array;
import java.util.Arrays;
public class ArrayMaker<T> {
private Class<T> kind;
public ArrayMaker(Class<T> kind) {
this.kind = kind;
}
@SuppressWarnings("unchecked")
T[] create(int size){
return (T[]) Array.newInstance(kind,size);
}
public static void main(String[] args) {
ArrayMaker<String> stringMacker = new ArrayMaker<>(String.class);
String[] stringArray = stringMacker.create(9);
System.out.println(Arrays.toString(stringArray));
}
}
即使kind被存储为Class<T>,擦除也意味着它实际将被存储为Class,没有任何参数。因此,当你在使用它时,例如在创建数组时,Array.newInstance()实际上并未拥有kind所蕴含的类型信息,因此这不会产生具体的结果,所以必须转型,这将产生一条令你无法满意的警告。
注意,对于在泛型中创建数组,使用Array.newInstance()是推荐的方式。
如果我们要创建一个容器而不是数组,情况就有些不同了:
package com.zjwave.thinkinjava.generics;
import java.util.ArrayList;
import java.util.List;
public class ListMaker<T> {
List<T> create(){
return new ArrayList<>();
}
public static void main(String[] args) {
ListMaker<String> stringMaker = new ListMaker<>();
List<String> stringList = stringMaker.create();
}
}
编译器不会给出任何警告,尽管我们(从擦除中)知道在create()内部的new ArrayList<T>中的<T>被移除了——在运行时,这个类的内部没有任何<T>,因此这看起来毫无意义。但是如果你遵从这种思路,并将表达式改为new ArrayList(),编译器就会给出警告。
在本例中,这是否真的毫无意义呢?如果返回list之前,将某些对象放入其中,就像下面这样,情况又会如何呢?
package com.zjwave.thinkinjava.generics;
import java.util.ArrayList;
import java.util.List;
public class FilledListMaker<T> {
List<T> create(T t,int n){
List<T> result = new ArrayList<>();
for (int i = 0; i < n; i++) {
result.add(t);
}
return result;
}
public static void main(String[] args) {
FilledListMaker<String> stringMaker = new FilledListMaker<>();
List<String> list = stringMaker.create("Hello", 4);
System.out.println(list);
}
}
即使编译器无法知道有关create()中的T的任何信息,但是它仍旧可以在编译器确保你放置到result中的对象具有T类型,使其适合ArrayList<T>。因此,即使擦除在方法或类内部移除了有关实际类型的信息,编译器仍旧可以确保在方法或类中使用的类型的内部一致性。
因为擦除在方法体中移除了类型信息,所以在运行时的问题就是边界:即对象进入和离开方法的地点。这些正是编译器在编译期执行类型检查并插入转型代码的地点。请考虑下面的非泛型示例:
package com.zjwave.thinkinjava.generics;
public class SimpleHolder {
private Object obj;
public Object get() {
return obj;
}
public void set(Object obj) {
this.obj = obj;
}
public static void main(String[] args) {
SimpleHolder holder = new SimpleHolder();
holder.set("Item");
String s = (String) holder.get();
}
}
如果用javap -c SimpleHolder反编译这个类,就可以得到下面的(经过编译的)内容:
public java.lang.Object get();
Code:
0: aload_0
1: getfield #2 // Field obj:Ljava/lang/Object;
4: areturn
public void set(java.lang.Object);
Code:
0: aload_0
1: aload_1
2: putfield #2 // Field obj:Ljava/lang/Object;
5: return
public static void main(java.lang.String[]);
Code:
0: new #3 // class com/zjwave/thinkinjava/generics/SimpleHolder
3: dup
4: invokespecial #4 // Method "<init>":()V
7: astore_1
8: aload_1
9: ldc #5 // String Item
11: invokevirtual #6 // Method set:(Ljava/lang/Object;)V
14: aload_1
15: invokevirtual #7 // Method get:()Ljava/lang/Object;
18: checkcast #8 // class java/lang/String
21: astore_2
22: return
set()和get()方法将直接存储和产生值,而转型是在调用get()的时候接受检查的。
现在将泛型合并到上面的代码中:
package com.zjwave.thinkinjava.generics;
public class GenericHolder<T> {
private T obj;
public T get() {
return obj;
}
public void set(T obj) {
this.obj = obj;
}
public static void main(String[] args) {
GenericHolder<String> holder = new GenericHolder<>();
holder.set("Item");
String s = holder.get();
}
}
从get()返回之后的转型消失了,但是我们还知道传递给set()的值在编译期会接受检查。下面是相关的字节码:
public T get();
Code:
0: aload_0
1: getfield #2 // Field obj:Ljava/lang/Object;
4: areturn
public void set(T);
Code:
0: aload_0
1: aload_1
2: putfield #2 // Field obj:Ljava/lang/Object;
5: return
public static void main(java.lang.String[]);
Code:
0: new #3 // class com/zjwave/thinkinjava/generics/GenericHolder
3: dup
4: invokespecial #4 // Method "<init>":()V
7: astore_1
8: aload_1
9: ldc #5 // String Item
11: invokevirtual #6 // Method set:(Ljava/lang/Object;)V
14: aload_1
15: invokevirtual #7 // Method get:()Ljava/lang/Object;
18: checkcast #8 // class java/lang/String
21: astore_2
22: return
所产生的字节码是相同的。对进入set()的类型进行检查是不需要的,因为这将由编译器执行。而对从get()返回的值进行转型仍旧是需要的,但这与你自己必须执行的操作是一样的——此处它将由编译器自动插入,因此你写入(和读取)的代码的噪声将更小。
由于所产生的get()和set()的字节码相同,所以在泛型中的所有动作都发生在边界处——对传递进来的值进行额外的编译器检查,并插入对传递出去的值的转型。这将有助于澄清对擦除的混淆,记住,“边界就是发生动作的地方。”
8.擦除的补偿
正如我们看到的,擦除丢失了在泛型代码中执行某些操作的能力。任何在运行时需要知道确切类型信息的操作都将无法工作:
package com.zjwave.thinkinjava.generics;
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];//Error
}
}
偶尔可以绕过这些问题来编程,但是有时必须通过引入类型标签来对擦除进行补偿。这意味着你需要显示地传递你的类型的Class对象,以便你可以在类型表达式中使用它。
例如,在前面示例中对使用instanceof的尝试最终失败了,因为其类型信息已经被擦除了。如果引入类型标签,就可以转而使用动态的isInstance():
package com.zjwave.thinkinjava.generics;
public class ClassTypeCapture<T> {
Class<T> kind;
public ClassTypeCapture(Class<T> kind) {
this.kind = kind;
}
public boolean f(Object arg){
return kind.isInstance(arg);
}
public static void main(String[] args) {
ClassTypeCapture<Building> cttl = new ClassTypeCapture<>(Building.class);
System.out.println(cttl.f(new Building()));
System.out.println(cttl.f(new House()));
ClassTypeCapture<House> cttl2 = new ClassTypeCapture<>(House.class);
System.out.println(cttl2.f(new Building()));
System.out.println(cttl2.f(new House()));
}
}
class Building{}
class House extends Building{}
编译器将确保类型标签可以匹配泛型参数。
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<Bar> fb;
Foo<int> fi; // ... and it works with primitives
}
Java中的解决方案是传递一个工厂对象,并使用它来创建新的实例。最便利的工厂对象就是Class对象,因此如果使用类型标签,那么你就可以使用newInstance()来创建这个类型的新对象:
package com.zjwave.thinkinjava.generics;
public class InstantiateGenericType {
public static void main(String[] args) {
ClassAsFactory<Employee> fe = new ClassAsFactory<>(Employee.class);
System.out.println("ClassAsFactory<Employee> succeeded");
try {
ClassAsFactory<Integer> fi = new ClassAsFactory<>(Integer.class);
}catch (Exception e) {
System.out.println("ClassAsFactory<Employee> failed");
}
}
}
class ClassAsFactory<T>{
T x;
public ClassAsFactory(Class<T> kind) {
try {
this.x = kind.newInstance();
} catch (Exception e) {
throw new RuntimeException(e);
}
}
}
class Employee{}
这可以编译,但是会因ClassAsFactory<Integer>而失败,因为Integer没有任何默认的构造器。因为这个错误不是在编译器捕获的,所以建议使用显式的工厂,并将限制其类型,使得只能接受是实现了这个工厂的类:
package com.zjwave.thinkinjava.generics;
public class FactoryConstraint {
public static void main(String[] args) {
new Foo2<>(new IntegerFactory());
new Foo2<>(new Widget.Factory());
}
}
interface FactoryI<T>{
T create();
}
class Foo2<T>{
private T x;
public <F extends FactoryI<T>> Foo2(F factory){
x = factory.create();
}
}
class IntegerFactory implements FactoryI<Integer>{
@Override
public Integer create() {
return new Integer(0);
}
}
class Widget{
public static class Factory implements FactoryI<Widget>{
@Override
public Widget create() {
return new Widget();
}
}
}
注意,这确实只是传递Class<T>的一种变体。两种方式都传递了工厂对象,Class<T>碰巧是内建的工厂对象,而上面的方式创建了一个显式的工厂对象,但是你却获得了编译期检查。
另一种方式是模板方法设计模式。在下面的示例中,get()是模板方法,而create()是在子类中定义的、用来产生子类类型的对象:
package com.zjwave.thinkinjava.generics;
public class CreatorGeneric {
public static void main(String[] args) {
Creator c = new Creator();
c.f();
}
}
abstract class GenericWithCreate<T>{
final T element;
public GenericWithCreate() {
this.element = create();
}
abstract T create();
}
class X{}
class Creator extends GenericWithCreate<X>{
@Override
X create() {
return new X();
}
void f(){
System.out.println(element.getClass().getSimpleName());
}
}
8.2 泛型数组
正如你在Erased.java中所见,不能创建泛型数组。一般的解决方案是在任何想要创建泛型数组的地方都使用ArrayList:
package com.zjwave.thinkinjava.generics;
import java.util.ArrayList;
import java.util.List;
public class ListOfGenerics<T> {
private List<T> array = new ArrayList<>();
public void add(T item){
array.add(item);
}
public T get(int index){
return array.get(index);
}
}
这里你将获得数组的行为,以及由泛型提供的编译期的类型安全。
有时,你仍旧希望创建泛型类型的数组(例如,ArrayList内部使用的是数组)。有趣的是,可以按照编译器喜欢的方式来定义一个引用,例如:
package com.zjwave.thinkinjava.generics;
public class ArrayOfGenericReference {
static Generic<Integer>[] gia;
}
class Generic<T>{}
编译器将接受这个程序,而不会产生任何警告。但是,永远都不能创建这个确切类型的数组(包括类型参数),因此这有一点令人困惑。既然所有数组无论它们持有的类型如何,都具有相同的结构(每个数组槽位的尺寸和数组的布局),那么看起来你应该能够创建一个Object数组,并将其转型为所希望的数组类型。事实上这可以编译,但是不能运行,它将产生ClassCastException:
package com.zjwave.thinkinjava.generics;
public class ArrayOfGenericReference {
static final int SIZE = 100;
static Generic<Integer>[] gia;
@SuppressWarnings("unchecked")
public static void main(String[] args) {
// Compiles: produces ClassCastException:
//Required:
//com.zjwave.thinkinjava.generics.Generic<java.lang.Integer>[]
//Found:
//java.lang.Object[]
//! gia = new Object[SIZE];
//Runtime type is the raw (erased) type
gia = new Generic[SIZE];
System.out.println(gia.getClass().getSimpleName());
gia[0] = new Generic<>();
//! gia[1] = new Object(); // Compile-to,e error
// Discovers type mismatch at compile time:
//! gia[2] = new Generic<Double>();
}
}
class Generic<T>{}
问题在于数组将跟踪它们的实际类型,而这个类型是在数组被创建时确定的,因此,即使gia已经被转型为Generic<Integer>[],但是这个信息只存在于编译期(并且如果没有@SuppressWarnings注解,你将得到有关这个转型的警告)。在运行时,它仍旧是Object数组,而这将引发问题。成功创建泛型数组的唯一方式就是创建一个被擦除类型的新数组,然后对其转型。
让我们看一个更复杂的示例。考虑一个简单的泛型数组包装器:
package com.zjwave.thinkinjava.generics;
public class GenericArray<T> {
private T[] array;
@SuppressWarnings("unchecked")
public GenericArray(int sz) {
this.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<Integer> gai = new GenericArray<>(10);
//This causes a ClassCastException
//! Integer[] rep = gai.rep();
// This is OK :
Object[] oa = gai.rep();
}
}
与前面相同,我们并不能声明T[] array = new T[sz],因此我们创建了一个对象数组,然后将其转型。
rep()方法将返回T[],它在main()中将用于gai,因此应该是Integer[],但是如果调用它,并尝试着将结果作为Integer[]引用来捕获,就会得到ClassCastException,这还是因为实际的运行时类型是Object[]。
如果在注释掉@SuppressWarnings注解之后再编译GenericArray.java,编译器就会产生警告:
在这种情况下,我们将只获得单个的警告,并且相信这事关转型。但是如果真的想要确定是否是这么回事,就应该用-Xlint:unchecked来编译:
这确实是对转型的抱怨。因为警告会变得令人迷惑,所以一旦我们验证某个特定警告是可以预期的,那么我们的上策就是用@SuppressWarnings关闭它。通过这种方式,当警告确实出现时,我们就可以真正地展开对它的调查了。
因为有两个擦除,数组的运行时类型就只能是Object[]。如果我们立即将其转型为T[],那么在编译期该数组的实际类型就将丢失,而编译器可能会错过某些潜在的错误检查。正因为这样,最好是在集合内部使用Object[],然后当你使用数组元素时,添加一个对T的转型。让我们看看这是如何作用于GenericArray.java示例的:
package com.zjwave.thinkinjava.generics;
public class GenericArray2<T> {
private Object[] array;
public GenericArray2(int sz) {
this.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<Integer> 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);
}
}
}
初看起来,这好像没多大变化,只是转型挪了地方。如果没有@SuppressWarnings("unchecked")注解,你仍旧会得到unchecked警告。但是,现在的内部表示是Object[]而不是T[]。当get()被调用时,它将对象转型为T,这实际上是正确的类型,因此这是安全的。然而,如果你调用rep(),它还是尝试着将Object[]转型为T[],这仍旧是不正确的,将在编译期产生警告,在运行时产生异常。因此,没有任何方式可以推翻底层的数组类型,它只能是Object[]。在内部将array当做Object[]而不是T[]处理的优势是:我们不太可能忘记这个数组的运行时类型,从而意外地引入缺陷(尽管大多数也可能是所有这类缺陷都可以在运行时快速地探测到)。
对于新代码,应该传递一个类型标记。在这种情况下,GenericArray看起来会像下面这样:
package com.zjwave.thinkinjava.generics;
import java.lang.reflect.Array;
public class GenericArrayWithTypeToken<T> {
private T[] array;
public GenericArrayWithTypeToken(Class<T> 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];
}
public T[] rep(){
return array;
}
public static void main(String[] args) {
GenericArrayWithTypeToken<Integer> gai = new GenericArrayWithTypeToken<>(Integer.class,10);
// This now works
Integer[] ia = gai.rep();
}
}
类型标记Class<T>被传递到构造器中,以便从擦除中恢复,使得我们可以创建需要的实际类型的数组,尽管从转型中产生的警告必须用@SuppressWarnings("unchecked")压制住。一旦我们获得了实际类型,就可以返回它,并获得想要的结果,就像在main()中看到的那样。该数组的运行时类型是确切类型T[]。
遗憾的是,如果查看Java SE5标准类库中的源代码,你就会看到从数组到参数化类型的转型遍及各处。例如,下面是警告过整理和简化后的从Collection中复制ArrayList的构造器:
public ArrayList(Collection c) {
size = c.size();
elementData = (E[])new Object[size];
c.toArray(elementData);
}
如果你通读ArrayList.java,就会发现它充满了这种转型。如果我们编译它,又会发生什么呢?
可以十分肯定,标准类库会产生大量的警告。如果你曾经用过C++,特别是ANSI C之前的版本,你就会记得警告的特殊效果:当你发现可以忽略它们时,你就可以忽略。正是因为这个原因,最好是从编译器中不要发出任何消息,除非程序员必须对其进行相应。
Neal Gafter(Java SE5的领导开发者之一)在他的博客中指出,在重写Java类库时,他十分懒散,而我们不应该像他那样。Neal还指出,在不破坏现有接口的情况下,他将无法修改某些Java类库代码。因此,即使在Java类库源代码中出现了某些惯用法,也不能表示这就是正确的解决之道。当查看类库源码时,你不能认为它就是应该在自己的代码中遵循的示例。
9.边界
边界使得你可以在用于泛型的参数类型上设置限制条件。尽管这使你可以强制规定泛型可以应用的类型,但是其潜在的一个更重要的效果是你可以按照自己的边界类型来调用方法。
因为擦除移除了类型信息,所以,可以用无界泛型参数调用的方法只是那些可以用Object调用的方法。但是,如果能够将这个参数限制为某个类型子集,那么你就可以用这些类型子集来调用方法。为了执行这种限制,Java泛型重用了extends关键字。对你来说有一点很重要,即要理解extends关键字在泛型边界上下文环境中和在普通情况下所具有的意义是完全不同的。下面的示例展示了边界的基本要素:
package com.zjwave.thinkinjava.generics;
import java.awt.*;
public class BasicBounds {
public static void main(String[] args) {
Solid<Bounded> solid = new Solid<>(new Bounded());
solid.color();
solid.getY();
solid.weight();
}
}
interface HasColor{
Color getColor();
}
class Colored<T extends HasColor>{
T item;
public Colored(T item) {
this.item = item;
}
// The bound allows you to call a method:
Color color(){
return item.getColor();
}
}
class Dimension{
public int x,y,z;
}
// This won't work -- class must be first,then interfaces:
//class ColoredDimension<T extends HasColor & Dimension>{}
//Multiple bounds
class ColoredDimension<T extends Dimension & HasColor>{
T item;
public ColoredDimension(T item) {
this.item = item;
}
Color color(){
return item.getColor();
}
public int getX() {
return item.x;
}
public int getY() {
return item.y;
}
public int getZ() {
return item.z;
}
public T getItem() {
return item;
}
}
interface Weight{
int weight();
}
// As with inheritance,you can have only one
// concrete class but multiple interfaces:
class Solid<T extends Dimension & HasColor & Weight>{
T item;
public Solid(T item) {
this.item = item;
}
Color color(){
return item.getColor();
}
public int getX() {
return item.x;
}
public int getY() {
return item.y;
}
public int getZ() {
return item.z;
}
public T getItem() {
return item;
}
int weight(){
return item.weight();
}
}
class Bounded extends Dimension implements HasColor,Weight{
@Override
public Color getColor() {
return null;
}
@Override
public int weight() {
return 0;
}
}
你可能已经观察到了,BasicBounds.java看上去包含可以通过继承消除的冗余。下面,可以看到如何在继承的每个层次上添加边界限制:
package com.zjwave.thinkinjava.generics;
import java.awt.*;
public class InheritBounds {
public static void main(String[] args) {
Solid2<Bounded> solid2 = new Solid2<>(new Bounded());
solid2.color();
solid2.getY();
solid2.weight();
}
}
class HoldItem<T> {
T item;
public HoldItem(T item) {
this.item = item;
}
public T getItem() {
return item;
}
}
class Colored2<T extends HasColor> extends HoldItem<T> {
public Colored2(T item) {
super(item);
}
Color color() {
return item.getColor();
}
}
class ColoredDimension2<T extends Dimension & HasColor> extends Colored2<T> {
public ColoredDimension2(T item) {
super(item);
}
public int getX() {
return item.x;
}
public int getY() {
return item.y;
}
public int getZ() {
return item.z;
}
}
class Solid2<T extends Dimension & HasColor & Weight> extends ColoredDimension2<T>{
public Solid2(T item) {
super(item);
}
int weight(){
return item.weight();
}
}
HoldItem直接持有一个对象,因此这种行为被继承到了Colored2中,它也要求其参数与HasColor一致。ColoredDimension2和Solid2进一步扩展了这个层次结构,并在每个层次上都添加了边界。现在这些方法被继承,因而不必在每个类中重复。
下面是具有更多层次的示例:
package com.zjwave.thinkinjava.generics;
import java.util.List;
public class EpicBattle {
//Bounds in generic methods:
static <POWER extends SuperHearing> void useSuperHearing(SuperHero<POWER> hero){
hero.getPower().hearSubtleNoises();
}
static <POWER extends SuperHearing & SuperSmell> void superFind(SuperHero<POWER> 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<? extends SuperHearing> audioBoys;
//But you can't do this
//List<? extends SuperHearing & SuperSmell> dogBoys;
}
}
interface SuperPower{}
interface XRayVision extends SuperPower{
void seeThroughWalls();
}
interface SuperHearing extends SuperPower{
void hearSubtleNoises();
}
interface SuperSmell extends SuperPower{
void trackBySmell();
}
class SuperHero<POWER extends SuperPower>{
POWER power;
public SuperHero(POWER power) {
this.power = power;
}
POWER getPower(){
return power;
}
}
class SuperSleuth<POWER extends XRayVision> extends SuperHero<POWER>{
public SuperSleuth(POWER power) {
super(power);
}
void see(){
power.seeThroughWalls();
}
}
class CanineHero<POWER extends SuperHearing & SuperSmell> extends SuperHero<POWER>{
public CanineHero(POWER power) {
super(power);
}
void hear(){
power.hearSubtleNoises();
}
void smell(){
power.trackBySmell();
}
}
class SuperHearSmell implements SuperHearing,SuperSmell{
@Override
public void hearSubtleNoises() {
}
@Override
public void trackBySmell() {
}
}
class DogBoy extends CanineHero<SuperHearSmell> {
public DogBoy() {
super(new SuperHearSmell());
}
}
注意,通配符被限制为单一边界。
10.通配符
我们开始入手的示例要展示数组的一种特殊行为:可以向导出类型的数组赋予基类型的数组引用:
package com.zjwave.thinkinjava.generics;
public class CoVariantArrays {
public static void main(String[] args) {
Fruit[] fruits = new Apple[10];
fruits[0] = new Apple();
fruits[1] = new Jonathan();
// Runtime type is Apple[],not Fruit[] or Orange[]
try{
// Compiler allows you to add Fruit:
fruits[0] = new Fruit();// ArrayStoreException
}catch (Exception e){
System.out.println(e);
}
try{
// Compiler allows you to add Oranges:
fruits[0] = new Orange();// ArrayStoreException
}catch (Exception e){
System.out.println(e);
}
}
}
class Fruit{}
class Apple extends Fruit{}
class Jonathan extends Apple{}
class Orange extends Fruit{}
main()中的第一行创建了一个Apple数组,并将其赋值给一个Fruit数组引用。这是有意义的,因为Apple也是一种Fruit,因此Apple数组应该也是一个Fruit数组。
但是,如果实际的数组类型是Apple[],你应该只能在其中放置Apple或Apple的子类型,这在编译期和运行时都可以工作。但是请注意,编译器允许你将Fruit放置到这个数组中,这对于编译器来说是有意义的,因为它有一个Fruit[]引用——它有什么理由不允许将Fruit对象或者任何从Fruit集成出来的对象(例如Orange),放置到这个数组中呢?因此,在编译期,这是允许的。但是,运行时的数组机制知道它处理的是Apple[],因此会在向数组中放置异构类型时抛出异常。
实际上,向上转型不合适用在这里。你真正做的是将一个数组赋值给另一个数组。数组的行为应该是它可以持有其他对象,这里只是因为我们能够向上转型而已,所以很明显,数组对象可以保留有关它们包含的对象类型的规则。就好像数组对它们持有的对象是有意识的,因此在编译期检查和运行时检查之间,你不能滥用它们。
对数组的这种赋值并不是那么可怕,因为在运行时可以发现你已经插入了不正确的类型。但是泛型的主要目标之一是将这种错误检测移入到编译期。因此当我妈试图使用泛型容器来代替数组时,会发生什么呢?
package com.zjwave.thinkinjava.generics;
import java.util.ArrayList;
import java.util.List;
public class NonCovariantGenerics {
// Compile Error : Incompatible types.
//List<Fruit> flist = new ArrayList<Apple>();
}
尽管你在第一次阅读这段代码时会认为:“不能将一个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类型。
真正的问题是我们在谈论容器的类型,而不是容器持有的类型。与数组不同,泛型没有内建的协变类型。这是因为数组在语言中是完全定义的,因此可以内建了编译期和运行时的检查,但是在使用泛型时,编译器和运行时系统都不知道你想用类型做什么,以及应该采用什么样的规则。
但是,有时你想要在两个类型之间建立某种类型的向上转型关系,这正是通配符所允许的:
package com.zjwave.thinkinjava.generics;
import java.util.ArrayList;
import java.util.List;
public class GenericsAndCovariance {
public static void main(String[] args) {
//Wildcards allow covariance:
List<? extends Fruit> flist = new ArrayList<Apple>();
// Compile Error: can't add any type of object:
//flist.add(new Apple());
//flist.add(new Fruit());
//flist.add(new Object());
flist.add(null);
//We know that it returns at least Fruit:
Fruit f = flist.get(0);
}
}
flist类型现在是List<? extends Fruit>,你可以将其读作“具有任何从Fruit继承的类型的列表”。但是,这实际上并不意味着这个List将持有任何类型的Fruit。通配符引用的是明确的类型,因此它意味着“某种flist引用没有指定的具体类型”。因此这个被赋值的List必须持有诸如Fruit或Apple这样的某种指定类型,但是,为了向上转型为flist,这个类型是什么并没有人关心。
如果唯一的限制是这个List要持有某种具体的Fruit或Fruit的子类型,但是你实际上并不关心它是什么,那么你能用这样的List做什么呢?如果不知道List持有什么类型,那么你怎样才能安全地向其中添加对象呢?就像在CoVariantArrays.java中向上转型数组一样,你不能,除非编译器而不是运行时系统可以阻止这种操作的发生。你很快就会发现这一问题。
你可能会认为,事情变得有点走极端了,因为现在你甚至不能向刚刚声明过将持有Apple对象的List中放置一个Apple对象了。是的,但是编译器并不知道这一点。List<? extends Fruit>可以合法地指向一个List<Orange>。一旦执行这种类型的向上转型,你就将丢失掉向其中传递任何对象的能力,甚至是传递Object也不行。
另一方面,如果你调用一个返回Fruit的方法,则是安全的,因为你知道在这个List中的任何对象至少具有Fruit类型,因此编译器将允许这么做。
10.1 编译器有多聪明
现在,你可能会猜想自己被阻止去调用任何接受参数的方法,但是请考虑下面的程序:
package com.zjwave.thinkinjava.generics;
import java.util.Arrays;
import java.util.List;
public class CompilerIntelligence {
public static void main(String[] args) {
List<? extends Fruit> flist = Arrays.asList(new Apple());
Apple a = (Apple) flist.get(0);
flist.contains(new Apple());//Argument is 'Object'
flist.indexOf(new Apple());//Argument is 'Object'
}
}
你可以看到,对contains()和indexOf()的调用,这两个方法都接受Apple对象作为参数,而这些调用都可以正常执行。这是否意味着编译器实际上将检查代码,以查看是否有某个特定的方法修改了它的对象?
通过查看ArrayList的文档,我们可以发现,编译器并没有那么聪明。尽管add()将接受一个具有泛型参数类型的参数,但是contains()和indexOf()将接受Object类型的参数。因此当你指定一个ArrayList<? extends Fruit>时,add()参数就变成了“? extends Fruit”。从这个描述中,编译器并不能了解这里需要Fruit的哪个具体子类型,因此它不会接受任何类型的Fruit。如果先将Apple向上转型为Fruit,也无关紧要——编译器将直接拒绝对参数列表中设计通配符的方法(例如add())的调用。
在使用contains()和indexOf()时,参数类型是Object,因此不涉及任何通配符,而编译器也将允许这个调用。这意味着将由泛型类的设计者来决定哪些调用是“安全的”,并使用Object类型作为其参数类型。为了在类型中使用了通配符的情况下禁止这类调用,我们需要在参数列表中使用类型参数。
可以在一个非常简单的Holder类中看到这一点:
package com.zjwave.thinkinjava.generics;
public class Holder<T> {
private T value;
public Holder(T value) {
this.value = value;
}
public Holder() {
}
public T get() {
return value;
}
public void set(T value) {
this.value = value;
}
@Override
public boolean equals(Object obj) {
return value.equals(obj);
}
public static void main(String[] args) {
Holder<Apple> apple = new Holder<>(new Apple());
Apple d = apple.get();
apple.set(d);
//Holder<Fruit> fruit = apple; // Cannot upcast
Holder<? extends Fruit> fruit = apple;//OK
Fruit p = fruit.get();
d = (Apple) fruit.get();//Returns 'Object'
try{
Orange c = (Orange) fruit.get();//No warning
}catch (Exception e){
//fruit.set(new Apple()); //Cannot call set()
//fruit.set(new Fruit()); //Cannot call set()
System.out.println(e);
System.out.println(fruit.equals(d));//OK
}
}
}
Holder有一个接受T类型对象的set()方法,一个get()方法,以及一个接受Object对象的equals()方法。正如你已经看到的,如果创建了一个Holder<Apple>,不能将其向上转型为Holder<Fruit>,但是可以将其向上转型为Holder<? extends Fruit>。如果调用get(),它只会返回一个Fruit——这就是在给定“任何扩展自Fruit的对象”这一边界之后,它所能知道的一切了。如果能够了解更多的信息,那么你可以转型到某种具体的Fruit类型,而这不会导致任何警告,但是你存在着得到一个ClassCastException的风险。set()方法不能工作于Apple或Fruit,因为set()的参数也是“? extends Fruit”,这意味着它可以是任何事物,而编译器无法验证“任何事物”的类型安全性。
但是,equals()方法工作良好,因为它将接受Object类型而非T类型的参数。因此,编译器只关注传递进来和要返回的对象类型,它并不会分析代码,以查看是否执行了任何实际的写入和读取操作。
10.2 逆变
还可以走另外一条路,即使用超类型通配符。这里可以声明通配符是由某个特定类的任何基类来界定的,方法是指定<? super MyClass>,甚至或者使用类型参数:<? super T>(尽管你不能对泛型参数给出一个超类型边界;即不能声明<T super MyClass>)。这使得你可以安全地传递一个类型对象到泛型类型中。因此,有了超类型通配符,就可以向Collection写入了:
package com.zjwave.thinkinjava.generics;
import java.util.List;
public class SuperTypeWildcards {
static void writeTo(List<? super 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类型的对象,而这是违反静态类型安全的。
因此你可能会根据如何能够向一个泛型类型“写入”(传递给一个方法),以及如何能够从一个泛型类型中“读取”(从一个方法中返回),来着手思考四类型和超类型边界。
超类型边界放松了在可以向方法传递的参数上所作的限制:
package com.zjwave.thinkinjava.generics;
import java.util.ArrayList;
import java.util.List;
public class GenericWriting {
static <T> void writeExact(List<T> list ,T item){
list.add(item);
}
static List<Apple> apples = new ArrayList<>();
static List<Fruit> fruit = new ArrayList<>();
static void f1(){
writeExact(apples,new Apple());
//writeExact(fruit,new Apple()); // Error:
}
static <T> void writeWithWildcard(List<? super 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();
System.out.println(apples);
System.out.println(fruit);
}
}
writeExact()方法使用了一个确切参数类型(无通配符)。在f1()中,可以看到这工作良好——只要你只向List<Apple>中放置Apple。但是,writeExact()不允许将Apple放置到List<Fruit>中,即使知道这应该是可以的。
在writeWithWildcard()中,其参数限制是List<? super T>,因此,这个List将持有从T导出的某种具体类型,这样就可以安全地将一个T类型的对象或者从T导出的任何对象作为参数传递给List的方法。在f2()中可以看到这一点,在这个方法中我们仍旧可以向前面那样,将Apple放置到List<Apple>中,但是我们现在还可如你所期望的那样,将Apple放置到List<Fruit>中。
我们可以执行下面这个相同的类型分析,作为对协变和通配符的一个复习:
package com.zjwave.thinkinjava.generics;
import java.util.Arrays;
import java.util.List;
public class GenericReading {
static <T> T readExact(List<T> list){
return list.get(0);
}
static List<Apple> apples = Arrays.asList(new Apple());
static List<Fruit> 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>{
T readExact(List<T> list){
return list.get(0);
}
}
static void f2(){
Reader<Fruit> fruitReader = new Reader<>();
Fruit f = fruitReader.readExact(fruit);
//Fruit a = fruitReader.readExact(apples); // Error
// readExact(List<Fruit>) cannot be
// applied to (List<Apple>)
}
static class CovariantReader<T>{
T readCovariant(List<? extends T> list){
return list.get(0);
}
}
static void f3(){
CovariantReader<Fruit> 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>中返回已给Apple,从List<Fruit>中返回一个Fruit,就像在f1()中看到的那样。因此,如果可以摆脱静态泛型方法,那么当只是读取时,就不需要协变类型来了。
但是,如果有一个泛型类,那么当你创建这个类的实例时,要为这个类确定参数。就像在f2()中看到的,fruitReader实例可以从List<Fruit>中读取一个Fruit,因为这就是它的确切类型,但是List<Apple>还应该产生Fruit对象,而fruitReader不允许这么做。
为了休整这个问题,CovariantReader.readCovariant()方法将接受List<? extends T>,因此,从这个列表中读取一个T是安全的(你知道在这个列表中的所有对象至少是一个T,并且可能是从T导出的某种对象)。在f3()中,你可以看到现在可以从List<Apple>中读取Fruit了。
10.3 无界通配符
无界通配符<?>看起来意味着“任何事物”,因此使用无界通配符好像等价于使用原生类型。事实上,编译器初看起来是支持这种判断的:
package com.zjwave.thinkinjava.generics;
import java.util.ArrayList;
import java.util.List;
public class UnboundedWildcards1 {
static List list1;
static List<?> list2;
static List<? extends Object> list3;
static void assign1(List list){
list1 = list;
list2 = list;
//list3 = list; // Warning : unchecked conversion
}
static void assign2(List<?> list){
list1 = list;
list2 = list;
list3 = list;
}
static void assign3(List<? extends Object> list){
list1 = list;
list2 = list;
list3 = list;
}
public static void main(String[] args) {
assign1(new ArrayList());
assign2(new ArrayList());
assign3(new ArrayList());//Warning
assign1(new ArrayList<String>());
assign2(new ArrayList<String>());
assign3(new ArrayList<String>());
// Both forms are acceptable as List<?>:
List<?> wildList = new ArrayList<>();
wildList = new ArrayList<String>();
assign1(wildList);
assign2(wildList);
assign3(wildList);
}
}
有很多情况都和你在这里看到的情况类似,即编译器很少关心使用的是原生类型还是<?>。在这些情况中,<?>可以被认为是一种装饰,但是它仍旧是很有价值的,因为,实际上,它是在声明:“我是想用Java的泛型来编写这段代码,我在这里并不是要用原生类型,但是在当前这种情况下,泛型参数可以持有任何类型。”
第二个示例展示了无界通配符的一个重要应用。当你在处理多个泛型参数时,有时允许一个参数可以是任何类型,同时为其他参数确定某种特定类型的这种能力会显得很重要:
package com.zjwave.thinkinjava.generics;
import java.util.HashMap;
import java.util.Map;
public class UnboundedWildcards2 {
static Map map1;
static Map<?,?> map2;
static Map<String,?> map3;
static void assign1(Map map){
map1 = map;
}
static void assign2(Map<?,?> map){
map2 = map;
}
static void assign3(Map<String,?> map){
map3 = map;
}
public static void main(String[] args) {
assign1(new HashMap());
assign2(new HashMap());
//assign3(new HashMap());//Warning
assign1(new HashMap<String,Integer>());
assign2(new HashMap<String,Integer>());
assign3(new HashMap<String,Integer>());
}
}
但是,当你有用的全都是无界通配符时,就像在Map<?,?>中看到的那样,编译器看起来就无法将其与原生Map区分开了。另外,UnboundedWildcards.java展示了编译器处理List<?>和List<? extends Object>时是不同的。
令人困惑的是,编译器并非总是关注像List和List<?>之间的这种差异,因此它们看起来就像是相同的事物。因为,事实上,由于泛型参数将擦除到它的第一边界,因此List<?>看起来等价于List<Object>,而List实际上也是List<Object>——除非这些语句都不为真。List实际上表示“持有任何Object类型的原生List”,而List<?>表示“具有某种特定类型的非原生List,只是我们不知道那种类型是什么。”
编译器何时才会关注原生类型和涉及无界通配符的类型之间的差异呢?下面的示例使用了前面定义的Holder<T>类,它包含接受Holder作为参数的各种方法,但是它们具有不同的形式:作为原生类型,具有具体的类型参数以及具有无界通配符参数:
package com.zjwave.thinkinjava.generics;
public class Wildcards {
// Raw argument:
static void rawArgs(Holder holder, Object arg) {
//holder.set(arg); // Warning
//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<?>) in Holder cannot be applied to (java.lang.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> T exact1(Holder<T> holder) {
T t = holder.get();
return t;
}
static <T> T exact2(Holder<T> holder, T arg) {
holder.set(arg);
T t = holder.get();
return t;
}
static <T> T wildSubtype(Holder<? extends T> holder, T arg) {
//holder.set(arg); // Error
//set (capture<? extends T>) in Holder cannot be applied to (T)
T t = holder.get();
return t;
}
static <T> void wildSupertype(Holder<? super T> holder, T arg) {
holder.set(arg);
//T t = holder.get(); // Error
//Incompatible types. Required: T Found: capture<? super T>
// OK, but type information has been lost:
Object obj = holder.get();
}
public static void main(String[] args) {
Holder raw = new Holder<Long>();
// Or:
raw = new Holder();
Holder<Long> qualified = new Holder<>();
Holder<?> unbounded = new Holder<Long>();
Holder<? extends Long> 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);
// Unchecked assignment:
// 'com.zjwave.thinkinjava.generics.Holder' to
// 'com.zjwave.thinkinjava.generics.Holder<java.lang.Object>'
//Object r1 = exact1(raw);
Long r2 = exact1(qualified);
Object r3 = exact1(unbounded);//Must return Object
Long r4 = exact1(bounded);
//Long r5 = exact2(raw, lng);//Warnings
//Unchecked assignment
Long r6 = exact2(qualified, lng);
//Long r7 = exact2(unbounded,lng); // Error
//Long r8 = exact2(bounded,lng); // Error
//Long r9 = wildSubtype(raw, lng);//Warning
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
wildSupertype(qualified,lng);
//wildSupertype(unbounded,lng);//Error
//wildSupertype(bounded,lng);//Error:
//wildSupertype (com.zjwave.thinkinjava.generics.Holder<? super T>,T)
//in Wildcards cannot be applied to
//(com.zjwave.thinkinjava.generics.Holder<capture<? extends java.lang.Long>>,java.lang.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<Apple>,这是合法的。为了防止将Orange放置到Holder<Apple>中,对set()的调用(或者任何接受这个类型参数为参数的方法的调用)都是不允许的。但是,你仍旧知道任何来自Holder<? extends Fruit>的对象至少是Fruit,因此get()(或者任何将产生具有这个类型参数的返回值的方法)都是允许的。
wildSupertype()展示了超类型通配符,这个方法展示了与wildSubtype()相反的行为:holder可以是持有任何T的基类型的容器。因此,set()可以接受T,因为任何可以工作于基类的对象都可以多态地作用域导出类(这里就是T)。但是,尝试着调用get()是没有用的,因为由holder持有的类型可以是任何超类型,因此唯一安全的类型就是Object。
这个示例还展示了对于unboundedArg()中使用无界通配符能够做什么、不能做什么所做出的限制。对于迁移兼容性,rawArgs()将接受所有Holder的不同变体,而不会产生警告。unboundedArg()方法也可以接受相同的所有类型,尽管如前所述,它在方法体内部处理这些类型的方式并不相同。
如果向接受“确切”泛型类型(没有通配符)的方法传递一个原生Holder引用,就会得到一个警告,因为确切的参数期望得到在原生类型中并不存在的信息。如果向exact1()传递一个无界引用,就不会有任何可以确定返回类型的类型信息。
可以看到,exact2()具有最多的限制,因为它希望精确地得到一个Holder<T>,以及一个具有类型T的参数,正由于此,它将产生错误或警告,除非提供确切的参数。有时,这样做很好,但是如果它过于受限,那么就可以使用通配符,这取决于是否想要从泛型参数中返回类型确定的返回值(就像在wildSubtype()中看到的那样),或者是否想要向泛型参数传递类型确定的参数(就像在wildSupertype()中看到的那样)。
因此,使用确切类型来替代通配符类型的好处是,可以用泛型参数来做更多的事,但是使用通配符使得你必须接受范围更宽的参数化类型作为参数。因此,必须逐个情况地权衡利弊,找到更适合你的需求的方法。
10.4 捕获转换
有一种情况特别需要使用<?>而不是原生类型。如果向一个使用<?>的方法传递原生类型,那么对编译器来说,可能会推断出实际的类型参数,是的这个方法可以回转并调用另一个使用确切类型的方法。下面的示例演示了这种技术,它被称为捕获转换,因为未指定的通配符类型被捕获,并被转换为确切类型。这里,有关警告的注释只有在@SuppressWarnings("unchecked")注解被移除之后才能起作用:
package com.zjwave.thinkinjava.generics;
public class CaptureConversion {
static <T> void f1(Holder<T> 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<Integer>(1);
//f1(raw);//Produces warnings
f2(raw);//No warnings
Holder rawBasic = new Holder();
rawBasic.set(new Object()); // Warning
f2(rawBasic);//NoWarnings
Holder<?> wildcarded = new Holder<Double>(1.0);
f2(wildcarded);
}
}
f1()中的类型参数都是确切的,没有通配符或边界。在f2()中,Holder参数是一个无界通配符,因此它看起来是未知的。但是,在f2()中,f1()被调用,而f1()需要一个已知参数。这里所发生的是:参数类型在调用f2()的过程中被捕获,因此它可以在对f1()的调用中被使用。
你可能想知道,这项技术是否可以用于写入,但是这要求要在传递Holder<?>时,同时传递一个具体类型。捕获转换只有在这样的情况下可以工作:即在方法内部,你需要使用确切的类型。注意,不能从f2()中返回T,因为T对于f2()来说是位置的。捕获转换十分有趣,但是非常受限。
11.问题
本节将阐述在使用Java泛型时会出现的各类问题。
11.1 任何基本类型都不能作为参数
正如先前提到过的,你将在Java泛型中发现的限制之一是,不能将基本类型用作类型参数。因此,不能创建ArrayList<int>之类的东西。
解决之道是使用基本类型的包装器类以及Java SE5的自动包装机制。如果创建一个ArrayList<Integer>,并将基本类型int应用于这个容器,那么你返现自动包装机制将自动地实现int到Integer的双向转换——因此,这几乎就像是有一个ArrayList<int>一样:
package com.zjwave.thinkinjava.generics;
import java.util.ArrayList;
import java.util.List;
public class ListOfInt {
public static void main(String[] args) {
List<Integer> li = new ArrayList<>();
for (int i = 0; i < 5; i++) {
li.add(i);
}
for (Integer i : li) {
System.out.print(i + " ");
}
}
}
注意,自动包装机制解决了一些问题,但并不是解决了所有问题。下面是示例展示了一个泛型的Generator接口,它指定next()方法返回一个具有其参数类型的对象。FArray类包含一个泛型方法,它通过使用生成器在数组中填充对象(这使得类泛型在本例中无法工作,因为这个方法是静态的)。
package com.zjwave.thinkinjava.generics;
import com.zjwave.thinkinjava.generics.coffee.Coffee;
import com.zjwave.thinkinjava.generics.coffee.CoffeeGenerator;
public class PrimitiveGenericTest {
public static void main(String[] args) {
Coffee[] coffees = FArray.fill(new Coffee[7], new CoffeeGenerator());
for (Coffee coffee : coffees) {
System.out.println(coffee);
}
Integer[] integers = FArray.fill(new Integer[7], new Fibonacci());
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 Fibonacci()); //Error
}
}
}
class FArray{
public static <T> T[] fill(T[] a,Generator<T> gen){
for (int i = 0; i < a.length; i++) {
a[i] = gen.next();
}
return a;
}
}
由于Fibonacci实现了Generator<Integer>,所以我的希望是自动包装机制可以自动地将next()的值从Integer转换为int。但是,自动包装机制不能应用于数组,因此这无法工作。
11.2 实现参数化接口
一个类不能实现同一个泛型接口的两种变体,由于擦除的原因,这两个变体会成为相同的接口。下面是产生这种冲突的情况:
package com.zjwave.thinkinjava.generics;
public class MultipleInterfaceVariants {
}
interface Payable<T>{}
class Employees implements Payable<Employees>{}
class Hourly extends Employees implements Payable<Hourly>{}
//'com.zjwave.thinkinjava.generics.Payable' cannot be inherited with
// different type arguments: 'com.zjwave.thinkinjava.generics.Employees'
// and 'com.zjwave.thinkinjava.generics.Hourly
Hourly不能编译,因为擦除会将Payable<Employees>和Payable<Hourly>简化为相同的类Payable,这样,上面的代码就意味着在重复两次实现相同的接口。十分有趣的是,如果从Payable的两种用法中都移除掉泛型参数(就像编译器在擦除阶段所做的那样)这段代码就可以编译。
在使用某些更基本的Java接口,例如Comparable<T>时,这个问题可能会变得十分令人恼火。
11.3 转型和警告
使用带有泛型类型参数的转型或instanceof不会有任何效果。下面的容器在内部将各个值存储为Object,并在获取这些值时,再将它们转型回T:
package com.zjwave.thinkinjava.generics;
public class GenericCast {
public static final int SIZE = 10;
public static void main(String[] args) {
FixedSizeStack<String> 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 + " ");
}
}
}
class FixedSizeStack<T>{
private int index;
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];
}
}
如果没有@SuppressWarnings("unchecked")注解,编译器将对pop()产生“unchecked cast”警告。由于擦除的原因,编译器无法知道这个转型是否是安全的,并且pop()方法实际上并没有执行任何转型。这是因为,T被擦除到它的第一个边界,默认情况下是Object,因此pop()实际上只是将Object转型为Object。
有时,泛型没有消除对转型的需要,这就会由编译器产生警告,而这个警告是不恰当的。例如:
package com.zjwave.thinkinjava.generics;
import java.io.FileInputStream;
import java.io.ObjectInputStream;
import java.util.List;
public class NeedCasting {
@SuppressWarnings("unchecked")
public void f(String[] args) throws Exception {
ObjectInputStream in = new ObjectInputStream(new FileInputStream(args[0]));
List<Widget> shapes = (List<Widget>) in.readObject();
}
}
readObject()无法知道它正在读取的是什么,因此它返回的是必须转型的对象。但是当注释掉@SuppressWarnings("unchecked"),并编译这个程序时,就会得到警告。
你会被强制要求转型,但是又被编译器告知不应该转型。为了解决这个问题,必须使用在Java SE5中引入的新的转型形式,即通过泛型类来转型:
package com.zjwave.thinkinjava.generics;
import java.io.FileInputStream;
import java.io.ObjectInputStream;
import java.util.List;
public class ClassCasting {
public void f(String[]args) throws Exception{
ObjectInputStream in = new ObjectInputStream(new FileInputStream(args[0]));
//Won't Compile
//List<Widget> lw1 = List<Widget>.class.cast(in.readObject());
List<Widget> lw2 = List.class.cast(in.readObject());
}
}
但是,不能转型到实际类型(List<Widget>)。也就是说,不能声明:
List<Widget>.class.cast(in.readObject());
甚至当你添加一个像下面这样的另一个转型时:
(List<Widget>)List.class.cast(in.readObject())
仍旧会得到一个警告。
11.4 重载
下面是程序是不能编译的,即使编译它是一种合理的尝试:
package com.zjwave.thinkinjava.generics;
import java.util.List;
public class UserList<W,T> {
void f(List<T> v){}
void f(List<W> v){}
}
由于擦除的原因,重载方法将产生相同的类型签名。
与此不同的是,当被擦除的参数不能产生唯一的参数列表时,必须提供明显有区别的方法名:
package com.zjwave.thinkinjava.generics;
import java.util.List;
public class UserList2<W,T> {
void f1(List<T> v){}
void f2(List<W> v){}
}
幸运的是,这类问题可以由编译器探测到。
11.5 基类劫持了接口
假设你有一个Pet类,它可以与其他的Pet对象进行比较(实现了Comparable接口):
package com.zjwave.thinkinjava.generics;
public class ComparablePet implements Comparable<ComparablePet> {
@Override
public int compareTo(ComparablePet o) {
return 0;
}
}
对可以与ComparablePet的子类比较的类型进行窄化是有意义的,例如,一个Cat对象就只能与其他的Cat对象比较:
//'java.lang.Comparable' cannot be inherited with different
// type arguments: 'com.zjwave.thinkinjava.generics.ComparablePet'
// and 'com.zjwave.thinkinjava.generics.Cat
class Cat extends ComparablePet implements Comparable<Cat>{
@Override
public int compareTo(ComparablePet o) {
return 0;
}
}
遗憾的是,这不能工作。一旦为Comparable确定了ComparablePet参数,那么其他任何实现类都不能与ComparablePet之外的任何对象比较:
class Hamster extends ComparablePet implements Comparable<ComparablePet>{
@Override
public int compareTo(ComparablePet o) {
return 0;
}
}
// Or just:
class Gecko extends ComparablePet{
@Override
public int compareTo(ComparablePet o) {
return 0;
}
}
Hamster说明再次实现ComparablePet中的相同接口是可能的,只要它们精确地相同,包括参数类型在内。但是,这只是与覆盖基类中的方法相同,就像在Gecko中看到的那样。
12.自限定的类型
在Java泛型中,有一个好像是经常性出现的惯用法,它相当令人费解:
class SelfBounded<T extends SelfBounded<T>>{
//...
}
这就像两面镜子彼此照向对方所引起的目眩效果一样,是一种无限反射。SelfBounded类接受泛型参数T,而T由一个边界类限定,这个边界就是拥有T作为其参数的SelfBounded。
当你首次看到它时,很难去解析它,它强调的是当extends关键字用于边界与用来创建子类时是明显不同的。
12.1 古怪的循环泛型
为了理解自限定类型的含义,我们从这个惯用法的一个简单版本入手,它没有自限定的边界。
不能直接继承一个泛型参数,但是,可以继承在其自己的定义中使用这个泛型参数的类。也就是说,可以声明:
package com.zjwave.thinkinjava.generics;
public class CuriouslyRecurringGeneric extends GenericType<CuriouslyRecurringGeneric>{
//...
}
class GenericType<T>{}
可以按照Jim Coplien在C++中的古怪的循环模板模式的命名方式,成为古怪的循环泛型(CRG)。“古怪的循环”是指类相当古怪地出现在它自己的基类中这一事实。
为了理解其含义,努力大声说:“我在创建一个新类,它继承自一个泛型类型,这个泛型类型接受我的类的名字作为其参数。”当给出导出类的名字时,这个泛型基类能够实现什么呢?好吧,Java中的泛型关乎参数和返回类型,因此它能够产生使用导出类作为其参数和返回类型的基类。它还能将导出类型用作其域类型,甚至那些将被擦除为Object的类型。下面是表示了这种情况的一个泛型类:
package com.zjwave.thinkinjava.generics;
public class BasicHolder<T> {
T element;
public T get() {
return element;
}
public void set(T element) {
this.element = element;
}
void f(){
System.out.println(element.getClass().getSimpleName());
}
}
这是一个普通的泛型类型,它的一些方法将接受和产生具有其参数类型的对象,还有一个方法将在其存储的域上执行操作(尽管只是在这个域上执行Object操作)。
我们可以在一个古怪的循环泛型中使用BasicHolder:
package com.zjwave.thinkinjava.generics;
public class CRGWithBasicHolder {
public static void main(String[] args) {
Subtype st1 = new Subtype(),
st2 = new Subtype();
st1.set(st2);
Subtype st3 = st1.get();
st1.f();
}
}
class Subtype extends BasicHolder<Subtype> {
}
注意,这里有些东西很重要:新类Subtype接受的参数和返回的值具有Subtype类型而不仅仅是基类BasicHolder类型。这就是CRG的本质:基类用导出类代替其参数。这意味着泛型基类变成了一种其所有导出类的公共模板,但是这些功能对于其所有参数和返回值,将使用导出类型。也就是说,在所产生的类中将使用确切类型而不是基类型。因此,在Subtype中,传递给set()的参数和从get()返回的类型都是确切的Subtype。
12.2 自限定
BasicHolder可以使用任何类型作为其泛型参数,就像下面看到的那样:
package com.zjwave.thinkinjava.generics;
public class Unconstrained {
public static void main(String[] args) {
BasicOther b = new BasicOther(),
b2 = new BasicOther();
b.set(new Other());
Other other = b.get();
b.f();
}
}
class Other {
}
class BasicOther extends BasicHolder<Other> {
}
自限定将采取额外的步骤,强制泛型当作其自己的边界参数来使用。观察所产生的类可以如何使用以及不可以如何使用:
package com.zjwave.thinkinjava.generics;
public class SeleBounding {
public static void main(String[] args) {
A a = new A();
a.set(new A());
a = a.set(new A()).get();
a = a.get();
C c = new C();
c = c.setAndGet(new C());
}
}
class SelfBounded<T extends SelfBounded<T>>{
T element;
public T get() {
return element;
}
public SelfBounded<T> set(T element) {
this.element = element;
return this;
}
}
class A extends SelfBounded<A>{}
class B extends SelfBounded<A>{} // Also OK
class C extends SelfBounded<C>{
C setAndGet(C c){
set(c);
return get();
}
}
class D{}
// Can't do this:
//class E extends SelfBounded<D>{}
// Compile error:
//Type parameter 'com.zjwave.thinkinjava.generics.D'
// is not within its bound; should extend
// 'com.zjwave.thinkinjava.generics.SelfBounded<com.zjwave.thinkinjava.generics.D>
//Alas, you can do this,so you can't force the idiom:
class F extends SelfBounded{}
自限定所做的,就是要求在继承关系中,像下面这样使用这个类:
class A extends SelfBounded<A>{}
这回强制要求将正在定义的类当做参数传递给基类。
自限定的参数有何意义呢?它可以保证类型参数必须与正在被定义的类相同。正如你在B类的定义中所看到的,还可以从使用了另一个SelfBounded参数的SelfBounded中导出,尽管在A类看到的用法看起来是主要的用法。对定义E的尝试说明不能使用不是SelfBounded的类型参数。
遗憾的是,F可以编译,不会有任何警告,因此自限定惯用法不是可以强制执行的。如果它确实很重要,可以要求一个外部工具来确保不会使用原生类型来替代参数化类型。
注意,可以移除自限定这个限制,这样所有的类仍旧是可以编译的,但是E也会因此而变得可编译:
package com.zjwave.thinkinjava.generics;
public class NotSelfBounded<T> {
T element;
NotSelfBounded<T> set(T arg){
element = arg;
return this;
}
T get(){
return element;
}
}
class A2 extends NotSelfBounded<A2>{}
class B2 extends NotSelfBounded<A2>{}
class C2 extends NotSelfBounded<C2>{
C2 setAndget(C2 arg){
set(arg);
return get();
}
}
class D2{}
// Now this is OK:
class E2 extends NotSelfBounded<D2>{}
因此很明显,自限定限制只能强制作用于继承关系。如果使用自限定,就应该了解这个类所用的类型参数将与使用这个参数的类具有相同的基类型。这回强制要求使用这个类的每个人都要遵循这种形式。
还可以将自限定用于泛型方法:
package com.zjwave.thinkinjava.generics;
public class SelfBoundingMethods {
static <T extends SelfBounded<T>> T f(T arg){
return arg.set(arg).get();
}
public static void main(String[] args) {
A a = f(new A());
}
}
这可以防止这个方法被应用于除上述形式的自限定参数之外的任何事物上。
12.3 参数协变
自限定类型的价值在于它们可以产生协变参数类型——方法参数类型会随子类而变化。尽管自限定类型还可以产生与子类类型相同的返回类型,但是这并不十分重要,因为协变返回类型是在Java SE5中引入的:
package com.zjwave.thinkinjava.generics;
public class ConvariantReturnTypes {
void test(DerivedGetter d){
Derived d2 = d.get();
}
}
class Base{}
class Derived extends Base{}
interface OrdinaryGetter{
Base get();
}
interface DerivedGetter extends OrdinaryGetter{
// Return type of overridden method is allowed to vary :
@Override
Derived get();
}
DerivedGetter中的get()方法覆盖了OrdinaryGetter中的get(),并返回了一个从OrdinaryGetter.get()的返回类型中导出的类型。尽管这是完全合乎逻辑的事情(导出类方法应该能够返回比它覆盖的基类方法更具体的类型)但是这在早先的Java版本中是不合法的。
自限定泛型事实上将产生确切的导出类型作为其返回值,就像是get()中所看到的一样:
package com.zjwave.thinkinjava.generics;
public class GenericsAndReturnTypes {
void test(Getter g){
Getter result = g.get();
GenericGetter gg = g.get();// Also the base type
}
}
interface GenericGetter<T extends GenericGetter<T>>{
T get();
}
interface Getter extends GenericGetter<Getter>{}
注意,这段代码在低版本的Java中不能编译,除非是使用了囊括了协变返回类型的Java SE5。然而,在非泛型代码中,参数类型不能随子类型发生变化:
package com.zjwave.thinkinjava.generics;
public class OrdinaryArguments {
public static void main(String[] args) {
Base base = new Base();
Derived derived = new Derived();
DerivedSetter ds = new DerivedSetter();
ds.set(derived);
ds.set(base);// Compiles : overloaded ,not overridden!
}
}
class OrdinarySetter{
void set(Base base){
System.out.println("OrdinarySetter.set(Base)");
}
}
class DerivedSetter extends OrdinarySetter{
void set(Derived derived) {
System.out.println("DerivedSetter.set(Derived)");
}
}
set(Derived)和set(Base)都是合法的,因此DerivedSetter.set()没有覆盖OrdinarySetter.set(),而是重载了这个方法。从输出中可以看到,在DerivedSetter中有两个方法,因此基类版本仍旧是可用的,因此可以证明它被重载过。
但是,在使用自限定类型时,在导出类中只有一个方法,并且这个方法接受导出类型而不是基类型为参数:
package com.zjwave.thinkinjava.generics;
public class SelfBoundingAndCovariantArguments {
void testA(Setter s1,Setter s2,SelfBoundSetter sbs){
s1.set(s2);
//s1.set(sbs);//Error
//set
//(com.zjwave.thinkinjava.generics.Setter)
//in SelfBoundSetter cannot be applied
//to
//(com.zjwave.thinkinjava.generics.SelfBoundSetter)
}
}
interface SelfBoundSetter<T extends SelfBoundSetter<T>>{
void set(T arg);
}
interface Setter extends SelfBoundSetter<Setter>{}
编译器不能识别将基类型当做参数传递给set()的尝试,因为没有任何方法具有这样的签名。实际上,这个参数已经被覆盖。
如果不使用自限定类型,普通的继承机制就会介入,而你将能够重载,就像在非泛型的情况下一样:
package com.zjwave.thinkinjava.generics;
public class PlainGenericInheritance {
public static void main(String[] args) {
Base base = new Base();
Derived derived = new Derived();
DerivedGS dgs = new DerivedGS();
dgs.set(derived);
dgs.set(base);//Compiles : overload, not overridden!
}
}
class GenericSetter<T>{//Not self-bounded
void set(T arg){
System.out.println("GenericSetter.set(base)");
}
}
class DerivedGS extends GenericSetter<Base>{
void set(Derived derived){
System.out.println("DerivedGS.set(Derived)");
}
}
这段代码在模仿OrdinaryArguments.java,在那个示例中,DerivedSetter继承自包含一个set(Base)的OrdinarySetter。而这里,DerivedGS继承自泛型创建的也包含有一个set(Base)的GenericSetter<Base>。就像OrdinaryArguments.java一样,你可以从输出中看到,DerivedGS包含两个set()的重载版本。如果不使用自限定,将重载参数类型。如果使用了自限定,只能获得某个方法的一个版本,它将接受确切的参数类型。
13.动态类型安全
因为可以向Java SE5之前的代码传递泛型容器,所以旧式代码仍旧有可能破坏你的容器,Java SE5的java.util.Collections中有一组便利工具,可以解决在这种情况下的类型检查问题,它们是静态方法:
-
checkedCollection()
-
checkedList()
-
checkedMap()
-
checkedSet()
-
checkedSortedMap()
-
checkedSortedSet()
这些方法每一个都会将你希望动态检查的容器当做第一个参数接受,并将你希望强制要求的类型作为第二个参数接受。
受检查的容器在你试图插入类型不正确的对象时抛出ClassCastException,这与泛型之前的(原生)容器形成了对比,对于后者来说,当你将对象从容器中取出时,才会通知你出现了问题。在后一种情况中,你知道存在问题,但是不知道罪魁祸首在哪里,如果使用受检查的容器,就可以发现谁在试图插入不良对象。
让我们用受检查的容器来看看“将猫插入到狗列表中”这个问题。这里oldStyleMethod()表示遗留代码,因为它接受的是原生的List,而@SuppressWarnings("unchecked")注解对于压制所产生的警告是必须的:
package com.zjwave.thinkinjava.generics;
import com.zjwave.thinkinjava.typeinfo.pets.Cat;
import com.zjwave.thinkinjava.typeinfo.pets.Dog;
import com.zjwave.thinkinjava.typeinfo.pets.Pet;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
public class CheckedList {
@SuppressWarnings("unchecked")
static void oldStyleMethod(List probablyDogs){
probablyDogs.add(new Cat());
}
public static void main(String[] args) {
List<Dog> dogs1 = new ArrayList<>();
oldStyleMethod(dogs1);//Quietly accepts a Cat
List<Dog> dogs2 = Collections.checkedList(new ArrayList<>(), Dog.class);
try {
oldStyleMethod(dogs2);
}catch (Exception e){
System.out.println(e);
}
// Derived types work fine
List<Pet> pets = Collections.checkedList(new ArrayList<>(),Pet.class);
pets.add(new Dog());
pets.add(new Cat());
}
}
运行这个程序是,你会发现插入一个Cat对于dogs1来说没有任何问题,而dogs2立即会在这个错误类型的插入操作上抛出一个异常。还可以看到,将导出类型的对象放置到将要检查基类型的受检查容器中是没有问题的。
14.异常
由于擦除的原因,将泛型应用于异常是非常受限的。catch语句不能捕获泛型类型的异常,因为在编译期和运行时都必须知道异常的确切类型。泛型类也不能直接或间接继承自Throwable(这将进一步阻止你去定义不能捕获的泛型异常)。
但是,类型参数可能会在一个方法的throws子句中用到。这使得你可以编写随检查形异常的类型而发生变化的泛型代码:
package com.zjwave.thinkinjava.generics;
import java.util.ArrayList;
import java.util.List;
public class ThrowGenericException {
public static void main(String[] args) {
ProcessRunner<String, Failure1> runner = new ProcessRunner<>();
for (int i = 0; i < 3; i++) {
runner.add(new Processor1());
}
try {
System.out.println(runner.processAll());
} catch (Failure1 e) {
System.out.println(e);
}
ProcessRunner<Integer, Failure2> runner2 = new ProcessRunner<>();
for (int i = 0; i < 3; i++) {
runner2.add(new Processor2());
}
try {
System.out.println(runner2.processAll());
} catch (Failure2 e) {
System.out.println(e);
}
}
}
interface Processor<T,E extends Exception>{
void process(List<T> resultCollector) throws E;
}
class ProcessRunner<T,E extends Exception> extends ArrayList<Processor<T,E>>{
List<T> processAll() throws E{
List<T> resultCollector = new ArrayList<>();
for (Processor<T, E> processor : this) {
processor.process(resultCollector);
}
return resultCollector;
}
}
class Failure1 extends Exception{}
class Processor1 implements Processor<String,Failure1>{
static int count = 3;
@Override
public void process(List<String> resultCollector) throws Failure1 {
if(count-- > 1){
resultCollector.add("Hep!");
}else{
resultCollector.add("Ho!");
}
if(count < 0){
throw new Failure1();
}
}
}
class Failure2 extends Exception{}
class Processor2 implements Processor<Integer,Failure2>{
static int count = 2;
@Override
public void process(List<Integer> resultCollector) throws Failure2 {
if(count-- == 0){
resultCollector.add(47);
}else {
resultCollector.add(11);
}
if(count < 0){
throw new Failure2();
}
}
}
Processor执行process(),并且可能会抛出具有类型E的异常。process()的结果存储在List<T> resultCollector中(这被称为收集参数)。ProcessRunner有一个processAll()方法,它将执行所持有的每个Process对象,并返回resultCollector。
如果不能参数化所抛出的异常,那么由于检查型异常的缘故,将不能编写出这种泛化的代码。
15.混型
术语混型随时间的推移好像拥有了无数的含义,但是其最基本的概念是混合多个类的能力,以产生一个可以表示混型中所有类型的类。这往往是你最后的手段,它将使组装多个类变得简单易行。
混型的价值之一是它们可以将特性和行为一致地应用于多个类之上。如果想在混型类中修改某些东西,作为一种意外的好处,这些修改将会应用于混型所应用的所有类型之上。正由于此,混型有一点面向切面变成(AOP)的味道,而切面经常被建议用来解决混型问题。
15.1 C++中的混型
在C++中,使用多重继承的最大理由,就是为了使用混型。但是,对于混型来说,更有趣、更优雅的方式是使用参数化类型,因为混型就是继承自其类型参数的类。在C++中,可以很容易的创建混型,因为C++能够记住其模板参数的类型。
下面是一个C++示例,它有两个混型类型:一个是的你可以在每个对象中混入拥有一个时间戳这样的属性,而另一个可以混入一个序列号。
//: generics/Mixins.cpp
#include <string>
#include <ctime>
#include <iostream>
using namespace std;
template<class T> class TimeStamped : public T {
long timeStamp;
public:
TimeStamped(){ timeStapm = time(0); }
long getStamp(){ return timeStamp; }
};
template<class T> class SerialNumbered : public T {
long serialNumber;
static long counter;
public:
SerialNumbered(){ serialNumber = counter++; }
long getSerialNumber(){ return serialNumber; }
}
// Define and initialize the static storage:
tempalte<class T> long SerialNumbered<T>::counter = 1;
class Basic{
string value;
public:
void set(string val){ value = val; }
string get() { return value; }
}
int main(){
TimeStamped<SerialNumbered<Basic>> mixin1,minix2;
mixin1.set("test string 1");
mixin2.set("test string 2");
cout << mixin1.get() << " " << mixin1.getStamp() << " " << mixin1.getSerialNumber() << endl;
cout << mixin2.get() << " " << mixin2.getStamp() << " " << mixin2.getSerialNumber() << endl;
}
/* Otuput:(Sample)
test string 1 1129840250 1
test string 2 1129840250 2
*/
在main()中,mixin1和mixin2所产生的类型拥有所混入类型的所有方法。可以将混型看作是一种功能,它可以将现有类映射到新的子类上。注意,使用这种技术来创建一个混型是多么地轻而易举。基本上,只需要声明“这就是我想要的”,紧跟着它就发生了:
TimeStamped<SerialNumbered<Basic>> mixin1,minix2;
遗憾的是,Java泛型不允许这样。擦除会忘记基类类型,因此泛型类不能直接继承自一个泛型参数。
15.2 与接口混合
一种更常见的推荐解决方案是使用接口来产生混型效果,就像下面这样:
package com.zjwave.thinkinjava.generics;
import java.util.Date;
public class Mixins {
public static void main(String[] args) {
Mixin mixin1 = new Mixin(),
mixin2 = new Mixin();
mixin1.set("test string 1");
mixin2.set("test string 2");
System.out.println(mixin1.get() + " " + mixin1.getStamp() + " " + mixin1.getSerialNumber());
System.out.println(mixin2.get() + " " + mixin2.getStamp() + " " + mixin2.getSerialNumber());
}
}
interface TimeStamped {
long getStamp();
}
class TimeStampedImpl implements TimeStamped {
private final long timeStamp;
public TimeStampedImpl() {
this.timeStamp = new Date().getTime();
}
@Override
public long getStamp() {
return timeStamp;
}
}
interface SerialNumbered {
long getSerialNumber();
}
class SerialNumberedImpl implements SerialNumbered {
private static long counter = 1;
private final long searialNumber = counter++;
@Override
public long getSerialNumber() {
return searialNumber;
}
}
interface Basic {
void set(String val);
String get();
}
class BasicImpl implements Basic {
private String value;
@Override
public void set(String val) {
value = val;
}
@Override
public String get() {
return value;
}
}
class Mixin extends BasicImpl implements TimeStamped, SerialNumbered {
private TimeStamped timeStamp = new TimeStampedImpl();
private SerialNumbered serialNumber = new SerialNumberedImpl();
@Override
public long getStamp() {
return timeStamp.getStamp();
}
@Override
public long getSerialNumber() {
return serialNumber.getSerialNumber();
}
}
Mixin类基本上是在使用代理,因此每个混入类型都要求在Mixin中有一个相应的域,而你必须在Mixin中编写所有必需的方法,将方法调用转发给恰当的对象。这个示例使用了非常简单的类,但是当使用更复杂的混型时,代码数量会急速增加。
15.3 使用装饰器模式
当你观察混型的使用方式时,就会发现混型概念好像与装饰器设计模式关系很近。装饰器经常用于满足各种可能的组合,而直接子类化会产生过多的类,因此是不实际的。
装饰器模式使用分层对象来动态透明地向单个对象中添加责任。装饰器指定包装在最初的对象周围的所有对象都具有相同的基本接口。某些事物是可装饰的,可以通过将其他类包装在这个可装饰对象的四周,来将功能分层。这使得对装饰器的使用是透明的——无论对象是否被装饰,你都拥有一个可以向对象发送的公共消息集。装饰类也可以添加新方法,但是正如你所见,这将是受限的。
装饰器是通过使用组合和形式化结构(可装饰物/装饰器层次结构)来实现的,而混型是基于继承的。因此可以将基于参数化类型的混型当作是一种泛型装饰器机制,这种机制不需要装饰器设计模式的继承结构。
前面的示例可以被改写为使用装饰器:
package com.zjwave.thinkinjava.generics.decorator;
import java.util.Date;
public class Decoration {
public static void main(String[] args) {
TimeStamped t = new TimeStamped(new Basic());
TimeStamped t2 = new TimeStamped(new SerialNumbered(new Basic()));
//t2.getSerialNumber(); // Not available
SerialNumbered s = new SerialNumbered(new Basic());
SerialNumbered s2 = new SerialNumbered((new TimeStamped(new Basic())));
//s2.getStamp(); // Not available
}
}
class Basic {
private String value;
public String get() {
return value;
}
public void set(String value) {
this.value = value;
}
}
class Decorator extends Basic {
protected Basic basic;
public Decorator(Basic basic) {
this.basic = basic;
}
@Override
public String get() {
return basic.get();
}
@Override
public void set(String value) {
basic.set(value);
}
}
class TimeStamped extends Decorator {
private final long timeStamp;
public TimeStamped(Basic basic) {
super(basic);
timeStamp = new Date().getTime();
}
public long getStamp() {
return timeStamp;
}
}
class SerialNumbered extends Decorator {
private static long counter = 1;
private final long serialNumber = counter++;
public SerialNumbered(Basic basic) {
super(basic);
}
public long getSerialNumber() {
return serialNumber;
}
}
产生自泛型的类包含所有感兴趣的方法,但是由使用装饰器所产生的对象类型是最后被装饰的类型。也就是说,尽管可以添加多个层,但是最后一层才是实际的类型,因此只有最后一层的方法是可视的,而混型的类型是所有被混合到一起的类型。因此对于装饰器来说,其明显的缺陷是它只能有效地工作于装饰中的一层(最后一层),而混型方法显然会更自然一些。因此,装饰器只是对由混型提出的问题的一种局限的解决方案。
15.4 与动态代理混合
可以使用动态代理来创建一种比装饰器更贴近混型模型的机制。通过使用动态代理,所产生的类的动态类型将会是已经混入的组合类型。
由于动态代理的限制,每个被混入的类都必须是某个接口的实现:
package com.zjwave.thinkinjava.generics;
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;
import java.util.HashMap;
import java.util.Map;
public class DynamicProxyMixin {
public static void main(String[] args) {
Object mixin = MixinProxy.newInstance(Tuple.tuple(new BasicImpl(), Basic.class), Tuple.tuple(new TimeStampedImpl(), TimeStamped.class), Tuple.tuple(new SerialNumberedImpl(), SerialNumbered.class));
Basic b = (Basic) mixin;
TimeStamped t = (TimeStamped) mixin;
SerialNumbered s = (SerialNumbered) mixin;
b.set("Hello");
System.out.println(b.get());
System.out.println(t.getStamp());
System.out.println(s.getSerialNumber());
}
}
class MixinProxy implements InvocationHandler{
Map<String,Object> delegatesByMethod;
public MixinProxy(TwoTuple<Object,Class<?>>... pairs){
delegatesByMethod = new HashMap<>();
for (TwoTuple<Object, Class<?>> pair : pairs) {
for (Method method : pair.second.getMethods()) {
String methodName = method.getName();
// The first interface in the map
// implements the method
if(!delegatesByMethod.containsKey(methodName)){
delegatesByMethod.put(methodName,pair.first);
}
}
}
}
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
String methodName = method.getName();
Object delegate = delegatesByMethod.get(methodName);
return method.invoke(delegate,args);
}
@SuppressWarnings("unchecked")
public static Object newInstance(TwoTuple... pairs){
Class[] interfaces = new Class[pairs.length];
for (int i = 0; i < pairs.length; i++) {
interfaces[i] = (Class) pairs[i].second;
}
ClassLoader cl = pairs[0].first.getClass().getClassLoader();
return Proxy.newProxyInstance(cl,interfaces,new MixinProxy(pairs));
}
}
因为只有动态类型而不是非静态类型才包含所有的混入类型,因此这仍旧不如C++的方式好,因为可以在具有这些类型的对象上调用方法之前,你被强制要求必须先将这些对象向下转型到恰当的类型。但是,它明显地更接近于真正的混型。
为了让Java支持混型,人们已经做了大量的工作朝着这个目标努力,包括创建了至少一种附加语言(Jam语言),它是专门用来支持混型的。
16.潜在类型机制
在本文的开头介绍过这样的思想,即要编写能够尽可能广泛地应用的代码。为了实现这一点,我们需要各种途径来放松对我们的代码将要作用的类型所作的限制,同时不丢失静态类型检查的好处。然后,我们就可以编写出无需修改就可以应用于更多情况的代码,即更加“泛化”的代码。
Java泛型看起来是向这一方向迈进了一步。当你在编写或使用知识持有对象的泛型时,这些代码可以工作于任何类型(除了基本类型,尽管正如你所见到的,自动包装机制可以克服这一点)。或者,换个角度讲,:“持有器”泛型能够声明:“我不关心你是什么类型”。如果代码不关心它将要作用的类型,那么这种代码就可以真正地应用于任何地方,并因此而相当“泛化”。
还是正如你所见到的,当要在泛型类型上执行操作(即调用Object方法之前的操作)时,就会产生问题,因为擦除要求指定可能会用到的泛型类型的边界,以安全地调用代码中的泛型对象上的具体方法。这是对“泛化”概念的一种明显的限制,因为必须限制你的泛型类型,使它们继承自特定的类,或者实现特定的接口。在某些情况下,你最终可能会使用普通类或普通接口,因为限定边界的泛型可能会和指定类或接口没有任何区别。
某些编程语言提供的一种解决方案称为潜在类型机制或结构化类型机制,而更古怪的术语称为鸭子类型机制,即“如果它走起来像鸭子,并且叫起来也像鸭子,那么你就可以将它当做鸭子对待。”鸭子类型机制变成了一种相当流行的术语,可能是因为它不像其他的术语那样承载着历史的报复。
泛型代码典型地将在泛型类型上调用少量方法,而具有潜在类型机制的语言只要求实现某个方法子集,而不是某个特定类或接口,从而放松了这种限制(并且可以产生更加泛化的代码)。正由于此,潜在类型机制使得你可以横跨类继承结构,调用不属于某个公共接口的方法。因此,实际上一段代码可以声明:“我不关心你是什么类型,只要你可以speak()和sit()即可。”由于不要求具体类型,因此代码就可以更加泛化。
潜在类型机制是一种代码组织和复用机制。有了它编写出的代码相对于没有它编写出的代码,能够更容易地复用。代码组织和复用是所有计算机编程的基本手段:编写一次,多次使用,并在一个位置保存代码。因为我并未被要求去命名我的代码要操作与其上的确切接口,所以,有了潜在类型机制,我就可以编写更少的代码,并更容易地将其应用于多个地方。
两种支持潜在类型机制的语言实例是Python和C++。Python是动态类型语言(事实上所有的类型检查都发生在运行时),而C++是静态类型语言(类型检查发生在编译器),因此潜在类型机制不要求静态或动态类型检查。
如果我们将上面的描述用Python来表示,如下所示:
#: generics/DogsAndRobots.py
class Dog:
def speak(self):
print "Arf!"
def sit(self):
print "Setting"
def reproduce(self):
pass
class Robot:
def speak(self):
print "Click!"
def sit(self):
print "Clank"
def oilChange(self):
pass
def perform(anything):
anything.speak()
anything.sit()
Python使用缩进来确定作用域(因此不需要任何花括号),而冒号将表示新的作用域的开始。“#”表示注释到行位,就像Java中的“//”。类的方法需要显示地指定this引用的等价物作为第一个参数,按惯例称为self。构造器调用不要求任何类型的“new”关键字,并且Python允许正则(非成员)函数,就像perform()所表明的那样。
注意,在perform(anything)中,没有任何针对anything的类型,anything只是一个标识符,它必须能够执行perform()期望它执行的操作,因此这里隐含着一个接口。但是你从来都不必显示地写出这个接口——它是潜在的。perform()不关心其参数的类型,因此我可以向它传递任何对象,只要该对象支持speak()和sit()方法。如果传递给perform()的对象不支持这些操作,那么将会得到运行时异常。
我们可以用C++产生相同的效果:
//: generics/DogsAndRobots.cpp
class Dog{
public:
void speak(){}
void sit(){}
void reproduce(){}
}
class Robot{
public:
void speak(){}
void sit(){}
void oilChange(){}
}
template<class T> void perform(T anything){
anything.speak();
anything.sit();
}
int main(){
Dog d;
Robot r;
perform(d);
perform(r);
}
在Python和C++中,Dog和Robot没有任何共同的东西,只是碰巧有两个方法具有相同的签名。从类型观点看,它们是完全不同的类型。但是,perform()不关心其参数的具体类型,并且潜在类型机制允许它接受这两种类型的对象。
C++确保了它实际上可以发送的那些消息,如果试图传递错误类型,比那一起就会给你一个错误消息(这些错误消息从历史上看是相当可怕和冗长的,而主要原因是因为C++的模板名声欠佳)。尽管它们是在不同时期实现这一点的,C++在编译器,而Python在运行时,但是这两种语言都可以确保类型不会被误用,因此被认为是强类型的。潜在类型机制没有损害强类型机制。
因为泛型是在这场竞赛的后期才添加到Java中的,因此没有任何机会可以去实现任何类型的潜在类型机制,因此Java没有对这种特性的支持。所以,初看起来,Java的泛型机制比支持潜在类型机制的语言更“缺乏泛化性”。例如,如果我们试图用Java实现上面的示例,那么就会被强制要求使用一个类或接口,并在边界表达式中指定它:
package com.zjwave.thinkinjava.generics;
public interface Performs {
void speak();
void sit();
}
package com.zjwave.thinkinjava.generics;
import com.zjwave.thinkinjava.typeinfo.pets.Dog;
// No latent typing in Java
public class DogsAndRobots {
public static void main(String[] args) {
PerformingDog d = new PerformingDog();
Robot r = new Robot();
Communicate.perform(d);
Communicate.perform(r);
}
}
class PerformingDog extends Dog implements Performs{
@Override
public void speak() {
System.out.println("Woof!");
}
@Override
public void sit() {
System.out.println("Sitting");
}
public void reproduce(){
}
}
class Robot implements Performs{
@Override
public void speak() {
System.out.println("Click!");
}
@Override
public void sit() {
System.out.println("Clank!");
}
public void oilChange(){
}
}
class Communicate{
public static <T extends Performs> void perform(T performer){
performer.speak();
performer.sit();
}
}
但是要注意,perform()不需要使用泛型来工作,它可以被简单地指定为接受一个Performs对象:
package com.zjwave.thinkinjava.generics;
public class SimpleDogsAndRobots {
public static void main(String[] args) {
CommunicateSimply.perform(new PerformingDog());
CommunicateSimply.perform(new Robot());
}
}
class CommunicateSimply{
static void perform(Performs performer){
performer.speak();
performer.sit();
}
}
在本例中,泛型不是必须的,因为这些类已经被强制要求实现Performs接口。
17.对缺乏潜在类型机制的补偿
尽管Java不支持潜在类型机制,但是这并不意味着有界泛型代码不能在不同的类型层次结构之间应用。也就是说,我们仍旧可以创建真正的泛型代码,但是这需要付出一些额外的努力。
17.1 反射
可以使用的一种方式是反射,下面的perform()方法就是用了潜在类型机制:
package com.zjwave.thinkinjava.generics;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
public class LatentReflection {
public static void main(String[] args) {
CommunicateReflectively.perform(new SmartDog());
CommunicateReflectively.perform(new Robot());
CommunicateReflectively.perform(new Mime());
}
}
class Mime{
public void walkAgainstTheWind(){}
public void sit(){
System.out.println("Pretending to sit");
}
public void pushInvisibleWalls(){}
@Override
public String toString() {
return "Mime";
}
}
//Does not implements Performs:
class SmartDog{
public void speak(){
System.out.println("Woof!");
}
public void sit(){
System.out.println("Sitting");
}
public void reproduce(){
}
}
class CommunicateReflectively {
public static void perform(Object speaker){
Class<?> spkr = speaker.getClass();
try {
try {
Method speak = spkr.getMethod("speak");
speak.invoke(speaker);
} catch (NoSuchMethodException e) {
System.out.println(speaker + " cannot speak");
}
try {
Method sit = spkr.getMethod("sit");
sit.invoke(speaker);
} catch (NoSuchMethodException e) {
System.out.println(speaker + " cannot sit");
}
}catch (Exception e){
throw new RuntimeException(speaker.toString(),e);
}
}
}
上例中,这些类完全是彼此分离的,没有任何公共基类(除了Object)或接口。通过反射,CommunicateReflectively.perform()能够动态地确定所需要的方法是否可用并调用它们。它甚至能够处理Mime只具有一个必须的方法这一事实,并能够部分实现其目标。
17.2 将一个方法应用于序列
反射提供了一些有趣的可能性,但是它将所有的类型检查都转移到了运行时,因此在许多情况下并不是我们所希望的。如果能够实现编译器类型检查,这通常会更符合要求。但是有可能实现比去年一起类型检查和潜在类型机制吗?
让我们看一个说明这个问题的示例。假设想要创建一个apply()方法,它能够将任何方法应用于某个序列中的所有对象。这是接口看起来并不合适的情况,因为你想要将任何方法应用于一个对象集合,而接口对于描述“任何方法”存在过多的限制。如何用Java来实现这个需求呢?
最初,我们可以用反射来解决这个问题,由于有了Java SE5的可变参数,这种方式被证明是相当优雅的:
package com.zjwave.thinkinjava.generics;
import java.lang.reflect.Method;
import java.util.ArrayList;
import java.util.Iterator;
import java.util.LinkedList;
import java.util.List;
public class Apply {
public static <T, S extends Iterable<? extends T>> void apply(S seq, Method f, Object... args) {
try {
for (T t : seq) {
f.invoke(t,args);
}
} catch (Exception e) {
// Failures are programmer errors
throw new RuntimeException(e);
}
}
}
class Shape {
public void rotate() {
System.out.println(this + " rotate");
}
public void resize(int newSize) {
System.out.println(this + " resize " + newSize);
}
}
class Square extends Shape {
}
class FilledList<T> extends ArrayList<T> {
public FilledList(Class<? extends T> type, int size) {
try {
for (int i = 0; i < size; i++) {
// Assumes default constructor:
add(type.newInstance());
}
} catch (Exception e) {
throw new RuntimeException(e);
}
}
}
class ApplyTest {
public static void main(String[] args) throws Exception{
List<Shape> shapes = new ArrayList<>();
for (int i = 0; i < 10; i++) {
shapes.add(new Shape());
}
Apply.apply(shapes,Shape.class.getMethod("rotate"));
Apply.apply(shapes,Shape.class.getMethod("resize",int.class),5);
List<Square> squares = new ArrayList<>();
for (int i = 0; i < 10; i++) {
squares.add(new Square());
}
Apply.apply(squares,Shape.class.getMethod("rotate"));
Apply.apply(squares,Shape.class.getMethod("resize",int.class),5);
Apply.apply(new FilledList<>(Shape.class,10),Shape.class.getMethod("rotate"));
Apply.apply(new FilledList<>(Square.class,10),Shape.class.getMethod("rotate"));
SimpleQueue<Shape> shapeQ = new SimpleQueue<>();
for (int i = 0; i < 5; i++) {
shapeQ.add(new Shape());
shapeQ.add(new Square());
}
Apply.apply(shapeQ,Shape.class.getMethod("rotate"));
}
}
class SimpleQueue<T> implements Iterable<T>{
private LinkedList<T> storage = new LinkedList<>();
public void add(T t){
storage.offer(t);
}
public T get(){
return storage.poll();
}
@Override
public Iterator<T> iterator() {
return storage.iterator();
}
}
在Apply中,apply()方法可以接受任何实现了Iterable接口的事物,包括诸如List这样的所有Collection类。但是它还可以接受其他任何事物,只要能够使这些事物是Iterable的——例如,自定义的SimpleQueue类。
在Apply.java中,异常被转换为RuntimeException,因为没有多少办法可以从这种异常中恢复——在这种情况下,它们实际上代表着程序员的错误。
注意,我必须放置边界和通配符,以便使Apply和FilledList在所有需要的情况下都可以使用。可以试验一下,将这些边界和通配符拿出来,你就会发现某些Apply和FilledList应用将无法工作。
FilledList表示有点进退两难的情况。为了使某种类型可用,它必须有默认(无参)构造器,但是Java没有任何方式可以在编译期断言这种事情,因此这就变成了一个运行时问题。确保编译期检查的常见建议是定义一个工厂接口,它有一个可以生成对象的方法,然后FilledList将接受这个接口而不是这个类型标记的“原生工厂”,而这样做的问题是在FilledList中使用的所有类都必须实现这个工厂接口。哎,大多数的类都是在不了解你的接口的情况下创建的,因此也就没有实现这个接口。
但是上面所展示的使用类型标记的方法可能是一种合理的折中(至少是一种马上就能想到的解决方案)。通过这种方式,使用想FilledList这样的东西就会非常容易,我们会马上想到要使用它而不是会忽略它。当然,因为错误是在运行时报告的,所以你要有把握,这些错误将在开发过程的早期出现。
注意,类型标记技术是Java文献推荐的技术,这是一种惯用法,例如,在操作注解的新API中得到了广泛的应用。但是,我发现人们对这种技术的适应程度不一,有些人强烈地首选前面描述的工厂方式。
尽管Java解决方案被证明很优雅,但是我们必须知道使用反射(尽管反射在最近版本的Java中已经明显地改善了)可能比非反射的实现要慢一些,因为有太多的动作都是在运行时发生的。这不应该阻止你使用这种解决方案的脚步,至少可以将其作为一种马上就能想到的解决方案(以防止陷入不成熟的优化中),但这毫无疑问是这两种方法之间的一个差异。
17.3 当你并未碰巧拥有正确的接口时
上面的示例是受益的,因为Iterable接口已经是Java类库中已经有的,而它正是我们需要的。但是更一般的情况又会怎样呢?如果不存在刚好适合你的需求的接口呢?
例如,让我们泛化FilledList中的四项,创建一个参数化的方法fill(),它接受一个序列,并使用Generator填充它。当我们尝试着用Java来编写时,就会陷入问题之中,因为没有任何像前面示例中的Iterable接口那样的“Addable”便利接口。因此你不能说:“可以在任何事物上调用add()。”而必须说:“可以在Collection的子类型调用add()。”这样产生的代码并不是特别泛化,因为它必须限制为只能工作于Collection实现。如果我试图使用没有实现Collection的类,那么我的泛化代码将不能工作。下面是这段代码段的样子:
package com.zjwave.thinkinjava.generics;
import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
public class Fill {
public static <T> void fill(Collection<T> collection,Class<? extends T> classToken,int size){
for (int i = 0; i < size; i++) {
//Assumes default constructor:
try{
collection.add(classToken.newInstance());
}catch (Exception e){
throw new RuntimeException(e);
}
}
}
}
class Contract{
private static long counter = 0;
private final long id = counter++;
@Override
public String toString() {
return getClass().getName() + " " + id;
}
}
class TitleTransfer extends Contract{}
class FillTest{
public static void main(String[] args) {
List<Contract> contracts = new ArrayList<>();
Fill.fill(contracts,Contract.class,3);
Fill.fill(contracts,TitleTransfer.class,2);
for (Contract c : contracts) {
System.out.println(c);
}
SimpleQueue<Contract> contractQueue = new SimpleQueue<>();
// Won't work. fill() is not generic enough:
//Fill.fill(contractQueue,Contract.class,3);
}
}
这正是具有潜在类型机制的参数化类型机制的价值所在,因为你不会受任何特定类库的创建者过去所作的设计角色的支配,因此不需要在每次碰到一个没有考虑到你的具体情况的新类库时,都去重写代码(因此这样的代码才是真正“泛化的”)。在上面的情况中,因为Java设计者(可以理解地)没有预见到对“Addable”接口的需要,所以我们被限制在Collection继承层次结构之内,即便SimpleQueue有一个add()方法,它也不能工作。因为这会将代码限制为只能工作于Collection,因此这样的代码不是特别“泛化”。有了潜在类型机制,情况就会不同了。
17.4 用适配器仿真潜在类型机制
Java泛型并没有潜在类型机制,而我们需要像潜在类型机制这样的东西去编写能够跨类边界应用的代码(也就是“泛化”代码)。存在某种方式可以绕过这项限制吗?
潜在类型机制将在这里实现什么?它意味着你可以编写代码声明:“我不关心我在这里使用的类型,只要它具有这些方法即可。”实际上,潜在类型机制创建了一个包含所需方法的隐式接口。因此它遵循这样的规则:如果我们手工编写了必须的接口(因为Java并没有为我们做这些事),那么它就应该能够解决问题。
从我们拥有的接口中编写代码来产生我们需要的接口,这是适配器设计模式的一个典型事例。我们可以使用适配器来适配已有的接口,以产生想要的接口。下面这种使用前面定义的Coffe继承结构的解决方案演示了编写适配器的不同方式:
package com.zjwave.thinkinjava.generics;
import com.zjwave.thinkinjava.generics.coffee.Coffee;
import com.zjwave.thinkinjava.generics.coffee.Latte;
import com.zjwave.thinkinjava.generics.coffee.Mocha;
import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
public class Fill2 {
//Classtoken version:
public static <T> void fill(Addable<T> addable,Class<? extends T> classToken,int size){
for (int i = 0; i < size; i++) {
try {
addable.add(classToken.newInstance());
}catch (Exception e){
throw new RuntimeException(e);
}
}
}
// GEnerator version:
public static <T> void fill(Addable<T> addable,Generator<T> generator,int size){
for (int i = 0; i < size; i++) {
addable.add(generator.next());
}
}
}
interface Addable<T> {
void add(T t);
}
//To adapt a base type,you must use composition.
//Make any Collection Addable using composition:
class AddableCollectionAdapter<T> implements Addable<T>{
private Collection<T> c;
public AddableCollectionAdapter(Collection<T> c) {
this.c = c;
}
@Override
public void add(T t) {
c.add(t);
}
}
// A Helper to capture the type automatically
class Adapter{
public static <T> Addable<T> collectionAdapter(Collection<T> c){
return new AddableCollectionAdapter<>(c);
}
}
// To adapt a specific type , you can use inheritance.
// Make a SimpleQueue Addable using inheritance:
class AddableSimpleQueue<T> extends SimpleQueue<T> implements Addable<T>{
@Override
public void add(T t) {
super.add(t);
}
}
class Fill2Test{
public static void main(String[] args) {
//Adapt a Collection:
List<Coffee> carrier = new ArrayList<>();
Fill2.fill(new AddableCollectionAdapter<>(carrier),Coffee.class,3);
//Helper method captures the type:
Fill2.fill(Adapter.collectionAdapter(carrier), Latte.class,2);
for (Coffee c : carrier) {
System.out.println(c);
}
System.out.println("----------------");
// Use an adapted class:
AddableSimpleQueue<Coffee> coffeeQueue = new AddableSimpleQueue<>();
Fill2.fill(coffeeQueue, Mocha.class,4);
Fill2.fill(coffeeQueue, Latte.class,1);
for (Coffee c : coffeeQueue) {
System.out.println(c);
}
}
}
Fill2对Colletion的要求与Fill不同,它只需要实现了Addable的对象,而Addable已经为Fill编写了——它是我希望编译器帮我创建的潜在类型的一种体现。
在这个版本中,我还添加了一个重载的fill(),它接受一个Generator而不是类型标记。Generator在编译期是类型安全的:编译器将确保传递的是正确的Generator,因此不会抛出任何异常。
第一个适配器,AddableCollectionAdapter,可以工作于基类型Collection,意味着Collection的任何实现都可以使用。这个版本直接存储Collection引用,并使用它来实现add()。
如果有一个具体类型而不是继承结构的基类,那么当使用继承来创建适配器时,你可以稍微少编写一些代码,就像在AddableSimpleQueue中看到的那样。
在Fill2Test.main()中,你可以看到各种不同类型的适配器在运行。首先,Collection类型是由AddableCollectionAdapter适配的。这个第二个版本使用了一个泛化的辅助方法,你可以看到这个泛化方法是如何捕获类型并因此而不必显示地写出来的——这是产生更优雅的代码的一种惯用技巧。
接下来,使用了预适配的AddableSimpleQueue。注意,在两种情况下,适配器都允许前面没有实现Addable的类用于Fill2.fill()中。
18.将函数对象用作策略
最后一个示例通过使用前面一节描述的适配器方式创建了真正泛化的代码。这个示例开始时是一种尝试,要创建一个元素序列的综合,这些元素可以是任何可以计算总和的类型,但是,后来这个示例使用功能型编程风格,演化成了可以执行通用的操作。
如果只查看尝试添加对象的过程,就会看到这是在多个类中的公共操作,但是这个操作没有在任何我们可以指定的基类中表示——有时甚至可以使用“+”操作符,而其他时间可以使用某种add方法。这是在试图编写泛化代码的时候通常会碰到的情况,因为你想将这些代码应用于多个类上——特别是,像本例一样,作用于多个已经存在且我们不能“修正”的类上。即使可以将这种情况摘花到Number的子类,这个超类也不包括任何有关“可添加性”的东西。
解决方案是使用策略设计模式,这种设计模式可以产生更优雅的代码,因为它将“变化的事物”完全隔离到了一个函数对象中。函数对象就是在某种程度上行为像函数的对象——一般地,会有一个相关的方法(在支持操作符重载的语言中,可以创建对这个方法的调用,而这个调用看起来就和普通的方法调用一样)。函数对象的价值就在于,与普通方法不同,它们可以传递出去,并且还可以拥有在多个调用之间持久化的状态。当然,可以用类中的任何方法来实现与此相似的操作,但是(与使用任何设计模式一样)函数对象主要是由其目的来区别的。这里的目的就是要创建某种事物,使它的行为就像是一个可以传递出去的单个方法一样,这样,它就和策略设计模式紧耦合了,有时甚至无法区分。
尽管可以发现我使用了大量的设计模式,但是在这里它们之间的界限是模糊的:我们在创建执行适配操作的函数对象,而它们将被传递到用作策略的方法中。
通过采用这种方式,我添加了最初着手创建的各种类型的泛型方法,以及其他的泛型方法。
下面是产生的结果:
package com.zjwave.thinkinjava.generics;
import java.math.BigDecimal;
import java.math.BigInteger;
import java.math.MathContext;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Iterator;
import java.util.List;
import java.util.concurrent.atomic.AtomicLong;
public class Functional {
// Calls the combiner object on each element to combine
// it with a running result , which is finally returned:
public static <T> T reduce(Iterable<T> seq, Combiner<T> combiner) {
Iterator<T> it = seq.iterator();
if (it.hasNext()) {
T result = it.next();
while (it.hasNext()) {
result = combiner.combine(result, it.next());
}
return result;
}
// If seq is the empty list
return null; // Or throw exception
}
// Take a function object and call it on each object in
// the list, ignoring the return value. The function
// object may act as a collecting parameter, so it is
// returned at the end
public static <T> Collector<T> forEach(Iterable<T> seq, Collector<T> func) {
for (T t : seq) {
func.function(t);
}
return func;
}
// Creates a list of results by calling a
// function object for each object in the list:
public static <R, T> List<R> transform(Iterable<T> seq, UnaryFunction<R, T> func) {
List<R> result = new ArrayList<>();
for (T t : seq) {
result.add(func.function(t));
}
return result;
}
// Applies a unary predicate to each item in a sequence,
// and returns a list of items that produced "true":
public static <T> List<T> filter(Iterable<T> seq, UnaryPredicate<T> pred) {
List<T> result = new ArrayList<>();
for (T t : seq) {
if (pred.test(t)) {
result.add(t);
}
}
return result;
}
// To use the above generic methods , we need to create
// function objects to adapt to our particular need:
static class Integeradder implements Combiner<Integer> {
@Override
public Integer combine(Integer x, Integer y) {
return x + y;
}
}
static class IntegerSubtracter implements Combiner<Integer> {
@Override
public Integer combine(Integer x, Integer y) {
return x - y;
}
}
static class BigDecimalAdder implements Combiner<BigDecimal> {
@Override
public BigDecimal combine(BigDecimal x, BigDecimal y) {
return x.add(y);
}
}
static class BigIntegerAdder implements Combiner<BigInteger> {
@Override
public BigInteger combine(BigInteger x, BigInteger y) {
return x.add(y);
}
}
static class AtomicLongAdder implements Combiner<AtomicLong> {
@Override
public AtomicLong combine(AtomicLong x, AtomicLong y) {
// not clear whether this is meaningful:
return new AtomicLong(x.addAndGet(y.get()));
}
}
// We can even make a UnaryFunction with an "ulp"
// (Units is the last place):
static class BigDecimalUlp implements UnaryFunction<BigDecimal, BigDecimal> {
@Override
public BigDecimal function(BigDecimal x) {
return x.ulp();
}
}
static class GreaterThan<T extends Comparable<T>> implements UnaryPredicate<T> {
private T bound;
public GreaterThan(T bound) {
this.bound = bound;
}
@Override
public boolean test(T x) {
return x.compareTo(bound) > 0;
}
}
static class MultiplyingIntegerCollector implements Collector<Integer> {
private Integer val = 1;
@Override
public Integer function(Integer x) {
val *= x;
return val;
}
@Override
public Integer result() {
return val;
}
}
public static void main(String[] args) {
//Generics , varargs & boxing working together:
List<Integer> li = Arrays.asList(1, 2, 3, 4, 5, 6, 7);
Integer result = reduce(li, new Integeradder());
System.out.println(result);
result = reduce(li, new IntegerSubtracter());
System.out.println(result);
System.out.println(filter(li, new GreaterThan<>(4)));
System.out.println(forEach(li, new MultiplyingIntegerCollector()).result());
System.out.println(forEach(filter(li,new GreaterThan<>(4)),new MultiplyingIntegerCollector()).result());
MathContext mc = new MathContext(7);
List<BigDecimal> lbd = Arrays.asList(new BigDecimal(1.1, mc), new BigDecimal(2.2, mc), new BigDecimal(3.3, mc), new BigDecimal(4.4, mc));
BigDecimal rbd = reduce(lbd, new BigDecimalAdder());
System.out.println(rbd);
System.out.println(filter(lbd,new GreaterThan<>(new BigDecimal(3))));
// Use the prime-generation facility of BigInteger:
List<BigInteger> lbi = new ArrayList<>();
BigInteger bi = BigInteger.valueOf(11);
for (int i = 0; i < 11; i++) {
lbi.add(bi);
bi = bi.nextProbablePrime();
}
System.out.println(lbi);
BigInteger rbi = reduce(lbi, new BigIntegerAdder());
System.out.println(rbi);
// The sum of this list of primes is also prime:
System.out.println(rbi.isProbablePrime(5));
List<AtomicLong> lal = Arrays.asList(new AtomicLong(11), new AtomicLong(47), new AtomicLong(74), new AtomicLong(133));
AtomicLong ral = reduce(lal, new AtomicLongAdder());
System.out.println(ral);
System.out.println(transform(lbd,new BigDecimalUlp()));
}
}
//Different types of function object:
interface Combiner<T> {
T combine(T x, T y);
}
interface UnaryFunction<R, T> {
R function(T x);
}
interface Collector<T> extends UnaryFunction<T, T> {
T result();//Extract result of collecting parameter
}
interface UnaryPredicate<T> {
boolean test(T x);
}
我是从为不同类型的函数对象定义接口开始的,这些接口都是按需创建的,因为我为每个接口都开发了不同的方法,并发现了每个接口的需求。Combiner类抽象掉了将两个对象添加在一起的具体细节,并且只是声明它们在某种程度上被结合在一起。因此,可以看到Integeradder和IntegerSubstract可以是Combiner类型。
UnaryFunction接受单一的参数,并产生一个结果;这个参数和结果不需要是相同的类型。Collector被用作“收集”参数,并且当你完成时,可以从中抽取结果。UnaryPredicate将产生一个boolean类型的结果。还可以创建其他类型的函数对象,但是这些已经足够说明问题了。
Functional类包含大量的泛型方法,它们可以将函数对象应用于序列。reduce()将Combiner中的函数应用于序列中的每个元素,以产生单一的结果。
forEach()接受一个Collector,并将其函数应用于每个元素,但同时会忽略每次函数调用的结果。这只能被称为是副作用(这不是“功能型”编程风格,但仍旧是有用的),或者我们可以让Collector维护内部状态,从而变成一个收集参数,就像在本例中看到的那样。
transform()通过在序列中的每个对象上调用UnaryFunction,并捕获调用结果,来产生一个列表。
最后,filter()将UnaryPredicate应用到序列中的每个对象上,并将那些返回true的对象存储到一个List中。
可以定义附加的泛型函数,例如,C++ STL就具有很多这类函数。
在C++中,潜在类型机制将你在调用函数时负责协调各个操作,但是在Java中,我们需要编写函数对象来将泛型方法适配为我们特定的需求。因此,这个类接下来的部分展示了函数对象的各种不同的实现。例如,注意,IntegerAdder和BigDecimalAdd通过为它们特定的类型调用恰当的方法,从而解决了相同的问题,即两个对象相加。因此,这是适配器模式和策略模式的结合。
在main()中,你可以看到,在每个方法调用中,都会传递一个序列和适当的函数对象。还有大量的、可能会相当复杂的表达式,例如:
forEach(filter(li,new GreaterThan<>(4)),new MultiplyingIntegerCollector()).result()
这将通过选取li中大于4的所有元素而产生一个列表,然后将MultiplyingIntegerCollector应用于所产生的列表,并抽取result()。
19.总结:转型真的如此之糟吗?
使用泛型类型机制的最吸引人的地方,就是在使用容器类的地方,这些类包括诸如各种List、各种Set、各种Map等集合中。在Java SE5之前,当你将一个对象放置到容器中时,这个对象就会被向上转型为Object,因此你会丢失类型信息。当你想要将这个对象从容器中取回,用它去执行某些操作时,必须将其向下转型回正确的类型。如果没有Java SE5的泛型版本的容器,你放到容器里的和从容器中取回的,都是Object。因此,我们很可能会将一个Dog放置到Cat的List中。
但是,泛型出现之前的Java并不会让你误用放入到容器中的对象。如果将一个Dog扔到Cat的容器中,并试图将这个容器中的所有东西都当作Cat处理,那么当你从这个Cat容器中取回那个Dog引用,并试图将其转型为Cat时,就会得到一个RuntimeException。你仍旧可以发现问题,但是是在运行时而非编译期发现它的。
但是,我相信被称为泛型的通用语言特性(并非必须是其在Java中的特定实现)的目的在于可表达性,而不仅仅是为了创建类型安全的容器。类型安全的容器是能够创建更通用代码这一能力所带来的副作用。
因此,即便“Dog在Cat列表中”这个论据经常被用来证明泛型是必要的,但是它仍旧是有问题的。就像我在本文开头声称的,我不相信这就是泛型这个概念真正的含义。相反,泛型正如其名称所暗示的:它是一种方法,通过它可以编写出更“泛化”的代码,这些代码对于它们能够作用的类型具有更少的限制,因此单个的代码段可以应用到更多的类型上。正如你在本文中看到的,编写真正泛化的“持有器”类(Java的容器就是这种类)相当简单,但是编写出能够操作其泛型类型的泛化代码就需要额外的努力了,这些努力需要类创建者和类消费者共同付出,他们必须理解适配器设计模式的概念和实现。这些额外的努力会增加使用这种特性的难度,并可能会因此而使其在某些场合缺乏可用性,而在这些场合中,它可能会带来附加的价值。
还要注意到,因为泛型是后来添加到Java中,而不是从一开始就涉及到这种语言中的,所以某些容器无法达到它们应该具备的健壮性。例如,观察一下Map,在特定的方法containsKey(Object key)和get(Object key)中就包含这类情况。如果这些类是使用在它们之前就存在的泛型设计的,那么这些方法将会使用参数化类型而不是Object,因此也就可以提供这些泛型假设会提供的编译期检查。例如,在C++的map中,键的类型总是在编译期检查的。
有一件事很明显:在一种语言已经被广泛应用之后,在较新的版本中引入任何种类的泛型机制,都会是一项非常非常棘手的任务,并且是一项不付出艰辛就无法完成的任务。在C++中,模板是在其最初的ISO版本中就引入的(即便如此,也引发了阵痛,因为在第一个标准C++出现之前,有很多非模板版本在使用),因此实际上模板一直都是这种语言的一部分。在Java中,泛型是在这种语言首次发布大约10年之后才引入的,因此向泛型迁移的问题特别多,并且对泛型的设计产生了明显的影响。其结果就是,程序员将承受这些痛苦,而这一切都是由于java设计者在设计1.0版本时所表现出来的短视造成的。当java最初被创建时,它的设计者们当然了解C++的模板,他们甚至考虑将其囊括到Java语言中,但是出于这样或那样的原因,他们决定将模板排除在外(其迹象就是他们过于匆忙)。因此,Java语言和使用它的程序员都将承受这些痛苦。只有时间将会说明java的泛型方式对语言所造成的最终影响。
某些语言,已经融入了更简洁、影响更小的方式,来实现参数化类型。我们不可能不去想象这样的语言将会成为Java的继任者,因为它们采用的方式,与C++通过C来实现的方式相同:按原样使用它,然后对其进行改进。
所有源码均可在https://gitee.com/zjwave/thinkinjava中下载
关联文章:
Thinking in Java——Java异常体系(通过异常处理错误)
Thinking in Java——String及相关类库的使用
Thinking in Java——运行时类型信息(RTTI)以及反射
转载请注明原文链接:ZJ-Wave