From Thinking in Java 4th Edition.
泛型实现了:参数化类型的概念,使代码可以应用于多种类型。“泛型”这个术语的意思是:“适用于许多许多的类型”。
如果你了解其他语言(例如: C++)中的参数化类型机制,你就会发现,有些以前能做到的事情,使用Java的泛型机制却无法做到。
Java中的泛型需要与C++进行一番比较。了解C++模板的某些方面,有助于你理解泛型的基础。同事,非常重要的一点是:你可以了解Java泛型的局限性是什么,以及为什么会有这些限制。最终是要让你理解,Java泛型的边界在哪里。
有许多原因促成了泛型的出现,而最引入注目的一个原因就是为了创造容器类。我们先来看看一个只能持有单个对象的类。当然了,这个类可以明确指定其持有的对象的类型:
class Automobile {} public class Holder1 { private Automobile a; public Holder1(Automobile a) { this.a = a; } Automobile get() { return a; } }
不过,这个类的可重用性具不怎么样了。它无法持有其他类型的任何对象。
在Java SE5之前,我们可以让这个类直接继承自Object类型的对象:
public class Holder2 { private Object a; public Holder2(Object a) { this.a = a; } public void set(Object a) { this.a = a; } public Object get() { return a; } public static void main(String[] args){ Holder2 h2 = new Holder2(new Automobile()); Automobile a = (Automobile)h2.get(); h2.set("Not an Automobile"); String s = (String)h2.get(); h2.set(1); // Autoboxes to Integer Integer x = (Integer)h2.get(); } }
这个例子中,只用了一个Holder2对象,却先后三次存储了不同类型的对象。
但通常情况,我们只希望容器来存储一种类型的对象。泛型的主要目的之一就是用来指定容器要持有什么类型的对象,而且由编译器来保证类型的正确性。
因此,与其使用Object,我们更喜欢暂时不指定类型,而是稍后再做决定具体使用什么类型。要达到这个目的,需要使用类型参数,用尖括号括住,放在类名后面。然后在使用这个类的时候,再用实际的类型替换此类型参数。下面的例子中,T就是类型参数:
public class Holder3<T> { private T a; public Holder3(T a) { this.a = a; } public void set(T a) { this.a = a; } public T get() { return a; } public static void main(String[] args) { Holder3<Automobile> h3 = new Holder3<Automobile>(new Automobile()); Automobile a = h3.get(); // No cast needed. //! h3.set("Not an automobile"); // Error //! h3.set(1); // Error } }
这就是Java泛型的核心概念:告诉编译器想使用什么类型,然后编译器帮你处理一切。
我们经常需要实现:仅一次方法调用返回多个对象。可是return语句只允许返回单个对象,因此解决办法就是创建一个对象,用它持有想要返回的多个对象。
这概念被称为元祖(tuple),它是将一组对象直接打包存储于其中的一个单一对象。这个容器对象允许读取其中的元素,但不允许向其中存放新的对象。(这个概念也被称作数据传送对象,或信使。)
package net.mindview.util.*; public class TwoTuple<A, B> { public final A first; public final B second; public TwoTuple(A a, B b) { first = a; second = b; } public String toString() { return "(" + first + ", " + second + ")"; } }
我们可以利用继承机制实现更长更远的元组:
//: net/mindview/util/ThreeTuple.java package net.mindview.util; public class ThreeTuple<A, B, C> extends TwoTuple<A, B> { public final C third; public ThreeTuple(A a, B b, C c) { super(a, b); third = c; } public String toString() { return "(" + first + ", " + second + ", " + third + ")"; } } //: net/mindview/util/FourTuple.java package net.mindview.util; public class FourTuple<A, B, C, D> extends ThreeTuple<A, B, C> { public final D fourth; public FourTuple(A a, B b, C c, D d) { super(a, b, c); fourth = d; } public String toString() { return "(" + first + ", " + second + ", " + third + ", " + fourth + ")"; } }
为了使用元组,你只需定义一个长度合适的元组,将其作为方法的返回值,然后在return语句中创建该元组,并返回即可:
import net.mindview.util.*; class Amphibian {} class Vehicle {} public class TupleTest { static TwoTuple<String, Integer> f() { // Autoboxing converts the int to Integer: return new TwoTuple<String, Integer>("hi", 47); } static ThreeTuple<Amphibian, String, Integer> g() { return new ThreeTuple<Amphibian, String, Integer>(new Amphibian(), "hi", 47); } public static void main(String[] args) { TwoTuple<String, Integer> ttsi = f(); System.out.println(ttsi); //! ttsi.first = "there"; // Compile error: final System.out.println(g()); } }
上面的表达式中,new表达式确实有点啰嗦,之后会利用泛型方法简化这样的表达式。
一个堆栈类,实现内部链式存储机制:
// A stack implemented with an internal linked structure. public class LinkedStack<T> { private static class Node<U> { U item; Node<U> next; Node() { item = null; next = null; } Node(U item, Node<U> next) { this.item = item; this.next = next; } boolean end() { return null == item && null = next; } } 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); } }
泛型接口
泛型也可用于接口,例如生成器(generator),这是一种专门负责创建对象的类。这是工厂方法设计模式的一种应用,只不过生成器无需额外信息就知道如何创建新对象。
// A generic interface. package net.mindview.util; public interface Generator<T> { T next(); }
方法next()返回类型是T。为了演示如何实现Generator接口,我们还需要一些别的类,例如:Coffee类层次结构如下:
Coffee.java:
//: generics/coffee/Coffee.java package generics.coffee; public class Coffee { private static long counter = 0; private final long id = counter++; public String toString() { return getClass().getSimpleName() + " " + id; } }
All kinds of coffee:
//: generics/coffee/Latte.java package generics.coffee; public class Latte extends Coffee {} //: generics/coffee/Mocha.java package generics.coffee; public class Mocha extends Coffee {} //: generics/coffee/Cappuccino.java package generics.coffee; public class Cappuccino extends Coffee {} //: generics/coffee/Americano.java package generics.coffee; public class Americano extends Coffee {} //: generics/coffee/Breve.java package generics.coffee; public class Breve extends Coffee {}
现在,我们可以编写一个类,实现Generator<Coffee>接口,它能够随机生成不同类型的Coffee对象:
// Generate different types of Coffee: package generics.coffee; import java.util.*; import net.mindview.util.*; public class CoffeeGenerator implements Generator<Coffee>, Iterable<Coffee> { private Class[] types = {Latte.class, Mocha.class, Cappuccino.class, Americano.class, Breve.class, }; private static Random rand = new Random(47); public CoffeeGenerator() {} // For iteration: private int size = 0; public CoffeeGenerator(int sz) { size = sz; } public Coffee next() { try { return (Coffee)types[rand.nextInt(types.length)].newInstance(); // Report programmer error at run time: } catch(Exception e) { throw new RuntimeException(e); } } class CoffeeIterator implements Iterator<Coffee> { int count = size; public boolean hasNext() { return count > 0; } public Coffee next() { count--; return CoffeeGenerator.this.next(); } public void remove() { // No implemented; throw new UnsupportedOperationException(); } } public Iterator<Coffee> iterator() { return new CoffeeIterator(); } public static void main(String[] args) { CoffeeGenerator gen = new CoffeeGenerator(); for(int i = 0; i < 5; ++i) System.out.println(gen.next()); for(Coffee c : new CoffeeGenerator(5)) System.out.println(c); } } /* Output: Americano 0 Latte 1 Americano 2 Mocha 3 Mocha 4 Breve 5 Americano 6 Latte 7 Cappuccino 8 Cappuccino 9 */
下面是Generator<T>接口的另一个实现,它负责生成Fibonacci数列:
// Generate a Fibonacci sequence. import net.mindview.util.*; public class Fibonacci implements Generator<Integer> { private int count = 0; public Integer next() { return fib(count++); } private int fib(int n) { if(n < 2) return 1; return fib(n - 2) + fib(n - 1); } public static void main(String[] args) { Fibonacci gen = new Fibonacci(); for(int i = 0; i < 18; ++i) System.out.print(gen.next() + " "); } } /* Output: 1 1 2 3 5 8 13 21 34 55 89 144 233 377 610 987 1597 2584 */
虽然Fibonacci里里外外用的都是int类型,但是其类型参数却是Integer。这个例子引出了Java泛型的一个局限性:基本类型无法作为类型参数。
如果要更进一步,可以编写一个实现了Iterable的Fibonacci生成器。我们的一个选择是重写一个类,令其实现Iterable接口。不过,你并不总是能够拥有源代码的控制权,我们可以选择另一个方法:创建一个适配器设计模式来实现所需的接口。
有很多种方法可以实现适配器,例如通过继承来创建适配器类:
// Adapt the Fibonacci class to make it Iterable. import java.util.*; public class IterableFibonacci extends Fibonacci implements Iterable<Integer> { private int n; public IterableFibonacci(int count) { n = count; } public Iterator<Integer> iterator() { return new Iterator<Integer>() { public boolean hasNext() { return n > 0; } public Integer next() { n--; return IterableFibonacci.this.next(); } public void remove() { // Not implemented throw new UnsupportedOperationException(); } }; } public static void main(String[] args) { for(int i : new IterableFibonacci(18)) System.out.print(i + " "); } } /* Output: 1 1 2 3 5 8 13 21 34 55 89 144 233 377 610 987 1597 2584 */
在for循环中使用IterableFibonacci,必须向IterableFibonacci的构造器提供一个边界值,然后hasNext()方法才能知道何时应该返回false。
泛型方法
同样可以在类中包含参数化的方法,而这个方法所在的类可以使泛型类,也可以不是。也就是说,是否拥有泛型方法,与其所在的类是否是泛型类没有关系。
泛型方法使得该方法能够独立于类而产生变化。
以下是一个基本指导原则:无论何时,如果你能够使用泛型方法,就应该尽量使用。也就是说,如果使用泛型方法可以取代将整个类泛型化,那么就应该只使用泛型方法,因为它可以使事情更清楚明白。
另外,对于一个static的方法,它是无法访问泛型类的类型参数。所以,如果static方法需要使用泛型能力,就必须使其成为泛型方法。
要定义泛型方法,只需要将泛型参数列表置于返回值之前:
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); } } /* Output: java.lang.String java.lang.Integer java.lang.Double java.lang.Float java.lang.Character GenericMethods */
GenericMethods并不是参数化的,尽管这个类和其内部的方法可以被同时参数化,但是这个例子中,只有方法f()拥有类型参数。
当使用泛型类时,必须在创建的时候指定类型参数的值,而使用泛型方法的时候,通常不必指明参数类型,因为编译器会为我们找出具体的类型,这被称为类型参数推断(type argument inference)。
如果调用f()时传入基本类型,自动打包机制就会介入其中,将基本类型的值包装为对应的对象。
人们对泛型有一个抱怨,使用泛型时需要向程序中加入更多的代码,例如要创建一个持有List的Map,就像下面这样:
Map<Person, List<? extends Pet>> petPeople = new HashMap<Person, List<? extends Pet>>();
编译器本来应该能够从泛型参数列表中的一个推断出另一个参数,可惜编译器还暂时做不到。然而,在泛型方法中,类型参数推断可以为我们简化一部分工作。例如,我们可以编写一个工具类,它包含各式各样的static方法,专门用来创建各种常用的容器对象:
// Utilities to simplify generic container creation // by using type argument inference. package net.mindview.util; 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>(); } // Examples: 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()方法演示了如何使用这个类,类型参数推断避免了重复的泛型参数列表。它同样可以用在之前的例子:
import typeinfo.pets.*; import java.util.*; import net.mindview.util.*; public class SimplerPets { public static void main(String[] args) { Map<Person, List<? extends Pet>> petPeople = New.map(); // Rest of the code is the same ... } }
类型推断只对赋值操作有效,其他时候并不起作用。
如果你将一个泛型方法调用的结果作为参数传递给另一个方法,这时编译器并不会执行类型推断。这种情况下,编译器认为:调用泛型方法后,其返回值被赋给一个Object类型的变量:
import typeinfo.pets.*; import java.util.*; public class LimitsOfInference { static void f(Map<Person, List<? extends Pet>> petPeople) {} public static void main(String[] args) { //! f(New.map()); // Does not compile. } }
显式地类型说明
要显式地指明类型,必须在点操作符与方法名之间插入尖括号,然后把类型置于尖括号内。如果是在定义该方法的类的内部,必须在点操作符之前使用this关键字,如果是使用static方法,必须在点操作符之前加上类名:
import typeinfo.pets; import java.util.*; import net.mindview.util.*; public class ExplicitTypeSpecification { static void f(Map<Person, List<Pet>> petPeople) {} public static void main(String[] args) { f(New.<Person, List<Pet>>map()); } }
泛型与可变参数列表可以很好地共存:
import java.util.*; public class GenericVarargs { public static <T> List<T> makeList(T... args) { List<T> result = new ArrayList<T>(); 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); } } /* Output: [A] [A, B, C] [A, B, C, D, E, F, G, H, I, J, K, L, M, N, O, P, Q, R, S, T, U, V, W, X, Y, Z] */
makeList()方法展现了与标准类库中java.util.Arrays.asList()方法相同的功能。
利用生成器,可以很方便地填充一个Collection,而泛型化这种操作是具有实际意义的:
// A utility to use with Generators. import generics.coffee.*; import java.util.*; import net.mindview.util.*; 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(int i : fnumbers) System.out.print(i + ", "); } } /* Output: Americano 0 Latte 1 Americano 2 Mocha 3 1, 1, 2, 3, 5, 8, 13, 21, 34, 55, 89, 144, */
请注意,fill()方法是如何透明地应用于Coffee和Integer的容器和生成器。
一个通用的Generator
下面的程序可以为任何类构造一个Generator,只要该类具有默认的构造器。为了减少类型声明,它提供了一个泛型方法,用以生成BasicGenerator:
// Automatically create a Generator, given a class // with a default (non-arg) constructor. public class BasicGenerator<T> implements Generator<T> { private Class<T> type; public BasicGenerator(Class<T> type) { this.type = type; } public T next() { try { // Assumes type is a public class: return type.newInstance(); } catch(Exception e) { throw new RuntimeException(e); } } // Produce a default generator given a type token: public static <T> Generator<T> create(Class<T> type) { return new BasicGenerator<T>(type); } }
这个类提供了一个基本实现,用以生成某个类的对象。这个类必须满足两个特点:
- 它必须声明为public。(因为BasicGenerator与要处理的类在不同的包中,所以该类必须声明为public,并且不只具有包内访问权。)
- 它必须具有默认构造器。(因为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:
import net.mindview.util.*; public class BasicGeneratorDemo { public static void main(String[] args) { Generator<CountedObject> gen = BasicGenerator.create(CountedObject.class); for(itn i = 0; i < 5; ++i) System.out.println(gen.next()); } }
有了参数推断,再加上static方法,我们可以重新编写之前看到的元组工具,使其成为更通用的工具类库。我们通过重载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 class TupleTest2 { static TwoTuple<String, Integer> f() { return tuple("hi", 47); } static TwoTuple f2() { return tuple("hi", 47); } static ThreeTuple<Amphibian, String, Integer> g() { return tuple(new Amphibian(), "hi", 47); } public static void main(String[] args) { TwoTuple<String, Integer> ttsi = f(); System.out.println(ttsi); System.out.println(f2()); } }
方法f()返回一个参数化了的TwoTuple对象,而f2()返回的是非参数化的TwoTuple对象。如果试图将f2()的返回值转型为参数化的TwoTuple,编译器就会报错。
一个Set实用工具
另一个示例,用Set来表达数学中的集合关系:
import java.util.*; 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; } }
下面的示例使用Sets.difference()打印出java.util包中各种Collection类与Map类之间的方法差异:
import java.lang.reflect.*; 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<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); // Don't show 'Object' methods System.out.println(comp); interfaces(superset); } public static void main(String[] args) { System.out.println("Collection: " + methodSet(Collection.class)); interfaces(Collection.class); difference(Set.class, Collection.class); difference(HashSet.class, Set.class); difference(LinkedHashSet.class, HashSet.class); difference(TreeSet.class, Set.class); difference(List.class, Collection.class); difference(ArrayList.class, List.class); } } /* Output: Collection: [add, addAll, clear, contains, containsAll, equals, hashCode, isEmpty, iterator, remove, removeAll, retainAll, size, toArray] Interfaces in Collection: [Iterable] Set extends Collection, adds: [] Interfaces in Set: [Collection] HashSet extends Set, adds: [] Interfaces in HashSet: [Set, Cloneable, Serializable] LinkedHashSet extends HashSet, adds: [] Interfaces in LinkedHashSet: [Set, Cloneable, Serializable] TreeSet extends Set, adds: [lower, last, higher, descendingIterator, subSet, pollLast, comparator, pollFirst, floor, headSet, ceiling, tailSet, first, descendingSet] Interfaces in TreeSet: [NavigableSet, Cloneable, Serializable] List extends Collection, adds: [get, set, listIterator, lastIndexOf, indexOf, subList] Interfaces in List: [Collection] ArrayList extends List, adds: [trimToSize, ensureCapacity] Interfaces in ArrayList: [List, RandomAccess, Cloneable, Serializable] */
泛型还可以应用于内部类以及匿名内部类:
// A very simple bank teller simulation. import java.util.*; import net.mindview.util.*; class Customer { private static long counter = 1; private final long id = counter++; private Customer() {} public String toString() { return "Customer " + id; } // A method to produce Generator objects: public static Generator<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; } // A single Generator object: 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); } } /* Output: Teller 3 serves Customer 1 Teller 2 serves Customer 2 Teller 3 serves Customer 3 Teller 1 serves Customer 4 Teller 1 serves Customer 5 Teller 3 serves Customer 6 Teller 1 serves Customer 7 Teller 2 serves Customer 8 Teller 3 serves Customer 9 Teller 3 serves Customer 10 Teller 2 serves Customer 11 Teller 4 serves Customer 12 Teller 2 serves Customer 13 Teller 1 serves Customer 14 Teller 1 serves Customer 15 */
Customer和Teller类都只有private的构造器,这可以强制你必须使用Generator对象。
Customer有一个generator()方法,每次执行它都会生成一个新的Generator<Customer>对象。Teller就只创建了一个public的generator对象。
构建复杂模型
下面示例构建一个零售店,它包含走廊、货架和商品:
// Building up a complex model using generic containers. import java.util.*; import net.mindview.util.*; class Product { private final int id; private String description; private double price; public Product(int IDnumber, String descr, double price) { id = IDnumber; description = descr; this.price = price; System.out.println(toString()); } public String toString() { return id + ": " + description + ", price: $" + price; } public void priceChange(double change) { price += change; } public static Generator<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) { 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 {} public class Store extends ArrayList<Aisle> { private ArrayList<CheckoutStand> checkouts = new ArrayList<CheckoutStand>(); private Office office = new Office(); public Store(int nAisles, int nShelves, int nProducts) { for(int i = 0; i < nAisles; ++i) add(new Aisle(nShelves, nProducts)); } public String toString() { StringBuilder result = new StringBuilder(); for(Aisle a : this) for(Shelf s : a) for(Product p : s) { result.append(p); result.append(" "); } return result.toString(); } public static void main(String[] args) { System.out.println(new Store(14, 5, 10)); } } /* Output: 258: Test, price: $400.99 861: Test, price: $160.99 868: Test, price: $417.99 207: Test, price: $268.99 551: Test, price: $114.99 278: Test, price: $804.99 520: Test, price: $554.99 140: Test, price: $530.99 */
其结果是许多层容器,但是它们是类型安全且可管理的。
擦除(Erase)的神秘之处
当你开始更深入地钻研泛型时,会发现有大量东西初看起来是没有意义的,例如尽管可以声明ArrayList.class,但是不能声明ArrayList<Integer>.class:
import java.util.*; 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); } } /* Output: true */
下面的示例是对整个谜题的补充:
import java.util.*; 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())); } } /* Output: [E] [K, V] [Q] [POSITION, MOMENTUM] */
在泛型代码内部,无法获得任何有关泛型参数类型的信息。因此,你可以知道诸如类型参数标示符和泛型类型边界这类的信息——你却无法知道用来创建某个特定实例的实际的类型参数。如果你是一个C++程序员,那么这个事实肯定会让你很沮丧。
Java泛型是使用擦除来实现的,这意味着当你在使用泛型时,任何具体的类型信息都被擦除了,你唯一知道的就是你在使用一个对象。
因此List<String>和List<Integer>在运行时事实上是相同的类型。
这两种形式都被擦除成“原生”类型,即List。
理解擦除以及应该如何处理它,是你在学习Java泛型时面临的最大障碍。
C++的方式:
#include <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(); } /* Output: HasF::f() */
有意思的地方是在obj上调用f(),它怎么知道f()方法是为参数类型T而存在的呢?当你实例化这个模板时,C++编译器将进行检查。因此在Manipulator<HasF>被实例化的这一刻,它看到HasF拥有一个方法f()。如果情况并非如此,编译器就会报错。
C++编写这种代码很简单,因为当模板被实例化时,模板代码知道其模板参数的类型。而Java泛型就不同了:
// {CompileTimeError} (won't compile) public class HasF { public void f() { System.out.println("HasF.f()"); } } class Manipulator<T> { private T obj; public Manipulator(T x) { obj = x; } //! Error: cannot find symbol: method f(); public void manipulate() { obj.f(); } } public class Manipulation { public static void main(String[] args) { HasF hf = new HasF(); Manipulator<HasF> manipulator = new Manipulator<HasF>(hf); manipulator.manipulate(); } }
由于有了擦除,Java编译器无法将manipulate()必须能够在obj上调用f()这一需求映射到HasF拥有f()这一事实上。为了调用f(),我们必须协助泛型类,给定泛型类的边界,以此告知编译器只能接受遵循这个边界的类型。这里重用了extends关键字。由于有了边界,下面的代码就可以编译了:
class Manipulator2<T extends HasF> { private T obj; public Manipulator2(T x) { obj = x; } public void manipulate() { obj.f(); } }
边界<T extends HasF>声明T必须具有类型HasF或者从HasF导出的类。
我们说泛型类型参数将擦除到它的第一个边界(它可能会有很多边界,稍后会看到)。
我们还提到了类型参数的擦除。 编译器实际上会把类型参数替换为它的擦除,就像上面的例子一样。T擦除到了HasF,就好像是在类的声明中用HasF替换了T一样。
无论何时,当你在编写泛型代码时,必须时刻提醒自己,你只是看起来好像拥有有关参数的类型信息而已。因此,如果你编写了:
class Foo<T> { T var; }
那么当你在创建Foo的实例时:
Foo<Cat> f = new Foo<Cat>();
class Foo中的代码好像应该知道现在工作于Cat上,以为类型T都被替换为了Cat。但事实并非如此。无论何时,你要提醒自己,它只是一个Object。
因为有了擦除,泛型最令人困惑的方面源自于这样一个事实,即可以表示没有任何意义的事物:
import java.lang.reflect.*; import java.util.*; 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> stringMaker = new ArrayMaker<String>(String.class); String[] stringArray = stringMaker.create(9); System.out.println(Arrays.toString(stringArray)); } } /* Output: [null, null, null, null, null, null, null, null, null] */
即使kind被存储为Class<T>,擦除也意味着它实际将被存为Class,没有任何参数。所以在创建数组时,Array.newInstance()实际上并未拥有kind所蕴含的类型信息。因此这不会产生具体的结果,所以必须转型。
注意在泛型中创建数组,使用Array.newInstance() 是推荐的方式。
如果创建一个容器:
import java.util.*; public class ListMaker<T> { List<T> create() { return new ArrayList<T>(); } public static void main(String[] args) { ListMaker<String> stringMaker = new ListMaker<String>(); List<String> stringList = stringMaker.create(); } }
尽管我们知道在create()内部的new ArrayList<T>中的<T>被移除了,但编译器不会给出任何警告。并且,如果将表达式改为new ArrayList(),编译器则会发出警告。
如果返回list之前,将某些对象放入其中,就像下面这样:
import java.util.*; public class FilledListMaker<T> { List<T> create(T t, int n) { List<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> stringMaker = new FilledListMaker<String>(); List<String> list = stringMaker.create("Hello", 4); System.out.println(list); } }
即使编译器无法知道有关create()中的T的任何信息,但是它仍旧可以在编译器保证你放置到result中的对象具有T类型,使其适合ArrayList<T>。
因为擦除在方法体中移除了类型信息,所以在运行时的问题就是边界:即对象进入和离开方法的地点。这些正是编译器在编译执行类型检查并插入转型代码的地点。
正如我们看到的,擦除丢失了在泛型代码中执行某些操作的能力,任何在运行时需要知道确切类型信息的操作都将无法工作:
public class Erased<T> { private final int SIZE = 100; public static void f(Object arg) { //! if(arg instanceof T) {} //! T var = new T(); //! T[] array = new T[SIZE]; T[] array = (T)new Object[SIZE]; // Unchecked warning } }
偶尔可以绕过这些问题来编程,但是有时必须通过引入类型标签来对擦除进行补偿。例如上面的示例中,instanceof的尝试失败了,因为其类型信息已经被擦除了。但如果引入了类型标签,就可以转而使用动态的isInstance():
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> ctt1 = new ClassTypeCapture<Building>(Building.class); System.out.println(ctt1.f(new Building())); System.out.println(ctt1.f(new House())); ClassTypeCapture<House> ctt2 = new ClassTypeCapture<House>(House.class); System.out.println(ctt2.f(new Building())); System.out.println(ctt2.f(new House())); } } /* Output: true true false true */
编译器将确保类型标签可以匹配泛型参数。
创建类型的实例
在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()来创建类型的新对象:
import static net.mindview.util.Print.*; 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); print("ClassAsFactory<Employee> succeeded"); try { ClassAsFactory<Integer> fi = new ClassAsFactory<Integer>(Integer.class); } catch(Exception e) { print("ClassAsFactory<Integer> failed"); } } } /* Output: ClassAsFactory<Employee> succeeded ClassAsFactory<Integer> failed */
这个可以编译,但是会因为ClassAsFactory<Integer>而失败,因为Integer根本没有任何默认的构造器。建议使用显示的工厂,并将限制其类型,使得只能接受实现了这个工厂的类:
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> { public Integer create() { return new Integer(0); } } class Widget { public static class Factory implements FactoryI<Widget> { public Widget create() { return new Widget(); } } } public class FactoryConstraint { public static void main(String[] args) { new Foo2<Integer>(new IntegerFactory()); new Foo2<Widget>(new Widget.Factory()); } }
注意,这确实只是传递Class<T>的一种变体。 两种方式都传递了工厂对象,Class<T>碰巧是内建的工厂对象,而上面的方式创建了一个显示的工厂对象,但是你却获得了编译期检查。
另一种方式是模板方法设计模式。下面的示例中,get()是模板方法,而create()是在子类中定义的,用来产生子类类型的对象:
abstract class GenericWithCreate<T> { final T element; GenericWithCreate() { 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(); } }
泛型数组
正如在Erased.java中所见,不能创建泛型数组。一般的解决方案是在任何想要创建泛型数组的地方使用ArrayList:
import java.util.*; 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); } }
这你你将获得数组的行为,以及由泛型提供的编译期的类型安全。
有时你仍然希望创建泛型类型的数组(例如,ArrayList内部使用的是数组) 。你可以按照编译器喜欢的方式来定义一个引用:
class Generic<T> {} public class ArrayOfGenericReference { static Generic<Integer>[] gia; }
编译器会接受这个程序,但永远都不能创建这个确切类型的数组。既然所有数组无论它们持有的类型如何,都具有相同的结构,那么看起来你应该能够创建一个Object数组,并将其转型为所希望的数组类型。但这种方式只能够通过编译,而不能够运行:
class Generic<T> {} public class ArrayOfGenericReference { static Generic<Integer>[] gia; } public class ArrayOfGeneric { static final int SIZE = 100; static Generic<Integer>[] gia; @SuppressWarnings("unchecked") public static void main(String[] args) { // Compiles: produces ClassCastException: //! gia = (Generic<Integer>[])new Object[SIZE]; // Runtime type is the raw (erased) type: gia = (Generic<Integer>[])new Generic[SIZE]; System.out.println(gia.getClass().getSimpleName()); gia[0] = new Generic<Integer>(); //! gia[1] = new Object(); // Compile-time error // Discovers type mismatch at compile time; //! gia[2] = new Generic<Double>(); } }
问题在于数组将跟踪它们的实际类型,而这个类型是在数组被创建时确定的。
另一方面,虽然gia已经被转型为Generic<Integer>[],但是这个信息只存在于编译期,在运行时,它仍旧是Object数组。
成功创建泛型数组的唯一方式就是创建一个被擦除类型的新数组,然后对其转型。
考虑一个泛型数组的包装器:
public class GenericArray<T> { private T[] array; @SuppressWarnings("unchecked") public GenericArray(int sz) { array = (T[])new Object[sz]; } public void put(int index, T item) { array[index] = item; } public T get(int index) { return array[index]; } // Method that exposes the underlying representation: public T[] rep() { return array; } public static void main(String[] args) { GenericArray<Integer> gai = new GenericArray<Integer>(10); // This causes a ClassCastException: //! Integer[] ia = gai.rep(); // This is ok. Object[] oa = gai.rep(); } }
与前面相同,我们并不能声明T[] array = new T[sz],因此我们创建了一个对象数组,然后将其转型。
rep()方法将返回T[],它在main()中将用于gai,因此应该是Integer[],但是如果调用它,并尝试将结果作为Integer[]引用来捕获,就会得到ClassCastException,这还是因为实际的运行时类型是Object[]。
因为有了擦除,数组的运行时类型就只能是Object[]。如果我们立即将其转型为T[],那么在编译期该数组的实际类型将会丢失,而编译器可能会错过某些潜在的错误检查。所以,最好在集合内部使用Object[],然后当你使用数组元素时,添加一个对T的转型:
public class GenericArray2<T> { private Object[] array; public GenericArray2(int sz) { array = new Object[sz]; } public void put(int index, T item) { array[index] = item; } @SuppressWarnings("unchecked") public T get(int index) { return (T)array[index]; } @SuppressWarnings("unchecked") public T[] rep() { return (T[])array; // Warning: unchecked cast } public static void main(String[] args) { GenericArray2<Integer> gai = new GenericArray2<Integer>(10); for(int i = 0; i < 10; ++i) gai.put(i, i); for(int i = 0; i < 10; ++i) System.out.println(gai.get(i) + " "); System.out.println(); try { Integer[] ia = gai.rep(); } catch(Exception e) { System.out.println(e); } } } /* Output: 0 1 2 3 4 5 6 7 8 9 java.lang.ClassCastException: [Ljava.lang.Object; cannot be cast to [Ljava.lang.Integer; */
这里调用rep()仍旧是不正确的。因此,没有任何方式可以推翻底层的数组类型,它只能是Object[]。在内部将array当做Object[]而不是T[]处理的优势是:我们不太可能忘记这个数组的运行时类型,从而意外地引入缺陷。
对于新代码,应该传递一个类型标记:
import java.lang.reflect.*; public class GenericArrayWithTypeToken<T> { private T[] array; @SuppressWarnings("unchecked") 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]; } // Expose the underlying representation public T[] rep() { return array; } public static void main(String[] args) { GenericArrayWithTypeToken<Integer>(Integer.class, 10); // This now works: Integer[] ia = gai.rep(); } }
类型标记Class<T>被传递到构造器中,以便从擦除中恢复,使得我们可以创建需要的实际类型的数组。一旦我们获得了实际类型,就可以返回它。该数组的运行时类型是确切的T[]。
遗憾的是在Java SE5标准类库中的源代码诸如:
public ArrayList(Collection c) { size = c.size(); elementData = (E[])new Object[size]; c.toArray(elementData); }
可以十分肯定标准类库会产生大量的警告。因此你查看类库源代码时,你不能认为它就是应该在自己的代码中遵循的示例。
边界
边界使得你可以在用于泛型的参数类型上设置限制条件。这不仅使得你可以强制规定泛型可以应用的类型,更重要的是你可以按照自己的边界类型来调用方法。
因为擦除移除了类型信息,所以,可以用无界泛型参数调用的方法只是那些可以用Object调用的方法。但是,如果能够将这个参数限制为某个类型子集,那么你就可以用这些类型子集来调用方法。
为了执行这种限制,Java泛型重用了extends关键字。你需要知道,extends关键字在泛型边界上下文环境中和在普通情况下所具有的意义是完全不同的。
interface HasColor { java.awt.Color getColor(); } class Colored<T extends HasColor> { T item; Colored(T item) { this.item = item; } T getItem() { return item; } // The bound allows you to call a method: java.awt.Color color() { return item.getColor(); } } class Dimension { public int x, y, z; } // This won't work -- class must be first, then interfaces: // class ColoredDimension<T extends HasColor & Dimension> {} // Multiple method: class ColoredDimension<T extends Dimension & HasColor> { T item; ColoredDimension(T item) { this.item = item; } T getItem() { return item; } java.awt.Color color() { return item.getColor(); } int getX() { return item.x; } int getY() { return item.y; } int getZ() { return item.z; } } interface Weight { int weight(); } // As with inheritance, you can have only one // concrete class but multiple interfaces; class Solid<T extends Dimension & HasColor & Weight> { T item; Solid(T item) { this.item = item; } T getItem() { return item; } java.awt.Color color() { return item.getColor(); } int getX() { return item.x; } int getY() { return item.y; } int getZ() { return item.z; } int weight() { return item.weight(); } } class Bounded extends Dimension implements HasColor, Weight { public java.awt.Color getColor() { return null; } public int weight() { return 0; } } public class BasicBounds { public static void main(String[] args) { Solid<Bounded> solid = new Solid<Bounded>(new Bounded()); solid.color(); solid.getY(); solid.weight(); } }
BasicBounds.java看上去可以通过继承消除冗余。
下面可以看到如何在继承的每个层次上添加边界限制:
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); } java.awt.Color color() { return 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 { Solid2(T item) { super(item); } int weight() { return 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一致。ColoredDimension2和Solid2进一步扩展了这个层次结构,并在每个层次结构上都添加了边界。现在这些方法被继承,因而不必在每个类中重复。
通配符被限制为单一边界:
public class EpicBattle { public static void main(String[] args) { /// You can do this: List<? extends SuperHearing> audioBoys; // But you can't do this: List<? extends SuperHearing & SuperSmell> dogBoys; } }
通配符
我们开始入手的示例要展示数组的一种特殊行为:可以向导出类型的数组赋予基类型的数组引用:
class Fruit {} class Apple extends Fruit {} class Jonathan extends Apple {} class Orange extends Fruit {} public class CovariantArrays { public static void main(String[] args) { Fruit[] fruit = new Apple[10]; fruit[0] = new Apple(); // OK fruit[1] = new Jonathan(); // OK // Runtime type is Apple[], not Fruit[] or Orange[]: try { // Compiler allows you to add Fruit: fruit[0] = new Fruit(); // ArrayStoreException } catch(Exception e) { System.out.println(e); } try { // Compiler allows you to add Oranges: fruit[0] = new Orange(); } catch(Exception e) { System.out.println(e); } } } /* Output: java.lang.ArrayStoreException: Fruit java.lang.ArrayStoreException: Orange */
运行时的数组机制知道它处理的是Apple[],因此会在向数组中放置异构类型时抛出异常。所以很明显,数组对象可以保留有关它们包含的对象类型的规则。就好像数组对它们持有的对象是有意识的,因此在编译期检查和运行时检查之间,你不能滥用它们。
泛型的主要目标之一就是讲这种错误检测移到编译期:
// {CompileTimeError} (Won't compile) import java.util.*; public class NonCovariantGenerics { // Compile Error: incompatible types: //! List<Fruit> flist = new ArrayList<Apple>(); }
真正的问题是:我们在谈论容器的类型,而不是容器持有的类型。如果想在两个类型之间建立某种类型的向上转型关系,这正是通配符所允许的:
import java.util.*; 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); // Legal but uninteresting // We know that it returns at least Fruit: Fruit f = flist.get(0); } }
flist类型现在是List<? extends Fruit>,你可以将其读作为“具有任何从Fruit继承的类型的列表”。但是,这并不意味着这个List将持有任何类型的Fruit。
现在你甚至不能向刚刚声明过将持有Apple对象的List中放置一个Apple对象了。
List<? extends Fruit>可以合法地指向一个List<Orange>。一旦执行这种类型的向上转型,你就将丢失掉向其中传递任何对象的能力,甚至是传递Object也不行。因为<? extends Fruit>表示的是Fruit的某个子类型,但这个子类型并没有被确定,这种不确定性导致无法使用add()。
现在你可能会猜想自己被阻止调用任何接受参数的方法,但是:
import java.util.*; public class CompilerIntelligence { public static void main(String[] args) { List<? extends Fruit> flist = Arrays.asList(new Apple()); Apple a = (Apple)flist.get(0); // No warning flist.contains(new Apple()); // Argument is 'Object' flist.indexOf(new Apple()); // Argument is 'Object' } }
- add()方法接受一个具有泛型参数类型的参数
- 但是contains()和indexOf()将接受Object类型的参数。
因此,当你指定一个ArrayList<? extends Fruit>时,add()的参数就变成了"? extends Fruit"。从 这个描述中,编译器并不能了解这里需要Fruit的哪个具体子类型,因此它不会接受任何类型的Fruit。
而在使用contains()和indexOf()时,参数类型是Object,因此不涉及任何通配符,而编译器也将允许这个调用。这意味着将有泛型类的设计者来决定哪些调用是“安全的”,并使用Object类型作为参数类型。
为了在类型中使用了通配符的情况下禁止这类调用,我们需要在参数列表中使用类型参数:
import java.util.*; public class SuperTypeWildcards { static void writeTo(List<? super Apple> apples) { apples.add(new Apple()); apples.add(new Jonathan()); // apples.add(new Fruit()); // Error } }
根据如何能够向一个泛型类型“写入”(传递给一个方法),以及如何能够从一个泛型类型中“读取”(从一个方法中返回),来着手思考子类型和超类型的边界:
import java.util.*; public class GenericWriting { static <T> void writeExact(List<T> list, T item) { list.add(item); } static List<Apple> apples = new ArrayList<Apple>(); static List<Fruit> fruit = new ArrayList<Fruit>(); static void f1() { writeExact(apples, new Apple()); //! writeExact(fruit, new Apple()); // Error; // Incompatible types: found Fruit, required Apple } static <T> void writeWithWildcards(List<? super T> list, T item) { list.add(item); } static void f2() { writeWithWildcards(apples, new Apple()); writeWithWildcards(fruit, new Apple()); } public static void main(String[] args) { f1(); f2(); } }
带有super超类型限定的通配符可以向泛型对易用写入,带有extends子类型限定的通配符可以向泛型对象读取。——《Core Java》
作为对协变和通配符的一个复习:
import java.util.*; 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>(); Fruit f = fruitReader.readExact(fruit); //! Fruit a = fruitReader.readExact(apples); // Error // readExact(List<Fruit>) cannot be // applied to (List<Apples>). } 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(); } }
无界通配符<?>看起来意味着“任何事物”,因此使用无界通配符好像等价于使用原生类型。很多情况下,<?>可以被认为是一种装饰,但是它仍旧是很有价值的。当你在处理多个泛型参数时,有时允许一个参数可以是任何类型,同时为其他参数确定某种特定类型的这种能力会显得很重要:
import java.util.*; 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 // Unchecked conversion. Found: HashMap // Required: Map<String, ?> assign1(new HashMap<String, Integer>()); assign2(new HashMap<String, Integer>()); assign3(new HashMap<String, Integer>()); } }
编译器何时才会关注“原生类型”和涉及“无界通配符”的类型之间的差异呢?
// Exploring the meaning of wildcards. public class Wildcards { // Raw argument: static void rawArgs(Holder holder, Object arg) { // holder.set(arg); // Warning // unchecked call to set(T) as a // member of the raw type Holder // holder.set(new Wildcards()); // Same warning // Can't do this; don't have any 'T': // T t = holder.get(); // Ok, but type information has been lost: Object obj = holder.get(); } // Similar to rawArgs(), but errors instead of warnings: static void unboundedArg(Holder<?> holder, Object arg) { // holder.set(arg); // Error // set(capture of ?) in Holder<capture of ?> // cannot be applied to (Object) // holder.set(new Wildcards()); // Same error // Can't do this; don't have any 'T': // T t = holder.get(); // Ok, but type information has been lost: Object obj = holder.get(); } static <T> 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 of ? extends T) in // Holder<capture of ? extends T> // 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: found Object, required T // Ok, but type information has been lost: Object obj = holder.get(); } }
人们很自然地考虑原生Holder和Holder<?> 是大致相同的事物。但是unboundedArg()强调它们是不同的——它揭示了相同的问题,但是它将这些问题作为错误而不是警告报告,因为原生Holder将持有任何类型的组合,而Holder<?>将持有某种具体类型的同构集合,因此不能只是向其传递Object。
捕获转换
有一种情况特别需要使用<?>而不是原生类型。如果向一个使用<?>的方法传递原生类型,那么对编译器来说,可能会推断出实际的类型参数,使得这个方法可以回转并调用另一个使用这个确切类型的方法。
下面的示例演示了这种技术,它被称为捕获转换,因为未指定的通配符类型被捕获,并被转换为确切类型:
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); Holder rawBasic = new Holder(); rawBasic.set(new Object()); // Warning f2(rawBasic); // No warnings // Upcast to Holder<?>, still figures it out: Holder<?> wildcarded = new Holder<Double>(1.0); f2(wildcarded); } } /* Output: Integer Object Double */