第15章 泛 型
一般的类和方法,只能使用具体的类型,要么是基本类型,要么是自定义的类。如果要编写可以应用于多种类型的代码,这种刻板的限制对代码的束缚就会很大。
在面向对象的编程语言中,多态算是一种泛化的机制。例如,将方法参数设为基类,那么该方法就可以接收从这个基类中导出的任何类作为参数。在类的内部也如此,凡是需要说明类型的地方,如果都是用基类,确实能够具备更好的灵活性。(final类不能被继承,即不能成为基类)
单继承的体系,也使得程序受到限制。如果方法的参数是个接口,而不是类,则能够放松限制。因为任何实现了该接口的类都能够满足该方法。
但一旦指明了接口,意味着你的代码必须使用特定的接口。如果想要编写更加通用的代码,使代码能够应用于"某种不具体的类型",而不是一个具体的类或接口。泛型即可满足。
泛型 实现了参数化类型的概念,使代码可以应用于多种类型。
15.1 与C++比较
Java中的泛型与C++比较理由:
- 有助于理解泛型的基础,重要的是可以了解Java泛型的局限是什么,以及为什么会有这些限制。
- Java社区中,对C++模板的误解会令你在理解泛型的意图时产生偏差。
15.2 简单泛型
有许多原因促成了泛型的出现,而最引人注目的是,容器类的创造。容器就是存放要使用的对象的地方。事实上,所有的程序,在运行时都要求你持有一大堆对象,所以容器类算得上最具重用性的类库之一。
首先,只能持有单个对象的类:
class Automobile {
}
public class Holder1 {
private Automobile a;
public Holder1(Automobile a) {
this.a = a;
}
public Automobile getA() {
return a;
}
}
这个类的可重用性不行,它无法持有其他类型的任何对象。我们需要为每个类型都编写一个新的类。
在Java SE5之前,我们可以让这个类直接持有Object类型的对象:
public class Holder2 {
private Object o;
public Holder2(Object o) {
this.o = o;
}
public Object getO() {
return o;
}
public void setO(Object o) {
this.o = o;
}
public static void main(String[] args) {
Holder2 h2 = new Holder2(new Automobile());
Automobile a = (Automobile) h2.getO();
h2.setO("Not an Automobile");
String s = (String) h2.getO();
h2.setO(1);
Integer x = (Integer) h2.getO();
}
}
现在,Holder2可以存储任何类型的对象。
有些情况下,我们确实希望容器能够同时持有多种类型的对象。但是,通常我们只会使用容器来存储一种类型的对象。
泛型的主要目的之一是:用来指定容器要持有什么类型的对象,而且由编译器来保证类型的正确性。
我们通常需要的是:暂时不指定类型,在使用时决定类型。
要达到这个目的,我们需要使用类型参数,用尖括号括住,放在类名后,这个类即为泛型类:
public class Holder3<T> {
private T t;
public Holder3(T t) {
this.t = t;
}
public T getT() {
return t;
}
public void setT(T t) {
this.t = t;
}
public static void main(String[] args) {
Holder3<Automobile> h3 = new Holder3<Automobile>(new Automobile());
Automobile a = h3.getT();
// error
// h3.setT("Not an Automobile");
// h3.setT(1);
}
}
现在,当你创建泛型类(Holder3)对象时,必须指明想持有对象的类型,使用时,只能存入该类型(或其子类,因为多态与泛型不冲突)的对象。
Java泛型的核心概念:告诉编译器想使用什么类型,然后编译器会帮你处理一切细节。
所以,从泛型类取出它持有的对象时,自动地就是正确类型。
15.2.1 一个元组类库
我们经常遇到:一次方法调用需要返回多个对象,可是return语句只能允许返回单个对象。因此,我们需要创建一个对象,让它来持有想要返回的多个对象。
元组:将一组对象直接打包存储于其中的一个单一对象。这个容器对象允许读取其中元素,但不允许向其中存放新的对象。(也称为:数据传送对象,或信使)
通常,元组可以具有任意长度,同时,元组中的对象可以是任意不同的类型。不过,我们希望在使用时指明类型,并在读取时得到正确的类型。(泛型)
2维元组:
public class TwoTuple<A, B> {
public final A a;
public final B b;
public TwoTuple(A a, B b) {
this.a = a;
this.b = b;
}
public String toString() {
return "TwoTuple [a=" + a + ", b=" + b + "]";
}
}
注意:这里没有将成员变量设置为private并为它们提供getter()和setter()方法,而是选择使用public修饰,安全性由final提供。这种格式简洁明了。
我们可以利用继承实现长度更长的元组:
public class ThreeTuple<A, B, C> extends TwoTuple<A, B> {
public final C c;
public ThreeTuple(A a, B b, C c) {
super(a, b);
this.c = c;
}
public String toString() {
return "ThreeTuple [a=" + a + "b=" + b + "c=" + c + "]";
}
}
public class FourTuple<A, B, C, D> extends ThreeTuple<A, B, C> {
public final D d;
public FourTuple(A a, B b, C c, D d) {
super(a, b, c);
this.d = d;
}
public String toString() {
return "FourTuple [ a=" + a + ", b=" + b + ", c=" + c + ", d=" + d + "]";
}
}
需要返回多个类型的对象时,只需定义一个长度合适的元组,将其作为方法的返回值,在return 后返回即可。
class Amphibian {}
class Vehicle {}
public class TupleTest {
static TwoTuple<String, Integer> f() {
return new TwoTuple<String, Integer>("hi", 47);
}
static ThreeTuple<Amphibian, String, Integer> g() {
return new ThreeTuple<Amphibian, String, Integer>(new Amphibian(), "hi", 47);
}
static FourTuple<Vehicle, Amphibian, String, Integer> h() {
return new FourTuple<Vehicle, Amphibian, String, Integer>(new Vehicle(), new Amphibian(), "hi", 47);
}
public static void main(String[] args) {
System.out.println(f());
System.out.println(g());
System.out.println(h());
}
}
有了泛型,你只需要编写表达式即可创建元组,返回一组任意类型的对象。
15.2.2 一个堆栈类
在11章中,我们通过使用LinkedList实现Stack,现在通过内部链式存储机制实现:
public class LinkedStack<T> {
private static class Node<U> {
U item;
Node<U> next;
public Node() {
item = null;
next = null;
}
public Node(U item, Node<U> next) {
super();
this.item = item;
this.next = next;
}
boolean end() {
return item == null && next == null;
}
}
private Node<T> top = new Node<T>(); //End sentinel 末端哨兵
public void push(T item) {
top = new Node<T>(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<String>();
for (String s : "Phasers on stun!".split(" ")) {
lss.push(s);
}
String s;
while ((s = lss.pop()) != null) {
System.out.println(s);
}
}
}
两个泛型类,LinkedStack和Node,它们拥有自己的类型参数。从上例可以看出,泛型类型也就是另一种类型罢了。
15.2.3 RandomList
假设我们需要一个持有特定类型对象的列表,每次调用其上的select()方法时,它可以随见地选取一个元素。如果我们希望以此构建一个可以应用于各种类型的对象的工具,就需要使用泛型:
public class RandomList<T>{
private ArrayList<T> storage = new ArrayList<T>();
private Random rand = new Random(47);
public void add(T t){ storage.add(t); }
public T select(){
return storage.get(rand.nextInt(storage.size()));
}
public static void main(String[] args) {
RandomList<String> randomList = new RandomList<String>();
for (String s : "The quick brown fox jumped over the lazy brown dog".split(" ")) {
randomList.add(s);
}
for (int i = 0; i < 11; i++) {
System.out.print(randomList.select() + " ");
}
}
}
15.3 泛型接口
泛型也可以应用于接口。例如生成器(generator),这是一种专门负责创建对象的类,实际上,这是工厂方法设计模式的一种应用。 不过生成器不需要参数即可创建对象,工厂方法则需要参数。
一般而言,一个生成器只定义一个方法,该方法用以产生新的对象:
public interface Generator<T> {
T next();
}
方法next()返回的类型是参数化的T,如上例中,接口使用泛型与类使用泛型没有区别。
下例演示如何实现Generator接口:
public class Coffee {
private static long counter = 0;
private final long id = counter++;
public String toString() {
return getClass().getSimpleName() + " " + id;
}
}
public class Latte extends Coffee {}
public class Mocha extends Coffee {}
public class Cappuccino extends Coffee {}
public class Americano extends Coffee {}
public class Breve extends Coffee {}
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;
}
public Coffee next() {
try {
return (Coffee) types[rand.nextInt(types.length)].newInstance();
} catch (Exception e) {
throw new RuntimeException(e);
}
}
public Iterator<Coffee> iterator() {
return new Iterator<Coffee>() {
int count = size;
public Coffee next() {
count--;
return CoffeeGenerator.this.next();
}
public boolean hasNext() {
return count > 0;
}
public void remove() {
throw new UnsupportedOperationException();
}
};
}
public static void main(String[] args) {
CoffeeGenerator gen = new CoffeeGenerator();
for (int i = 0; i < 5; i++) {
System.out.println(gen.next());
}
for (Coffee coffee : new CoffeeGenerator(5)) {
System.out.println(coffee);
}
}
}
参数化的Generator接口确保next()的返回值是参数的类型。CoffeeGenerator同时实现了Iterable接口,所以可以在foreach语句中使用,并且第二个构造器为其提供了限制,判断何时停止。
下面是Generator< T >接口的另一实现,它负责生成Fibonacci数列:
public class Fibonacci implements Generator<Integer> {
private int count = 0;
public Integer next() {
return fib(count++);
}
private int fib(int n) {
if (n < 2)
return 1;
return fib(n - 2) + fib(n - 1);
}
public static void main(String[] args) {
Fibonacci gen = new Fibonacci();
for (int i = 0; i < 18; i++) {
System.out.print(gen.next() + " ");
}
}
}
虽然我们在使用Fibonacci类的时候都使用了int类型,但是Fibonacci类的类型参数却是Integer。上例说明了Java泛型的一个局限性:基本类型无法作为类型参数。不过,Java SE5具备自动拆装包的功能,可以很方便地在基本类型和其相应的包装器类型之间进行转换。
如果想要更进一步,编写一个实现了Iterable的Fibonacci生成器,我们可以选择:
-
重写这个类,令其实现Iterable。不过,你并不是总能拥有源代码的控制权,并且,除非必须那么做,否则,我们也不愿意重写一个类。
-
创建一个适配器,来实现所需要的接口,我们可以使用多种方法实现适配器。例如,通过继承来创建适配器类:
public class IterableFibonacci extends Fibonacci implements Iterable<Integer> {
private int n;
public IterableFibonacci(int count) {
this.n = count;
}
public Iterator<Integer> iterator() {
return new Iterator<Integer>() {
public Integer next() {
n--;
return IterableFibonacci.this.next();
}
public boolean hasNext() {
return n > 0;
}
public void remove() {
throw new UnsupportedOperationException();
}
};
}
public static void main(String[] args) {
for (Integer i : new IterableFibonacci(18)) {
System.out.print(i + " ");
}
}
}
如果要在Foreach语句中使用IterableFibonacci,必须向IterableFibonacci的构造器中提供一个边界值给hasNext()用于判断。
15.4 泛型方法
前面,我们看到的泛型作用在了类和接口上,同样,也可以作用在方法上。泛型方法所在的类可以是泛型类,也可以不是。
基本使用原则是:如果泛型方法可以取代将整个类泛型化,那么就应该只使用泛型方法,因为泛型方法使得该方法能够独立于类而产生变化,它更加精确化。
static方法如果需要使用泛型能力,它只能是泛型方法,因为其无法访问泛型类的类型参数。
定义泛型方法,只需要将泛型参数列表置于返回值之前,就像下面这样:
public class GenericMethods {
public <T> void f(T x) {
System.out.println(x.getClass().getSimpleName());
}
public static void main(String[] args) {
GenericMethods gm = new GenericMethods();
gm.f(1);
gm.f("s");
gm.f(1.0);
gm.f(1.0F);
gm.f(gm);
}
}
GenericMethods并不是参数化的,只有方法f()拥有类型参数。这由方法的返回类型前面的类型参数列表指明的。
注意:当使用泛型类时,必须在创建对象的时候指定类型参数的值,而使用泛型方法时,则不必指明参数类型,因为编译器会为我们找出具体的类型。这称为类型参数推断(type argument inference),因此,我们可以像调用普通方法一样调用f(),而f()好像被无限次地重载过。
15.4.1 利用类型参数推断
人们对泛型有一个抱怨,使用泛型有时候需要向程序中加入更多的代码。如果要创建一个持有List的Map,就要像下面:
Map<Person,List<? extends Pet>> petPerson =
new HashMap<Person,List<? extends Pet>>();
编译器本该是从泛型列表中的一个参数推断处另一个参数,但目前还未能实现。然而,在泛型方法中,类型参数推断可以为我们简化一部分工作,例如,我们可以编写一个工具类,它包含各种各样的static方法,专门用来创建各种常用的容器对象:
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.map())作为参数,传递给另一个方法,这时编译器会认为:调用泛型方法后,其返回值被赋值给Object类型的变量,从而不会执行类型推断:
public class LimitsOfInference {
static void f(Map<String, List<Integer>> map) {
}
public static void main(String[] args) {
// f(New.map()); //Does not compile
}
}
显式的类型说明
在泛型方法中,可以显式地指明类型,在点操作符与方法名之间插入尖括号,然后把类型置于尖括号内。(在定义方法的类内部,需要使用this,对于static方法,需要类名)
使用这种语法,可以解决上述将泛型方法调用结果作为参数而无法进行类型推断的问题:
public class ExplicitTypeSpecification {
static void f(Map<String, List<Integer>> map) {
}
public static void main(String[] args) {
f(New.<String, List<Integer>> map());
}
}
当然,这种方法抵消了New类为我们带来的好处(即省去了大量的类型说明),不过,只有在编写非赋值语句时,我们才需要这样的额外说明。
15.4.2 可变参数与泛型方法
泛型方法与可变参数列表能够很好地共存:
public class GenericVarargs {
public static <T> List<T> makeList(T... args) {
List<T> result = new ArrayList<T>();
for (T t : args) {
result.add(t);
}
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()方法相同的功能。
15.4.3 拥有Generator的泛型方法
利用生成器,我们可以很方便地填充一个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> coffee = fill(new ArrayList<Coffee>(), new CoffeeGenerator(), 4);
for (Coffee c : coffee) {
System.out.println(c);
}
Collection<Integer> fNumbers = fill(new ArrayList<Integer>(), new Fibonacci(), 12);
for (Integer n : fNumbers) {
System.out.print(n + " ");
}
}
}
15.4.4 一个通用的Generator
下面的程序可以为任何类构造一个Generator,只要该类具有默认的构造器:
public class BasicGenerator<T> implements Generator<T> {
private Class<T> type;
public BasicGenerator(Class<T> type) {
this.type = type;
}
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<T>(type);
}
}
需要用该生成器生成的类必须具备两个特点:
- 必须声明为public(生成器与要生成的类在不同的包)
- 必须具有默认构造器(Class.newInstance())
例如,下面是一个具有默认构造器的简单类:
public class CountedObject {
private static long counter = 0;
private final long id = counter++;
public long id() {
return id;
}
public String toString() {
return "CountedObject " + id;
}
}
使用BasicGenerator,可以很容易地为CountedObject创建一个Generator:
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对象的优点:通过参数传递Class对象进行类型推断,减少了使用new创建对象时的泛型参数说明。
15.4.5 简化元组的使用
有了类型参数推断,再加上static方法,我们可以重新编写元组工具:
public class Tuple {
public static <A, B> TwoTuple<A, B> tuple(A a, B b) {
return new TwoTuple<A, B>(a, b);
}
public static <A, B, C> ThreeTuple<A, B, C> tuple(A a, B b, C c) {
return new ThreeTuple<A, B, C>(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>(a, b, c, d);
}
}
下面是修改后的TupleTest.java:
public class TupleTest {
public static void main(String[] args) {
System.out.println(Tuple.tuple("hi", 47));
System.out.println(Tuple.tuple(new Amphibian(), "hi", 47));
System.out.println(Tuple.tuple(new Vehicle(), new Amphibian(), "hi", 47));
}
}
15.4.6 一个Set使用工具
通过使用泛型方法,用Set来表达数学中的关系式:
public class Sets {
public static <T> Set<T> union(Set<T> a, Set<T> b) {
Set<T> result = new HashSet<T>(a);
result.addAll(b);
return result;
}
public static <T> Set<T> intersection(Set<T> a, Set<T> b) {
Set<T> result = new HashSet<T>(a);
result.retainAll(b);
return result;
}
public static <T> Set<T> difference(Set<T> superset, Set<T> subset) {
Set<T> result = new HashSet<T>(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));
}
}
这四个方法中表达了如下数学集合操作:
- union():合并两个集合
- intersection():抽取公共部分元素
- difference():从superset中移除subset包含元素
- complement():除了交集外的所有元素
下面的示例使用Sets.difference()打印处java.util包中各种Collection类与Map类之间的方法差异:
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<String>();
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<?> superset, Class<?> subset) {
System.out.print(superset.getSimpleName() + " extends " + subset.getSimpleName() + ", adds: ");
Set<String> comp = Sets.difference(methodSet(superset), methodSet(subset));
comp.removeAll(object);
System.out.println(comp);
interfaces(superset);
}
public static void main(String[] args) {
System.out.println("Collection: " + methodSet(Collection.class));
interfaces(Collection.class);
System.out.println("--------------------");
difference(Set.class, Collection.class);
difference(HashSet.class, Set.class);
System.out.println("--------------------");
difference(List.class, Collection.class);
difference(ArrayList.class, List.class);
}
}
15.5 匿名内部类
泛型还可以应用于内部类以及匿名内部类,下例使用匿名内部类实现了Generator接口:
class Customer {
private static long counter = 1;
private final long id = counter++;
private Customer() {}
public String toString() {
return "Customer " + id;
}
public static Generator<Customer> generator() {
return new Generator<Customer>() {
public Customer next() {
return new Customer();
}
};
}
}
class Teller {
private static long counter = 1;
private final long id = counter++;
private Teller() {}
public String toString() {
return "Teller " + id;
}
public static Generator<Teller> generator =
new Generator<Teller>() {
public Teller next() {
return new Teller();
}
};
}
public class BankTeller {
public static void serve(Teller t, Customer c) {
System.out.println(t + " serves " + c);
}
public static void main(String[] args) {
Random rand = new Random(47);
Queue<Customer> line = new LinkedList<Customer>();
Generators.fill(line, Customer.generator(), 15);
List<Teller> tellers = new ArrayList<Teller>();
Generators.fill(tellers, Teller.generator(), 4);
for (Customer c : line) {
serve(tellers.get(rand.nextInt(tellers.size())), c);
}
}
}
Customer和Teller类都只有private的构造器,所以只能使用Generator对象去生成它们的对象。
15.6 构建复杂模型
泛型的一个重要好处是能够简单而安全地创建复杂的模型。 如:List元组:
public class TupleList<A, B, C, D> extends ArrayList<FourTuple<A, B, C, D>> {
public static void main(String[] args) {
TupleList<Vehicle, Amphibian, String, Integer> tl = new TupleList<Vehicle, Amphibian, String, Integer>();
tl.add(TupleTest.h());
tl.add(TupleTest.h());
for (FourTuple<Vehicle, Amphibian, String, Integer> t : tl) {
System.out.println(t);
}
}
}
尽管这看上去有些冗长(特别是迭代器的创建),但最终依然创造了一个相当强大的数据结构。
下面构建了一个零售店模型,包含走廊,货架和商品,它展示了使用泛型类型来构建复杂模型的简单之处,即使每个类都是作为一个构件块,但依然包含许多部分:
class Product {
private final int id;
private String desc;
private double price;
public Product(int id, String desc, double price) {
this.id = id;
this.desc = desc;
this.price = price;
System.out.println(this);
}
public String toString() {
return +id + ": " + desc + ", price: $" + price;
}
public void priceChange(double change) {
this.price += change;
}
public static Generator<Product> generator = new Generator<Product>() {
private Random rand = new Random(47);
public Product next() {
return new Product(rand.nextInt(1000), "Test",
Math.round(rand.nextDouble() * 1000.0) + 0.99);
}
};
}
class Shelf extends ArrayList<Product>{
public Shelf(int nProducts){
for (int i = 0; i < nProducts; i++) {
add(Product.generator.next());
}
}
}
class Aisle extends ArrayList<Shelf>{
public Aisle(int nShelfs ,int nProducts){
for (int i = 0; i < nShelfs; i++) {
add(new Shelf(nProducts));
}
}
}
class CheckoutStand{}
class Office{}
public class Store extends ArrayList<Aisle>{
private ArrayList<CheckoutStand> checkouts = new ArrayList<CheckoutStand>();
private Office office = new Office();
public Store(int nAisles , int nShelfs , int nProducts) {
for (int i = 0; i < nAisles; i++) {
add(new Aisle(nShelfs, nProducts));
}
}
public String toString() {
StringBuilder result = new StringBuilder();
for (Aisle a : this) {
for (Shelf s : a) {
for (Product p : s) {
result.append(p);
result.append("\n");
}
}
}
return result.toString();
}
public static void main(String[] args) {
System.out.println(new Store(2, 2, 10));
}
}
正如Store.toString()中所示,其结果是许多层容器,但它们是类型安全且可管理的,并且组装这个模型十分容易。
15.7 擦除的神秘之处
当深入钻研泛型时,会发现大量无意义的东西。例如,尽管可以声明ArrayList.class,但无法声明ArrayList.class,请思考如下情况:
public class ErasedTypeEquivalence {
public static void main(String[] args) {
Class c1 = new ArrayList<Integer>().getClass();
Class c2 = new ArrayList<String>().getClass();
System.out.println(c1 == c2);
}
}
由于不同类型在行为方面会不同,上述两个List存放不同类型的元素,所以会被认为是不同类型,可程序结果是相同类型。
下面示例对上述问题进行了分析:
class Frob{}
class Fnorkle{}
class Quark<Q>{}
class Particle<POSITION,MOMENTUM>{}
public class LostInformation {
public static void main(String[] args) {
List<Frob> list = new ArrayList<Frob>();
Map<Frob,Fnorkle> map = new HashMap<Frob, Fnorkle>();
Quark<Fnorkle> quark = new Quark<Fnorkle>();
Particle<Long,Double> p = new Particle<Long,Double>();
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.getTypeParameters():返回一个TypeVariable对象数组,表示有泛型声明的类型参数。但从输出可以看到,只是用作参数占位符的标识符,并非有用的信息。
因此,在泛型代码内部,无法获得任何有关泛型参数类型的信息。
所以,你可以知道诸如类型参数标识符和泛型类型边界这类的信息,却无法知道创建特定实例的具体的类型参数。
Java泛型是使用擦除来实现的,这意味着当你在使用泛型时,任何具体的类型信息都被擦除了,你唯一知道的就是你在使用一个对象。
15.7.1 C++的方式
下面是使用模板的C++示例,和Java用于参数化类型的语法十分相似:
#inclide <iostream>
using namespace std;
template<Class T> class Manipulator{
T obj;
public:
Manipulator(T x){ obj = x; }
void manipulate { obj.f(); }
};
class HasF {
public:
void f(){ cout << “HasF::f()” << endl;}
};
int main(){
HasF hf;
Manipulator<HasF> manipulator(hf);
manipulator.manipulate();
}
Manipulator类存储了一个类型T的对象,需要注意的是:manipulate()方法中,T类型的对象obj调用了方法f()。所以:当模板被实例化时,C++编译器才进行检查,根据实例化情况进行判断,这样类型安全就得到了保障。
用C++编写这种代码很简单,因为模板被实例化时就知道其模板参数的类型,Java泛型就不同。如果将manipulate()方法翻译成Java,则不能编译。
为了调用f(),我们必须协助泛型类,给定泛型类的边界,以此告知编译器只能接收遵循这个边界的类型。这里重用了extends关键字:
public class HasF {
public void f() {
System.out.println("HasF.f()");
}
}
class Manipulator<T extends HasF> {
private T obj;
public Manipulator(T obj) { this.obj = obj; }
public void manipulate() { obj.f(); }
}
public class Manipulation {
public static void main(String[] args) {
HasF hf = new HasF();
Manipulator<HasF> manipulation = new Manipulator<HasF>(hf);
manipulation.manipulate();
}
}
边界< T extends HasF > 声明T必须是HasF类型或从HasF导出的类型,这样就可以安全地在obj上调用f()了。就好像在类的声明中用HasF替换了T一样。
可以观察到,上例中,泛型并未贡献任何好处,因为我们自己很容易可以执行擦除,创建没有泛型的类:
public class Manipulator2 {
private HasF obj;
public Manipulator2(HasF obj) {
this.obj = obj;
}
public void manipulate(){
obj.f();
}
}
当我们需要返回确确切类型而非基类型时,< T extends HasF >便体现出用处了:
public class ReturnGenericType<T extends HasF> {
private T obj;
public ReturnGenericType(T obj) {
this.obj = obj;
}
public T get() {
return obj;
}
}
15.7.2 迁移兼容性
由于泛型不是Java语言出现时就有的组成部分,所以Java泛型不仅必须支持向后的兼容性(即现有的代码和类文件仍旧合法,并且继续保持之前的意思);而且还要支持迁移兼容性(即泛化的客户端可以用非泛化的类库来使用,反之亦然),当某个程序变为泛型时,不会破坏依赖它的代码和应用程序。为了达成这个目标,擦除是唯一可行的解决方案。
例如:
某个应用程序具有两个类库X和Y,且Y使用了类库Z。随着Java SE5的出现,希望将类库迁移到泛型上。 为了实现迁移兼容性,如果各个类库之间的调用和是否使用了泛型无关,则它们必须不具备探测其他类库是否使用了泛型的能力。因此,某个特定的类库使用了泛型这样的证据必须被擦除。
擦除减少了泛型的泛化性,但其仍然十分有用,但由于擦除,导致其不如想象中强大。
15.7.3 擦除的问题
擦除的主要作用是:从非泛化代码到泛化代码的转变过程,以及在不破坏现有类库的情况下,将泛型融入到Java语言。 擦除使得现在的非泛型客户端代码能够在不改变的情况下继续使用,直至客户端准备好用泛型重写这些代码。它不会突然间破坏所有现有的代码。
但是,擦除的代价是显著的:由于参数的类型信息丢失,所以泛型不能用于显式地引用运行时类型的操作,如instanceof和new。
当编写泛型代码时,好像看起来拥有有关参数的类型信息:
class Foo<T>{
T var;
}
通过创建泛型类的实例:
Foo<Cat> f = new Foo<Cat>();
泛型的语法会让我们产生错觉:在这个类中的各个地方,类型T都在被替换。事实上,它只是一个Object,并不会发生替换。
另外,擦除和迁移兼容性意味着,使用泛型并不是强制的:
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 extends GenericBase {}
public class ErasureAndInheritance {
@SuppressWarnings("unchecked")
public static void main(String[] args) {
Derived2 d2 = new Derived2();
Object obj = d2.get();
d2.set(obj);
}
}
Derived2 继承自 GenericBase ,但没有任何泛型参数,编译器会发出警告。
为了关闭警告,Java提供了注解。当你要关闭警告时,最好是尽量地"聚焦"。这样就不会因为过于宽泛地关闭警告,导致意外地屏蔽掉真正的问题。
15.7.4 边界处的动作
知道了擦除后,我们便觉得泛型参数不具有任何意义,其实不然:
public class FilledListMaker<T> {
List<T> create(T t, int n) {
ArrayList<T> result = new ArrayList<T>();
for (int i = 0; i < n; i++) {
result.add(t);
}
return result;
}
public static void main(String[] args) {
FilledListMaker<String> stringMarker = new FilledListMaker<String>();
List<String> stringList = stringMarker.create("Hello", 4);
System.out.println(stringList);
}
}
即使编译器无法知道有关create()的T的任何信息,但是它仍旧可以在编译期确保你放置到result中的对象具有T类型。
因此,即使擦除在方法或类内部移除了有关实际类型的信息,编译器仍旧可以确保在方法或类中使用的类型的内部一致性。
因为擦除在方法体中移除了类型信息,所以,在运行时的类型问题就发生在边界:即对象进入和离开方法的地点。这也正是编译器在编译期执行类型检查并插入转型代码的地点。对传递进来的值进行额外的编译期检查,并插入对传递出去的值的转型。
15.8 擦除的补救方式
擦除让任何运行时需要知道确切类型信息的操作都无法工作:
public class Erased<T> {
private static 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 = new Object[SIZE];
}
}
虽然如此,但我们可以通过引入类型标签来对擦除进行补救。 这意味着需要显式地传递类型的Class对象,以便可以在类型表达式中使用。
例如,在上例中由于类型信息被擦除无法使用instanceof,如果引入类型标签:
class Building{}
class House extends Building{}
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> ctc1 =
new ClassTypeCapture<Building>(Building.class);
System.out.println(ctc1.f(new Building()));
System.out.println(ctc1.f(new House()));
}
}
15.8.1 创建类型实例
new T()失败的原因:
- 由于擦除,T不具有意义。
- 编译器不能验证T具有默认(无参)构造器。
解决方案是传递一个工厂对象,并使用它来创建新的实例。
最便利的工厂对象就是Class对象,所以通过类型标签,你可以使用newInstance()来创建这个类型的新对象:
class ClassAsFactory<T> {
T x;
public ClassAsFactory(Class<T> kind) {
try {
x = kind.newInstance();
} catch (Exception e) {
throw new RuntimeException(e);
}
}
}
class Employee {}
public class InstantiateGenericType {
public static void main(String[] args) {
ClassAsFactory<Employee> fe = new ClassAsFactory<Employee>(Employee.class);
System.out.println("ClassAsFactory<Employee> successed");
try {
ClassAsFactory<Integer> fe2 = new ClassAsFactory<Integer>(Integer.class);
} catch (Exception e) {
System.out.println("ClassAsFactory<Integer> failed");
}
}
}
这可以编译,但会因为Intger没有默认无参构造器,创建实例会失败。 这个错误在编译期无法捕获,所以不建议使用。
推荐使用显性的工厂,并在实例泛型对象时,限制工厂类型,使其只能接收实现了这个工厂的类:
interface FactoryI<T> {
T create();
}
class Foo2<T> {
private T x;
public <F extends FactoryI<T>> Foo2(FactoryI<T> f) {
this.x = f.create();
}
}
class IntegerFactory implements FactoryI<Integer> {
public Integer create() {
return new Integer(0);
}
}
class Widght {
public static class Factory implements FactoryI<Widght> {
public Widght create() {
return new Widght();
}
}
}
public class FactoryConstraint {
public static void main(String[] args) {
new Foo2<Integer>(new IntegerFactory());
new Foo2<Widght>(new Widght.Factory());
}
}
传递FactoryI< T >是传递Class< T >的一种变体,两种方式都是传递了工厂对象,不过前者是显式的工厂对象,后者是内建的工厂对象。
另一种方式为模板方法设计模式:
abstract class GenericWithCreate<T> {
final T element;
public GenericWithCreate() {
this.element = create();
}
abstract T create();
}
class X{}
class Creator extends GenericWithCreate<X>{
X create() {
return new X();
}
void f(){
System.out.println(element.getClass().getSimpleName());
}
}
public class CreatorGeneric{
public static void main(String[] args) {
Creator c = new Creator();
c.f();
}
}
上例中,get()是模板方法,而create()是在子类中定义的,用来产生子类类型的对象。
15.8.2 泛型数组
正如Erased.java中所见,无法创建泛型数组。一般的解决方案是:在任何想要创建泛型数组的地方都使用ArrayList。
public class ListOfGenerics<T> {
private List<T> array = new ArrayList<T>();
public void add(T item) {
array.add(item);
}
public T get(int index) {
return array.get(index);
}
}
通过上例,你可以获得数组的行为,以及由泛型提供的编译期的类型安全。
当必须创建泛型类型的数组时,既然所有数组无论它们持有的类型如何,都具有相同的结果,我们尝试创建一个Object数组,并转型为泛化参数类型,可以编译,但不能允许,它将产生ClassCaseException:
class Generic<T> {}
public class ArrayOfGeneric {
static final int SIZE = 100;
static Generic<Integer>[] gia; //generic Integer Array
@SuppressWarnings("unchecked")
public static void main(String[] args) {
gia = (Generic<Integer>[]) new Object[SIZE];
}
}
问题在于:数组将跟踪它们的实际类型,而这个类型是在数组被创建时确定的,因此,即使gia已经转型为Generic[],但是这个信息只是存在于编译期,在运行时,它仍旧是Object数组。
成功创建泛型数组的唯一方式就是创建一个被擦除类型的心数组,然后对其转型:
gia = (Generic<Integer>[]) new Generic[SIZE];
下面是更复杂的示例,创建一个简单的泛型数组包装器:
public class GenericArray <T>{
private T[] array;
@SuppressWarnings("unchecked")
public GenericArray(int size) {
this.array = (T[]) new Object[size];
}
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) {
GenericArray<Integer> gia = new GenericArray<Integer>(10);
// This causes a ClassCastException
//! Integer[] ia = gia.rep();
// This is OK
Object[] oa = gia.rep();
}
}
由于不能声明T[] array = new T[size],我们选择创建一个对象数组,然后将其转型。在main()中,gai调用rep()应该返回Integer[],但使用Integer[]接收时,会得到ClassCastException,这还是因为数组实际运行时类型为Object[]。
因为有了擦除,数组的运行时类型就只能是Object[]。如果我们将其转型为T[],那么在编译期该数组的实际类型将丢失,而编译器则可能错过某些潜在的错误检查。所以,最好的是在集合内部使用Object数组,当你使用数组元素时,添加一个对T的转型:
public class GenericArray2<T> {
private Object[] array;
public GenericArray2(int size) {
this.array = new Object[size];
}
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;
}
public static void main(String[] args) {
GenericArray2<Integer> gia = new GenericArray2<Integer>(10);
for (int i = 0; i < 10; i++) {
gia.put(i, i);
}
for (int i = 0; i < 10; i++) {
System.out.print(gia.get(i) + " ");
}
System.out.println();
try {
Integer[] ia = gia.rep();
} catch (Exception e) {
System.out.println(e);
}
}
}
初看起来,似乎只是挪了下转型的地方,不过get()方法将对象转型为T是安全的,Object[]转型为T[]依然是会产生异常。因此:没有任何方式可以推翻底层的数组类型,它只能是Object[]。
当我们使用类型标记,便可以将类型从擦除中恢复,使我们可以创建需要的实际类型的数组,并返回它:
public class GenericArrayWithToken <T> {
private T[] array;
@SuppressWarnings("unchecked")
public GenericArrayWithToken(Class<T> type , int size) {
this.array = (T[]) Array.newInstance(type, size);
}
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) {
GenericArrayWithToken<Integer> gai =
new GenericArrayWithToken<Integer>(Integer.class, 10);
Integer[] ia = gai.rep();
}
}
该数组的运行时类型是确切类型T[]。
15.9 边界
边界使你可以在用于泛型的参数类型上设置限制条件。由于擦除移除了类型信息,用无界泛型参数调用的方法只是Object可以调将用的方法,如果将参数限制为某个类型,则可以调用这个类型拥有的方法。Java泛型重用了extends关键字执行这种限制:
interface HasColor {
Color getColor();
}
class Colored<T extends HasColor> {
T item;
Colored(T item) { this.item = item; }
T getItem() { return item; }
Color color() { return item.getColor(); }
}
class Dimension{
public int x,y,z;
}
class ColoredDimension <T extends Dimension & HasColor>{
T item;
ColoredDimension(T item) { this.item = item; }
T getItem() { return item; }
Color color() { return item.getColor(); }
int getX() { return item.x; }
int getY() { return item.y; }
int getZ() { return item.z; }
}
interface Weight{
int weight();
}
class Solid<T extends Dimension & HasColor & Weight>{
T item;
Solid(T item) { this.item = item; }
T getItem() { return item; }
Color color() { return item.getColor(); }
int getX() { return item.x; }
int getY() { return item.y; }
int getZ() { return item.z; }
int weight(){ return item.weight(); }
}
class Bounded extends Dimension implements HasColor,Weight{
public int weight() {
return 0;
}
public Color getColor() {
return null;
}
}
public class BasicBounds {
public static void main(String[] args) {
Solid<Bounded> solid = new Solid<Bounded>(new Bounded());
solid.color();
solid.getY();
solid.weight();
}
}
可以观察出,上述代码的类中含有大量重复代码,我们可以通过继承消除冗余:
class HoldItem<T> {
T item;
HoldItem(T item) { this.item = item; }
T getItem() { return item; }
}
class Colored2<T extends HasColor> extends HoldItem<T>{
Colored2(T item) { super(item); }
Color color(){ return super.item.getColor(); }
}
class ColoredDimension2<T extends Dimension & HasColor> extends Colored2<T>{
ColoredDimension2(T item) { super(item); }
int getX() { return item.x; }
int getY() { return item.y; }
int getZ() { return item.z; }
}
class Solid2<T extends Dimension & HasColor & Weight > extends ColoredDimension2<T>{
Solid2(T item) { super(item); }
int weight() { return super.item.weight(); }
}
public class InheritBounds {
public static void main(String[] args) {
Solid2<Bounded> solid2 = new Solid2<Bounded>(new Bounded());
solid2.color();
solid2.getY();
solid2.weight();
}
}
HoldItem直接持有一个对象,因此这种行为会被Colored2继承,并且它要求其参数类型为HasColor。通过进一步扩展这个层次结构,并在每个层次上都添加边界,这些方法都被继承,从而使代码不会在每个类中重复。
下面是具有更多层次的示例:
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;
SuperHero(POWER power) { this.power = power; }
POWER getPower() { return power; }
}
class SuperSleuth<POWER extends XRayVision> extends SuperHero<POWER>{
SuperSleuth(POWER power) { super(power); }
void see() { power.seeThroughWalls(); }
}
class CanineHero<POWER extends SuperHearing & SuperSmell> extends SuperHero<POWER>{
CanineHero(POWER power) { super(power); }
void hear() { power.hearSubtleNoises(); }
void smell() { power.trackBySmell(); }
}
class SuperHearSmell implements SuperHearing,SuperSmell{
public void trackBySmell() { }
public void hearSubtleNoises() { }
}
class DogBoy extends CanineHero<SuperHearSmell>{
DogBoy() { super(new SuperHearSmell()); }
}
public class EpicBattle {
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);
}
}
15.10 通配符
首先,我们观察一个示例:将导出类型的数组赋值给基类型的数组引用:
class Fruit{}
class Apple extends Fruit{}
class Jonathan extends Apple{}
class Orange extends Fruit{}
public class CovariantArrays {
public static void main(String[] args) {
Fruit[] fruit = new Apple[10];
fruit[0] = new Apple();
fruit[1] = new Jonathan();
try {
fruit[0] = new Fruit();
} catch (Exception e) {
System.out.println(e);
}
try {
fruit[0] = new Orange();
} catch (Exception e) {
System.out.println(e);
}
}
}
上例中,Apple是Fruit的子类,所以Apple数组通过向上转型为Fruit数组是可以的。但数组的实际类型为Apple,如果向数组中添加Fruit或其子类型,由于数组引用是Fruit类型,导致编译期是允许的,但是运行时的数组机制知道它处理的是Apple[],则会抛出异常。
数组是在运行是发现错误,但泛型的主要目标之一是将这种错误检测移入到编译期,若使用泛型容器代替数组:
public class NonCovariantGenerics {
// cannot convert from ArrayList<Apple> to List<Fruit>
// List<Fruit> flist = new ArrayList<Apple>();
}
上例在编译期会报错,与数组不同,泛型没有内建的协变类型。由于数组在语言中是完全定义的,所以内建了编译期和运行时的检查,而使用泛型时,编译器和运行时系统都不知道你想用类型做什么,以及应该采用何种规则。
若需要在两个类型之间建立向上转型的关系,子类型通配符则可以满足:
public class GenericsAndCovariance {
public static void main(String[] args) {
List<? extends Fruit> flist = new ArrayList<Apple>();
// can't add ant type of object
// flist.add(new Apple());
// flist.add(new Fruit());
// flist.add(new Orange());
flist.add(null);
Fruit fruit = flist.get(0);
}
}
flist类型现在是List<? extends Fruit>,可以理解为:任何继承Fruit的类型的列表。现在flist引用并没有指定具体类型,唯一的限制就是这个List可以持有某种具体的Fruit或其子类型。由于编译器并不知道声明的List是持有Apple的,List<? extends Fruit> 可以合法地指向任何一个持有Fruit的子类的List,如List< Orange >,所以容器将失去存放任何对象的能力,Object也不行。
但调用返回Fruit的方法,则是安全的。因为在这个List中的任何对象至少具有Fruit类型,编译器将允许这么做。
15.10.1 编译器有多聪明
通过上例,会觉得上述flist无法调用任何接收参数的方法,但事实并非如此:
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());
flist.indexOf(new Apple());
}
}
可以看到,contains()和indexOf()方法都接收Apple对象作为参数,并正常执行。
这并非意味着编译器实际上检查了代码,因为通过查看ArrayList文档,可以发现contains()和indexOf()方法都是接收Object类型的参数,而add()接收的是具有泛型参数类型的参数。 因此ArrayList<? extends Fruit>的add()方法的参数也就是 "? extends Fruit"了,从这个描述中,编译器无法知道需要的是Fruit的哪个具体子类型。所以它不会接收任何类型的Fruit。即编译器拒绝了所有对参数列表中涉及子类通配符的方法的调用(例如add())
在使用contains()和indexOf时,参数类型是Object,不涉及任何通配符,所以编译器将允许方法的调用。这意味着:泛型类的设计者可以决定哪些调用是安全的(即参数类型为Object类型),哪些调用是使用了子类通配符后禁止的(即在参数列表中使用类型参数):
public class Holder <T>{
private T value;
public Holder() { }
public Holder(T val) { value = val; }
public void set(T val) { value = val; }
public T get() { return value; }
public boolean equals(Object obj){
return value.equals(obj);
}
public static void main(String[] args) {
Holder<Apple> apple = new Holder<Apple>(new Apple());
Apple a = apple.get();
apple.set(a);
// Holder<Fruit> fruit = apple; cannot upcast
Holder<? extends Fruit> fruit = apple; //OK
Fruit f = fruit.get();
a = (Apple) fruit.get();
try{
Orange o = (Orange) fruit.get();
}catch(Exception e){
// fruit.set(new Apple()); cannot call set()
// fruit.set(new Fruit()); cannot call set()
System.out.println(fruit.equals(a));
}
}
}
如上例所示,Holder无法向上转型为Holder,但可以向上转型为Holder<? extends Fruit>。如果调用get(),编译器从"? extends Fruit"得到的信息只能返回Fruit,但你可以转型到具体的Fruit类型,但存在得到ClassCastException异常的风险。 由于set()方法参数为泛型,则编译器无法验证"? extends Fruit"的类型安全性。
equals()方法可以正常被调用,原因是,它接收的是Object类型而非T类型的参数。
因此,编译器只关注传递进来和要返回的对象类型,它并不会通过分析代码,以查看是否执行了任何实际的写入和读取操作。
15.10.2 逆变
当然,我们可以选择另一种通配符:超类型通配符(通配符是由某个特定类的任何基类来界定的,形式为:<? super MyClass>)。 这使得你可以安全地向泛型类中传递类型对象:
public class SuperTypeWildcards {
static void writeTo(List<? super Apple> apples){
apples.add(new Apple());
apples.add(new Jonathan());
// apples.add(new Fruit()); error
}
}
参数apples是Apple的某种基类的List,Apple则是泛型边界的下界,这样向其中添加Apple或其子类是安全的,但添加非Apple类型的对象则是不安全的,因其违反静态类型安全。
因此我们可以如何能够将一个泛型类型传递给方法,以及如何能够从方法中返回一个泛型类型,去思考子类型和超类型边界。
超类型边界放松了向方法传递参数上的限制:
public class GenericWriting {
static List<Apple> apples = new ArrayList<Apple>();
static List<Fruit> fruit = new ArrayList<Fruit>();
static <T> void writeExact(List<T> list,T item){
list.add(item);
}
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();
}
}
从上例中发现,当使用确切类型时,由于编译器无法得知持有类型与其他类型的关系,只能将持有的类型放入容器中,无法将其子类放入其中。
但使用超类型通配符后,List持有的是由T导出的任意类型。这样便可以安全地将一个T类型或其子类对象放置到容器中。
通过下面的例子,可以对子类通配符在返回泛型类型时有更清楚的认识:
public class GenericReading {
static List<Apple> apples = Arrays.asList(new Apple());
static List<Fruit> fruit = Arrays.asList(new Fruit());
static <T> T readExact(List<T> list){
return list.get(0);
}
static void f1(){
Apple a = readExact(apples);
Fruit f = readExact(fruit);
f = readExact(apples);
}
static class Reader<T>{
T readExact(List<T> list){
return list.get(0);
}
}
static void f2(){
Reader<Fruit> fruitReader = new Reader<Fruit>();
Fruit f = fruitReader.readExact(fruit);
// Fruit a = fruitReader.readExact(apples); error
}
static class CovariantReader<T>{
T readCovariant(List<? extends T> list){
return list.get(0);
}
}
static void f3(){
CovariantReader<Fruit> fruitReader = new CovariantReader<Fruit>();
Fruit f = fruitReader.readCovariant(fruit);
Fruit a = fruitReader.readCovariant(apples);
}
public static void main(String[] args) {
f1();
f2();
f3();
}
}
上例中:
-
readExact()方法使用精确类型,此时,向List中写入和读取的都是这个精确类型。对于返回值,该方法可以有效地根据泛型类型正确地返回。
-
Reader是一个泛型类,在创建这个类的实例时,要为这个类确定参数。虽然List< Apple >可以产生Fruit对象,但Reader类的实例不允许这么做。因为类中的readExact()方法的参数list所持有的类型(Fruit)也已经确定,并且只能从该list中读取这个确切类型的参数。
-
CovariantReader的readCovariant方法的参数为List<? extends T>,因此从这个列表中读取一个T是安全的(这个列表中所有的对象只能是T或其子类型)。
15.10.3 无界通配符
无界通配符<?>:任何事物,等价于原生类型:
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
}
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>());
List<?> wildList = new ArrayList();
wildList = new ArrayList<String>();
assign1(wildList);
assign2(wildList);
assign3(wildList);
}
}
上例中,<?>被认为是一种修饰,被用来声明这个代码是用Java泛型来编写的,并不是用原生类型,但在当前情况下,泛型参数可以是任何类型。
下面的例子展示了无界通配符的重要应用:当处理多个泛型参数时,允许一个参数是任意类型,同时赋予其他参数确切类型:
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)区分开。当只有一个参数是无界通配符时,其作用十分明显。
下面的示例展示了编译器何时才会关注原生类型和涉及通配符的类型之间的差异:
public class Wildcards {
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 o = holder.get();
}
static void unboundedArg(Holder<?> holder, Object arg) {
// holder.set(arg); Error
// 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 o = 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
T t = holder.get();
return t;
}
static <T> void wildSuperType(Holder<? super T> holder, T arg) {
holder.set(arg);
// T t = holder.get(); Error
// OK,but type information has been lost;
Object o = holder.get();
}
public static void main(String[] args) {
Holder raw = new Holder<Long>();
raw = new Holder();
Holder<Long> qualified = new Holder<Long>();
Holder<?> unbounded = new Holder<Long>();
Holder<? extends Long> bounded = new Holder<Long>();
Long lng = 1L;
rawArgs(raw, lng);
rawArgs(qualified, lng);
rawArgs(unbounded, lng);
rawArgs(bounded, lng);
unboundedArg(raw, lng);
unboundedArg(qualified, lng);
unboundedArg(unbounded, lng);
unboundedArg(bounded, lng);
// Object r1 = exact1(raw); Warning
Long r2 = exact1(qualified);
Object r3 = exact1(unbounded); // Must return Object
Long r4 = exact1(bounded);
// Long r5 = exact2(raw, lng); Warning
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);
// wildSubType(unbounded, lng); Error
Long r12 = wildSubType(bounded, lng);
// wildSuperType(raw, lng); Warning
wildSuperType(qualified, lng);
// wildSuperType(unbounded, lng); Error
// wildSuperType(bounded, lng); Error
}
}
通过观察上例,我们得出:
-
rawArgs()中,编译器知道Holder是一个泛型类型,即使在这里被表示成原生类型,编译器仍旧知道向Set()中传递一个Object是不安全的,因此会发出警告。你可以将任何类型的对象传递给Set(),而这个对象将被向上转型为Object。当你获取时,也只能获取Object类型。
-
unboundedArg()强调了无界通配符和原生类型的不同:原生Holder持有任何类型的组合,而Holder<?>持有的是具有某种具体类型的同构集合。在向set()中传递参数时,得到的将不是警告,而是错误。
-
exact1()和exact2()中使用的是确切的泛型参数(没有任何通配符),由于它们参数不一致,导致它们具有不同的限制:
-
exact1()接收一个具有确切类型的Holder:
- 如果传递一个原生Holder引用,则会因为在原生类型中得不到确切的类型而发出一个警告。
- 如果传递一个无界引用,就不会有任何可以确定返回类型的类型信息,只能得到Object类型。
-
exact2()接收一个具体确切类型的Holder,以及一个具有类型T的参数。除非提供确切参数,不然它将产生错误或警告。有时这样做很好,不过,当你觉得它过于受限,则可以使用通配符:
- 子类型通配符:从泛型参数中返回类型确定的返回值。
- 超类型通配符:向泛型参数中传递类型确定的参数。
-
-
wildSupType()中,Holder类型上的限制被放松为:持有继承自T类型的任意类型的对象。这意味着如果T是Fruit,那么holder可以是Holder< Apple >,为了防止将Orange放置到Holder< Apple >中:对set()或任何接收这个类型参数为参数的方法的调用都是不允许的。 由于任何来自Holder<? extends Fruit)的对象至少是Fruit类型:对get()或任何将产生具有这个类型参数的返回值的方法都是允许的。
-
wildSuperType()展示了超类型通配符:holder可以持有任何T的基类型。由于任何作用在基类上的行为都可以多态地作用在导出类,所以set()可以接收T类型。而holder持有的类型可以是任何超类型,那么get()则只能获取唯一安全的类型:Object。
因此,使用确切类型来替代通配符类型的好处是:可以用泛型参数来做更多的事,但是使用通配符使得你必须接收范围更宽的参数化类型作为参数。
15.10.4 捕获转换
有一种情况下,特别需要使用<?>而不是原生类型:如果向一个使用<?>的方法传递原生类型,对于编译器来说,可能会推断出其实际的类型参数,使得这个方法可以调用另一个使用确切类型的方法:
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);
}
public static void main(String[] args) {
Holder raw = new Holder<Integer>(1);
// f1(raw); Warning
f2(raw); //No warning
Holder rawBasic = new Holder();
rawBasic.set(new CaptureConversion()); // Warning
f2(rawBasic);
Holder<?> wildcarded = new Holder<Double>(1.0);
f2(wildcarded);
}
}
示例中演示了捕获转换:未指定的通配符类型被捕获,并被转换为确切类型。
示例中发生的是:参数类型在调用f2()的过程中被捕获,因此它可以在对f1()的调用中被作为参数使用。
15.11
15.11.1 任何基本类型都不能作为类型参数
不能将基本类型用作类型参数是Java泛型中发现的限制之一,解决之道则是使用基本类型包装器类以及Java SE5的自动包装机制:
public class ListOfInt {
public static void main(String[] args) {
List<Integer> list = new ArrayList<Integer>();
for (int i = 0; i < 5; i++)
list.add(i);
for (int i : list)
System.out.print(i + " ");
}
}
通常,这种方案能够成功地存储和读取int,有一些转换碰巧在发生的同时会对你屏蔽掉。但当性能出现问题时,就需要使用专门适配器基本类型的容器版本(如Org.apache.commons.collections.primitives)。
下面是另一种方式,它可以创建持有Byte的Set:
public class ByteSet {
Byte[] possibles = {1,2,3,4,5,6,7,8,9};
Set<Byte> mySet = new HashSet<Byte>(Arrays.asList(possibles));
}
自动包装机制解决了一些问题,但并不是解决了所有问题。下面的示例将展示自动包装机制不能应用于数组:
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;
}
}
public class PrimitiveGenericTest {
public static void main(String[] args) {
String[] strings = FArray.fill(new String[7], new RandomGenerator.String());
for (String s : strings)
System.out.println(s);
Integer[] integers = FArray.fill(new Integer[7], new RandomGenerator.Integer());
for (Integer i : integers)
System.out.println(i);
// error
// int[] b = FArray.fill(new int[7], new RandomGenerator.Integer());
}
}
15.11.2 实现参数化接口
一个类不能实现同一个泛型接口的两种变体:
interface Payable<T>{}
class Employee implements Payable<Employee>{}
//Error
//class Hourlt extends Employee implements Payable<Hourlt>{}
Hourlt不能编译,因为擦除会将 Payable< Employee >和Payable< Hourly >简化为相同的类Payable。这样,上面的代码意味着重复实现相同的接口两次,所以编译器报错。
在使用某些更基本的Java接口,例如Comparable时,你会更加深刻地了解这个问题。
15.11.3 转型和警告
使用带有泛型类型参数的转型或instanceof没有效果:
class FixedSizeStack<T> {
private int index = 0;
private Object[] storage;
public FixedSizeStack(int size) {
storage = new Object[size];
}
public void push(T item) {
storage[index++] = item;
}
@SuppressWarnings("unchecked")
public T pop() {
return (T) storage[--index];
}
}
public class GenericCast {
public static final int SIZE = 10;
public static void main(String[] args) {
FixedSizeStack<String> strings = new FixedSizeStack<String>(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++) {
System.out.print(strings.pop() + " ");
}
}
}
上例中,如果没有@SuppressWarnings注解,由于擦除的原因,编译器无法知道这个转型是否安全,会对pop()产生"unchecked cast"警告。T被擦除到它的第一个边界,默认情况下是Object,因此pop()实际上只是将Object转型为Object。
15.11.4 重载
下面的程序是不能编译的,即使它看似合理:
public class UseList<W,T> {
void f(List<T> v){}
void f(List<W> v){}
}
由于擦除的原因,重载方法将产生相同的参数类型。所以,当擦除的参数不能产生唯一的参数列表时,必须提供明显有区别的方法名:
public class UseList2<W,T> {
void f1(List<T> v){}
void f2(List<W> v){}
}
15.11.5 基类劫持了接口
假设有个类(ComparablePet)实现了Comparable接口,它可以与其他的ComparablePet对象进行比较:
public class ComparablePet implements Comparable<ComparablePet> {
public int compareTo(ComparablePet o) {
return 0;
}
}
将ComparablePet的子类比较的类型进行窄化也是有意义的,例如:一个Cat对象只能与其他的Cat对象进行比较:
//Error
class Cat extends ComparablePet implements Comparable<Cat>{
public int compareTo(ComparablePet o) {
return 0;
}
}
事实上,这无法工作。一旦为基类(ComparablePet)为接口(Comparable)确定了泛型类型,那么其它任何实现类都不能再为接口(Comparable)更改泛型类型:
class Hamster extends ComparablePet implements Comparable<ComparablePet> {
public int compareTo(ComparablePet o) {
return 0;
}
}
class Gecko extends ComparablePet{
public int compareTo(ComparablePet o) {
return 0;
}
}
Hamster说明了可以实现与基类实现相同的泛型接口是可以的,前提是它们泛型类型一致。 但是这和直接覆盖基类中的方法是一样的。
15.12 自限定的类型
在Java泛型中,有一个令人费解的写法:
class SelfBounded<T extends SelfBounded<T>>
SelfBounded类接收一个泛型参数T,而T有一个边界类限定,这个边界就是拥有T作为其参数的SelfBounded。
15.12.1 古怪的循环泛型
我们首先看一个简单版本的循环泛型,我们称其为:古怪的循环泛型(CRG)。一个类继承自一个泛型类型,这个泛型类型使用子类的类名作为泛型参数:
public class BasicHolder <T>{
T element;
void set(T arg){
element = arg;
}
T get(){
return element;
}
void f(){
System.out.println(element.getClass().getSimpleName());
}
}
class SubType extends BasicHolder<SubType> { }
public class CRGWithBasicHolder {
public static void main(String[] args) {
SubType st1 = new SubType();
SubType st2 = new SubType();
st1.set(st2);
SubType st3 = st1.get();
st1.f();
}
}
通过例子,我们发现,CRG能够产生使用导出类作为参数和返回类型的基类,并且将导出类型作为其域类型。即基类用导出类代替其参数。这意味着:泛型基类变成了其所有导出类的公共功能的模板,但这些功能的参数和返回值都将是导出类型。
15.12.2 自限定
BasicHolder可以使用任何类型作为其泛型参数,如:
class Other{}
class BasicOther extends BasicHolder<Other>{}
public class Unconstrained {
public static void main(String[] args) {
BasicOther b = new BasicOther();
b.set(new Other());
Other o = b.get();
b.f();
}
}
而自限定则有额外的限制:强制泛型参数为其的导出类型。
class SelfBounded<T extends SelfBounded<T>>{
T element;
SelfBounded<T> set(T arg) {
element = arg;
return this;
}
T get() { return element; }
}
class A extends SelfBounded<A>{}
class B extends SelfBounded<A>{} //Also Ok
class C extends SelfBounded<C>{
C setAndGet(C arg){
set(arg);
return get();
}
}
class D{}
//Error
//class E extends SelfBounded<D>{}
public class SelfBounding {
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());
}
}
强制将正在定义的类作为参数传递给基类的意义是:保证泛型类型参数与正在被定义的类的基类型一致。
注意,移除自限定的限制后,所有的类都将可以编译(包括E):
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 {}
class E2 extends NotSelfBounded<D2> {}
因此,自限定限制只能强制作用于继承关系。还可将自限定用于泛型方法:
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());
}
}
这可以防止这个方法被应用于除上述形式的自限定参数之外的任何事物上。
15.12.3 参数协变
自限定类型可以产生协变返回类型:与子类类型相同的返回类型。
interface GenericGetter <T extends GenericGetter<T>>{
T get();
}
interface Getter extends GenericGetter<Getter>{}
public class GenericAndReturnTypes{
void test(Getter g){
Getter result = g.get();
}
}
从上例中发现:自限定泛型事实上将产生确切的导出类型作为其返回值。
Java SE5也引入了协变返回类型:
class Base{}
class Derived extends Base{}
interface OrdinaryGetter{
Base get();
}
interface DerivedGetter extends OrdinaryGetter{
Derived get();
}
public class CovariantReturnTypes {
void test(DerivedGetter dg){
Derived derived = dg.get();
}
}
我们发现,在非泛型情况下,导出类中的方法在其返回值是更具体的类型时,可以覆盖基类中的方法。
但是,在非泛型情况下,方法中的参数类型则不能随子类型发生变化:
class OrdinarySetter {
void set(Base base) {
System.out.println("OrdinarySetter.set(Base)");
}
}
class DerivedSetter extends OrdinarySetter {
void set(Derived d) {
System.out.println("OrdinarySetter.set(Derived)");
}
}
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);
}
}
从上例的输出中可以看到,基类版本仍旧可用,因此方法是被重载了而不是覆盖。
但是,在使用自限定类型时,在导出类中只有一个方法,并且这个方法接收的参数是导出类型而不是基类型:
interface SelfBoundSetter<T extends SelfBoundSetter<T>> {
void set(T arg);
}
interface Setter extends SelfBoundSetter<Setter> {}
public class SelfBoundingAndCovariantArguments {
void testA(Setter s1, Setter s2, SelfBoundSetter sbs) {
s1.set(s2);
// s1.set(sbs); Error
}
}
编译器不能识别将基类型当作参数传递给set()的行为,事实上,这个参数已经被覆盖了。
如果不使用自限定类型,采用确切的泛型参数,则普通的继承机制就会介入,就像在非泛型的情况一样,方法将被重载:
class GenericSetter<T> {
void set(T arg) {
System.out.println("GenericSetter.set(Base)");
}
}
class DerivedGS extends GenericSetter<Base> {
void set(Derived derived) {
System.out.println("GenericSetter.set(Derived)");
}
}
public class PlainGenericInheritance {
public static void main(String[] args) {
Base base = new Base();
Derived derived = new Derived();
DerivedGS dgs = new DerivedGS();
dgs.set(base);
dgs.set(derived);
}
}
从输出可以看出:在使用泛型参数的情况下:如果不使用自限定,方法将会被重载,使用了自限定,方法则只接收确切的泛型参数类型。
15.13 动态类型安全
因为Java SE5之前的容器并未拥有泛型,泛型具有兼容性,则向Java SE5之前的代码传递泛型容器时,有可能破坏你的容器:
public class UnCheckedList {
@SuppressWarnings({ "unchecked", "rawtypes" })
static void oldStyleMethod(List probablyDogs) {
probablyDogs.add(new Cat());
}
public static void main(String[] args) {
List<Dog> dogs = new ArrayList<Dog>();
oldStyleMethod(dogs1);
Dog dog = dogs.get(0);
System.out.println(dog);
}
}
如上例所示,编译期并未产生任何警告与错误,但将对象从容器中取出时,会产生异常。
Java SE5的java.util.Collections中有一组便利工具,则可以解决这种情况下的类型检查问题,它们是静态方法:
- checkedCollection()
- checkedList()
- checkedMap()
- checkedSet()
- checkedSortedMap()
- checkedSortedSet()
这些方法的第一个参数是希望动态检查的容器,第二个参数是强制要求的类型。
受检查的容器会在试图插入类型不正确的对象时抛出ClassCastException:
public class CheckedList {
@SuppressWarnings({ "unchecked", "rawtypes" })
static void oldStyleMethod(List probablyDogs) {
probablyDogs.add(new Cat());
}
public static void main(String[] args) {
List<Dog> dogs = Collections.checkedList(new ArrayList<Dog>(), Dog.class);
try {
oldStyleMethod(dogs);
} catch (Exception e) {
System.out.println(e);
}
List<Pet> pets = Collections.checkedList(new ArrayList<Pet>(), Pet.class);
pets.add(new Dog());
pets.add(new Cat());
}
}
运行这个程序,你会发现在受检查的容器上(dogs)上插入一个错误类型(Cat)会抛出一个异常,并且将导出类型的对象(dog,cat)放置到将要检查基类型的受检查容器(pets)中是没有问题的。
15.14 异常
由于擦除的原因,将泛型应用于异常是十分受限的,因为在编译期和运行时都必须知道异常的确切类型。并且,泛型类也不能直接或间接继承自Throwable,这将使我们无法自定义泛型异常。
但是,泛型参数可以在throws子句中使用,这使得我们可以编写出随异常类型变化而变化的泛型代码:
interface Processor<T, E extends Exception> {
void process(List<T> resultCollection) throws E;
}
@SuppressWarnings("serial")
class ProcessRunner<T, E extends Exception> extends ArrayList<Processor<T, E>> {
List<T> processAll() throws E {
List<T> resultCollection = new ArrayList<T>();
for (Processor<T, E> processor : this) {
processor.process(resultCollection);
}
return resultCollection;
}
}
@SuppressWarnings("serial")
class Failure1 extends Exception {}
class Processor1 implements Processor<String, Failure1> {
static int count = 3;
public void process(List<String> resultCollection) throws Failure1 {
if (count-- > 1)
resultCollection.add("Hep!");
else
resultCollection.add("Ho!");
if (count < 0)
throw new Failure1();
}
}
@SuppressWarnings("serial")
class Failure2 extends Exception {}
class Processor2 implements Processor<Integer, Failure2> {
static int count = 2;
public void process(List<Integer> resultCollection) throws Failure2 {
if (count-- == 0)
resultCollection.add(47);
else
resultCollection.add(11);
if (count < 0)
throw new Failure2();
}
}
public class ThrowGenericException {
public static void main(String[] args) {
ProcessRunner<String, Failure1> runner = new ProcessRunner<String, Failure1>();
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<Integer, Failure2>();
for (int i = 0; i < 3; i++) {
runner2.add(new Processor2());
}
try {
System.out.println(runner2.processAll());
} catch (Failure2 e) {
System.out.println(e);
}
}
}
如果不能参数化所抛出的异常,将无法编写出这种泛化的代码。
15.15 混型
混型:混合多个类的能力,以产生一个可以表示混型中所有类型的类。如果想在混型类中修改某些东西,这些修改也将应用于混型所应用的所有类型上。
15.15.1 C++中的泛型
在C++中,使用多重继承的最大理由,就是为了使用混型。
C++可以很容易地通过泛型创建混型,因为C++能够记住其模板参数的类型。
下面是一个C++示例:
#include <string>
#include <ctime>
#include <iostream>
using namespace std;
template<class T> class TimeStamped : public T{
long timeStamp;
public:
TimeStamped(){ timeStamp = 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; }
};
template<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,mixin2;
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;
}
可以看出,mixin1和mixin2所产生的类型拥有所混入类型的所有方法。
遗憾的是,Java泛型并不允许这样。擦除会忘记基类类型,因此泛型类不能直接继承自一个泛型参数。
15.15.2 与接口混合
java中,一般常见的是使用接口来产生混型效果,就像下面这样:
interface TimeStamped { long getStamp(); }
class TimeStampedImp implements TimeStamped{
private final long timeStamp;
public TimeStampedImp(){ timeStamp = new Date().getTime(); }
public long getStamp() { return timeStamp; }
}
interface SerialNumbered { long getSerialNumber(); }
class SerialNumberedImp implements SerialNumbered{
private long serialNumber;
private static long counter = 1;
public SerialNumberedImp(){ serialNumber = counter++; }
public long getSerialNumber() { return serialNumber; }
}
interface Basic {
void set(String val);
String get();
}
class BasicImp implements Basic{
private String value;
public void set(String val){ value = val; }
public String get(){ return value; }
}
class Mixin extends BasicImp implements TimeStamped,SerialNumbered{
private TimeStamped timeStamp = new TimeStampedImp();
private SerialNumbered serialNumber = new SerialNumberedImp();
public long getSerialNumber() {
return serialNumber.getSerialNumber();
}
public long getStamp() {
return timeStamp.getStamp();
}
}
public class Mixins{
public static void main(String[] args) {
Mixin mixin1 = new Mixin();
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() );
}
}
Mixin类基本上是在使用代理,因此每个混入类型都要求在Mixin中有一个相应的域,而且Mixin中必须编写所有必需的方法,将方法调用发给恰当的对象。
15.15.3 使用装饰器模式
混型的概念似乎和装饰器设计模式有相似之处。装饰器经常用于满足各种可能的组合。和代理不同的是:代理直接对混入类型进行实例化,而装饰器会在构造函数处要求传入混入类型的实例。前者则会产生过多的类的实例,不合理。
前面的示例可以改写为使用装饰器:
class Basic {
private String value;
public void set(String val) { value = val; }
public String get() { return value; }
}
class Decorator extends Basic{
protected Basic basic;
public Decorator(Basic basic) { this.basic = basic; }
public void set(String val) { basic.set(val); }
public String get() { return basic.get(); }
}
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; }
}
public class Decoration {
public static void main(String[] args) {
TimeStamped t = new TimeStamped(new Basic());
System.out.println(t.getStamp());
SerialNumbered s = new SerialNumbered(new Basic());
System.out.println(s.getSerialNumber());
}
}
对于装饰器来说,其明显的缺陷是它只能有效地工作于装饰的那一层,而混型的类型是所有被混合到一起的类型。因此,装饰器只是混型的一种局限的解决方案。
15.15.4 与动态代理混合
可以使用动态代理来创建一种比装饰器更贴近混型模式的机制。通过使用动态代理,所产生的类的动态类型将会是已经混入的组合类型。
由于动态代理的限制,每个被混入的类都必须是某个接口的实现:
class MixinProxy implements InvocationHandler {
Map<String, Object> delegatesByMethod;
public MixinProxy(TwoTuple<Object, Class<?>>... pairs) {
delegatesByMethod = new HashMap<String, Object>();
for (TwoTuple<Object, Class<?>> pair : pairs) {
for (Method method : pair.second.getMethods()) {
String methodName = method.getName();
if (!delegatesByMethod.containsKey(methodName)) {
delegatesByMethod.put(methodName, pair.first);
}
}
}
}
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);
}
public static Object newInstance(TwoTuple... pairs) {
Class[] interfaces = new Class[pairs.length];
for (int i = 0; i < interfaces.length; i++) {
interfaces[i] = (Class) pairs[i].second;
}
ClassLoader c1 = pairs[0].first.getClass().getClassLoader();
return Proxy.newProxyInstance(c1, interfaces, new MixinProxy(pairs));
}
}
public class DynamicProxyMixin {
public static void main(String[] args) {
Object mixin = MixinProxy.newInstance(
Tuple.tuple(new BasicImp(), Basic.class),
Tuple.tuple(new TimeStampedImp(), TimeStamped.class),
Tuple.tuple(new SerialNumberedImp(), 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());
}
}
动态代理的方式可以包含所有的混入类型,缺陷是在具有这些类型的对象上调用方法之前,必须先将对象向下转型到恰当的类型。即使如此,它已经很接近于真正的混型。
15.16 潜在类型机制
Java泛型最重要的存在意义之一是:让我们编写出无需修改就可以应用于更多情况的代码,即更加泛化的代码。
在我们编写泛型代码时,会有很多情况:
-
当编写或使用的只是持有对象的泛型时,这些代码可以应用于任何类型。也就是说,当代码不关心它将要作用的类型时,这种代码就可以真正地应用到任何地方。
-
当需要在泛型类型上执行操作时,就会产生问题。由于擦除的原因,编译器无法得知泛型的具体类型,从而无法调用具体方法。这时,我们将用到泛型的边界,以安全地调用代码中泛型对象上的具体方法。这也是对泛化的一种明显的限制,它限制泛型类型,使得其必须继承自特定的类,或者实现特定的接口。
某些编程语言提供了一种解决方案称为潜在类型机制。
具有潜在类型机制的语言(如C++)只要求实现某个方法子集,而不是某个特定类或接口,从而可以产生更加泛化的代码:
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);
}
如上例所示,潜在类型机制使得代码可以横跨类继承结构,调用不属于某个公共接口的方法。潜在类型机制确保,在试图传递错误类型时,编译器将会报错。
由于Java中的泛型是后期加入的,导致Java并没有对这种特性的支持。所以,Java的泛型机制比支持潜在类型机制的语言缺乏泛化性。
如果试图用Java实现上例,那么就会被强制要求使用一个类或接口,并在边界表达式中指定它:
public interface Performs {
void speak();
void sit();
}
class PerformingDog extends Dog implements Performs{
public void speak() { System.out.println("Woof!"); }
public void sit() { System.out.println("Sitting"); }
public void reproduce() {}
}
class Robot implements Performs{
public void speak() { System.out.println("Click!"); }
public void sit() { System.out.println("Clank!"); }
public void oilChange() {}
}
class Communicate{
public static <T extends Performs> void perform(T performer){
performer.sit();
performer.speak();
}
}
public class DogsAndRobots {
public static void main(String[] args) {
PerformingDog d = new PerformingDog();
Robot r = new Robot();
Communicate.perform(d);
Communicate.perform(r);
}
}
但是,上例其实并不需要泛型来工作:
class CommunicateSimple {
static void perform(Performs performer) {
performer.speak();
performer.sit();
}
}
public class SimpleDogsAndRobots {
public static void main(String[] args) {
CommunicateSimple.perform(new PerformingDog());
CommunicateSimple.perform(new Robot());
}
}
所以,在某些情况下,我们最终可能会使用普通类或普通接口,使用多态的特性让代码工作。因为限定边界的泛型可能会和知道类或接口没有任何区别。
15.17 对缺乏潜在机制的补偿
尽管Java不支持潜在类型机制,但这并不意味着有界泛型代码不能在不同的类型层次结果之间应用。
15.17.1 反射
达到上述目的的一种方式就是反射:
class Mime {
public void walkAgainstTheWind() { }
public void sit() { System.out.println("Pretending to sit"); }
public void pushInvisibleWalls() { }
public String toString() { return "Mime"; }
}
class SmartDog {
public void speak() { System.out.println("Woof!"); }
public void sit() { System.out.println("Sitting!"); }
public void reproduce() { }
}
class CommunicateReflectivelt {
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);
}
}
}
public class LatentReflection {
public static void main(String[] args) {
CommunicateReflectivelt.perform(new SmartDog());
CommunicateReflectivelt.perform(new Robot());
CommunicateReflectivelt.perform(new Mime());
}
}
在上例中,通过反射CommunicateReflectivelt.perform()能够动态地确定所需要的方法是否可用并调用它们。
15.17.2 将一个方法应用于序列
反射为我们编写可复用代码提供了可能性,但是它将所有的类型检查都转移到了运行时。
所以我们尝试实现编译期检查和潜在类型机制。
我们尝试解决一个问题:创建一个apply()方法,它能够将任何方法应用于某个序列中的所有对象。
最初,我们使用反射解决这个问题:
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) {
throw new RuntimeException(e);
}
}
}
class Shape {
public void rotate() {
System.out.println(this + " rorate");
}
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++) {
add(type.newInstance());
}
} catch (Exception e) {
throw new RuntimeException(e);
}
}
}
public class SimpleQueue<T> implements Iterable<T> {
private LinkedList<T> storage = new LinkedList<T>();
public void add(T t) { storage.offer(t); }
public T get() { return storage.poll(); }
public Iterator<T> iterator() {
return storage.iterator();
}
}
在Apply中,碰巧Java中正好内建了一个由Java容器类库使用的Iterable接口,apply()方法则可以接收任何实现了Iterable接口的类,包括诸如List这样的所有Collection类,或自定义的实现了Iterable接口的类。
在FilledList中,我们使用了类型标记的方法去创建对象,缺陷是:我们在编译器无法得知该类型是否存在默认(无参)构造器,因此这就变成了一个运行时问题。解决方案在15.8.1中有描述,通过传递工厂接口代替类型标记:
interface Factory<T> {
T create();
}
class FilledList<T> extends ArrayList<T> {
public FilledList(Factory<? extends T> factory, int size) {
try {
for (int i = 0; i < size; i++) {
add(factory.create());
}
} catch (Exception e) {
throw new RuntimeException(e);
}
}
}
public class ApplyTest {
public static void main(String[] args) throws Exception {
Apply.apply(new FilledList<Shape>(new Factory<Shape>() {
public Shape create() {
return new Shape();
}
}, 10), Shape.class.getMethod("rotate"));
Apply.apply(new FilledList<Square>(new Factory<Square>() {
public Square create() {
return new Square();
}
}, 10), Shape.class.getMethod("rotate"));
}
}
15.17.3 当你并未碰巧拥有正确的接口时
上面的例子是不具有普遍性的,因为Iterable接口已经是内建的,并且它正好符合我们的需要。当不存在刚好适合需求的接口时,如:将FilledList更加泛化,让它接收一个序列,并使用Generator填充它。
当尝试使用Java编写时,由于没有前面示例中的Iterable接口那样的"Addable"便利接口,我们将无法做到在任何事物上调用add(),只能在任何Collection子类型上调用add():
class Fill {
public static <T> void fill(Collection<T> collection,
Class<? extends T> cls, int size) {
for (int i = 0; i < size; i++) {
try {
collection.add(cls.newInstance());
} catch (Exception e) {
throw new RuntimeException(e);
}
}
}
}
class Contract {
private static long counter = 0;
private final long id = counter++;
public String toString(){
return getClass().getName() + " " +id;
}
}
class TitleTransfer extends Contract{}
public class FillTest {
public static void main(String[] args) {
List<Contract> contracts = new ArrayList<Contract>();
Fill.fill(contracts, Contract.class, 3);
Fill.fill(contracts, TitleTransfer.class, 2);
for (Contract contract : contracts) {
System.out.println(contract);
}
SimpleQueue<Contract> contractQueue = new SimpleQueue<Contract>();
// Error
// Fill.fill(contractQueue, Contract.class, 5);
}
}
在上面的情况中,因为Java设计者没有预见到对"Addable"接口的需要,所以我们被限制在Collection继承层次结构之内。即便SipleQueue有一个add()方法,它也无法工作。
15.17.4 用适配器仿真潜在类型机制
潜在类型机制意味着:在调用方法时,不必关心该类型的具体类型,只要该类型具有这些方法即可。 实际上,潜在类型机制创建了一个包含所需方法的隐式接口。因此,我们手工编写了必需的接口,就能够解决问题。
从我们拥有的接口中编写代码来产生我们需要的接口是适配器设计模式的一个典型示例。我们可以使用适配器来适配已有的接口,以产生想要的接口。
interface Addable<T> {
void add(T t);
}
class Fill2 {
public static <T> void fill(Addable<T> addable,
Class<? extends T> cls, int size) {
for (int i = 0; i < size; i++) {
try {
addable.add(cls.newInstance());
} catch (Exception e) {
throw new RuntimeException(e);
}
}
}
public static <T> void fill(Addable<T> addable,
Generator<T> generator, int size) {
for (int i = 0; i < size; i++) {
addable.add(generator.next());
}
}
}
class AddableCollectionAdapter<T> implements Addable<T> {
private Collection<T> c;
public AddableCollectionAdapter(Collection<T> c) {
this.c = c;
}
public void add(T t) {
c.add(t);
}
}
class Adapter {
public static <T> Addable<T> collectionAdapter(Collection<T> c) {
return new AddableCollectionAdapter(c);
}
}
class AddableSimpleQueue<T> extends SimpleQueue<T> implements Addable<T> {
public void add(T item) {
super.add(item);
}
}
public class Fill2Test {
public static void main(String[] args) {
List<Coffee> carrier = new ArrayList<Coffee>();
Fill2.fill(new AddableCollectionAdapter(carrier), Coffee.class, 3);
Fill2.fill(Adapter.collectionAdapter(carrier), Latte.class, 2);
for (Coffee coffee : carrier) {
System.out.println(coffee);
}
System.out.println("---------------------");
AddableSimpleQueue<Coffee> coffeeQueue = new AddableSimpleQueue<Coffee>();
Fill2.fill(coffeeQueue, Mocha.class, 4);
Fill2.fill(coffeeQueue, Latte.class, 1);
for (Coffee coffee : coffeeQueue) {
System.out.println(coffee);
}
}
}
上例中,我们手工创建了Addable接口,和两个适配器:
-
AddableCollectionAdapter:可以工作于基类型Collection,Collection的任何实现都可以使用。通过存储Collection引用实现适配器。
-
AddableSimpleQueue:工作于具体类型的SimpleQueue,此时,可以通过继承来创建适配器。
两种情况下,Collection和SimpleQueue的适配器都实现了Addable接口,从而可以用于fill()中。
潜在类型机制的价值:通过移除这个额外的步骤(增加接口并适配),使得泛化代码更容易应用。
15.18
下例通过使用前面描述的适配器方式创建了真正泛化的代码:
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();
}
interface UnaryPredicate<T> {
boolean test(T x);
}
public class Functional {
public static <T> T reduce(Iterable<T> seq, Combiner<T> combiner) {
Iterator<T> it = seq.iterator();
T result = null;
if (it.hasNext()) {
result = it.next();
while (it.hasNext()) {
result = combiner.combine(result, it.next());
}
}
return result;
}
public static <T> Collector<T> forEach(Iterable<T> seq, Collector<T> func) {
for (T t : seq) {
func.function(t);
}
return func;
}
public static <R, T> List<R> transform(Iterable<T> seq, UnaryFunction<R, T> func) {
List<R> result = new ArrayList<R>();
for (T t : seq) {
result.add(func.function(t));
}
return result;
}
public static <T> List<T> filter(Iterable<T> seq, UnaryPredicate<T> pred) {
List<T> result = new ArrayList<T>();
for (T t : seq) {
if (pred.test(t)) {
result.add(t);
}
}
return result;
}
static class IntegerAdder implements Combiner<Integer> {
public Integer combine(Integer x, Integer y) {
return x + y;
}
}
static class IntegerSubtracter implements Combiner<Integer> {
public Integer combine(Integer x, Integer y) {
return x - y;
}
}
static class BigDecimalAdder implements Combiner<BigDecimal> {
public BigDecimal combine(BigDecimal x, BigDecimal y) {
return x.add(y);
}
}
static class BigIntegerAdder implements Combiner<BigInteger> {
public BigInteger combine(BigInteger x, BigInteger y) {
return x.add(y);
}
}
static class AtomicLongAdder implements Combiner<AtomicLong> {
public AtomicLong combine(AtomicLong x, AtomicLong y) {
return new AtomicLong(x.addAndGet(y.get()));
}
}
static class BigDecimalUlp implements UnaryFunction<BigDecimal, BigDecimal> {
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;
}
public boolean test(T x) {
return x.compareTo(bound) > 0;
}
}
static class MultiplyingIntegerCollector implements Collector<Integer> {
private Integer val = 1;
public Integer function(Integer x) {
val *= x;
return val;
}
public Integer result() {
return val;
}
}
public static void main(String[] args) {
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<Integer>(4)));
System.out.println(forEach(li, new MultiplyingIntegerCollector()).result());
System.out.println(forEach(filter(li, new GreaterThan<Integer>(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<BigDecimal>(new BigDecimal(3))));
List<BigInteger> lbi = new ArrayList<BigInteger>();
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);
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()));
}
}
15.19 总结:转型真的如此之糟吗?
使用泛型类型机制的最吸引人的地方,就是在使用容器类的地方,如各种List、Set、Map。在Java SE5之前,将一个对象放置到容器时,该对象会被向上转型为Object,因此会丢失类型信息,当想要从容器中取出该对象并操作时,必须将其向下转型为正确类型。
如果没有Java SE5的泛型版本的容器,放置容器和从容器中取出的都是Object类型,因此,我们很有可能将一个Dog放置到Cat的List中。但是,我们仍旧可以发现问题,因为,当你将Dog当作Cat处理时,会得到RuntimeException,不过是在运行期而不是编译期。
泛型的真正意义其实是:通过使用泛型编写出更加泛化的代码。
-
当编写泛化的持有类(Java的容器)时,我们可以很容易做到。
-
当编写能够操作泛型类型的泛化代码时,就需要额外的努力了。如:理解适配器设计模式的概念和实现。
public class MapPerformance { static List<test<map>> tests = new ArrayList<test<map>>(); static { tests.add(new Test<map>("put") { int test(Map map, TestParam p) { int loops = p.loops; int size = p.size; for (int i = 0; i < loops; i++) { map.clear(); for (int j = 0; j < size; j++) map.put(j,j); } return loops * size; } }); tests.add(new Test<map>("get") { int test(Map map, TestParam p) { int loops = p.loops; int span = p.size * 2; for (int i = 0; i < loops; i++) for (int j = 0; j < span; j++) map.get(j); return loops * span; } }); tests.add(new Test<map>("iterate") { int test(Map map, TestParam p) { int loops = p.loops * 10; for (int i = 0; i < loops; i++) { Iterator it = map.entrySet().iterator(); while(it.hasNext()) it.next(); } return loops * map.size(); } }); }
public static void main(String[] args) {
if(args.length > 0)
Tester.defaultParams = TestParam.array(args);
Tester.fieldWidth = 10;
Tester.run(new TreeMap<Integer,Integer>(), tests);
Tester.run(new HashMap<Integer,Integer>(), tests);
Tester.run(new LinkedHashMap<Integer,Integer>(), tests);
Tester.run(new IdentityHashMap<Integer,Integer>(), tests);
Tester.run(new WeakHashMap<Integer,Integer>(), tests);
Tester.run(new Hashtable<Integer,Integer>(), tests);
}
}