Java编程思想学习笔记(15)
泛型
一般的类和方法,只能使用具体的类型:要么是基本类型,要么是自定义的类。如果要编写可以应用千多种类型的代码,这种刻板的限制对代码的束缚就会很大。
为了解决这个问题,JDK 5中引入了一个新特性—泛型(generics),泛型提供了编译时类型安全监测机制,该机制允许程序员在编译时监测非法的类型。使用泛型机制编写的程序代码要比那些杂乱地使用Object变量,然后再进行强制类型转换的代码具有更好的安全性和可读性。泛型对于集合类尤其有用,例如,ArrayList就是一个无处不在的集合类。
泛型的本质是参数化类型,也就是所操作的数据类型被指定为一个参数。
简单泛型
有许多原因促成了泛型的出现,而最引人注目的一个原因,就是为了创造容器类。
例子:
public class Automobile {
}
public class Holder1 {
// 只能持有单个对象的类
private Automobile a;
public Holder1(Automobile a) { this.a = a; }
Automobile get() { return a; }
}
这个类的可重用性就不怎么样了,它无法持有其他类型的任何对象
在JavaSES之前,我们可以让这个类直接持有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可以存储任何类型的对象,在这个例子中,只用了一个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), 它是将一组对象直接打包存储于其中的一个单一对象 这个容器对象允许读取其中元素, 但是不允许向其中存放新的对象。(这个概念也称为数据传送对象, 或信使。)
通常, 元组可以具有任意长度,同时, 元组中的对象可以是任意不同的类型。 不过, 我们希望能够为每一个对象指明其类型,并且从容器中读取出来时,能够得到正确的类型。 要处理不同长度的问题,我们需要创建多个不同的元组。 下面的程序是一个2维元组,它能够持有两个对象:
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 + ")";
}
}
上面类的字段虽然没有声明private,但是final修饰后依旧保持了安全性。
可以利用继承机制实现长度更长的元组。 从下面的例子中可以看到, 增加类型参数是件很简单的事情:
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 + ")";
}
}
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 + ")";
}
}
public class FiveTuple<A,B,C,D,E>
extends FourTuple<A,B,C,D> {
public final E fifth;
public FiveTuple(A a, B b, C c, D d, E e) {
super(a, b, c, d);
fifth = e;
}
public String toString() {
return "(" + first + ", " + second + ", " +
third + ", " + fourth + ", " + fifth + ")";
}
}
为了使用元组,只需要定义一个长度合适的元组。
public class Amphibian {
}
public 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 FourTuple<Vehicle,Amphibian,String,Integer> h() {
return
new FourTuple<Vehicle,Amphibian,String,Integer>(
new Vehicle(), new Amphibian(), "hi", 47);
}
static FiveTuple<Vehicle,Amphibian,String,Integer,Double> k() {
return new
FiveTuple<Vehicle,Amphibian,String,Integer,Double>(new Vehicle(), new Amphibian(), "hi", 47, 11.1);
}
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());
System.out.println(h());
System.out.println(k());
}
}
通过ttsi.first= "there"语句的错误,我们可以看出,final声明确实能够保护public元素,在 对象被构造出来之后,声明为final的元素便不能被再赋予其他值了。
堆栈类
不用Linked.List,来实现自己的内部链式存储机制。
public class LinkedStack<T> {
// 内部类Node也是一个泛型,拥有自己的类型参数
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 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);
}
}
RandomList
假设我们锯要一个持有特定类型对象的列表,每次调用其上的 selectO方法时,它可以随机地选取一个元素。如果我们希望以此构建一个可以应用于各种类型 的对象的工具,就帣要使用泛型:
public class RandomList<T> {
private ArrayList<T> storage = new ArrayList<T>();
private Random rand = new Random(47);
public void add(T item) { storage.add(item); }
public T select() {
return storage.get(rand.nextInt(storage.size()));
}
public static void main(String[] args) {
RandomList<String> rs = new RandomList<String>();
for(String s: ("The quick brown fox jumped over " +
"the lazy brown dog").split(" "))
rs.add(s);
for(int i = 0; i < 11; i++)
System.out.print(rs.select() + " ");
}
}
泛型接口
泛型也可以应用千接口。例如生成器(generator),这是一种专门负责创建对象的类。
实际上,这是工厂方法设计模式的一种应用。不过,当使用生成器创建新的对象时,它不需要任何参数,而工厂方法一般锯要参数。也就是说,生成器无需额外的信息就知道如何创建新对象。
一个生成器只定义一个方法,该方法用以产生新的对象。在这里,就是nextO方法:
public interface Generator<T> {
// 方法next()的返回类型是参数化的T。接口使用泛型与类使用泛型没什么区别。
T next();
}
Coffee层次类:
public class Coffee {
private static long counter = 0;
private final long id = counter++;
public String toString() {
return getClass().getSimpleName() + " " + id;
}
}
public class Cappuccino extends Coffee{
}
public class Breve extends Coffee{
}
public class Americano extends Coffee{
}
public class Latte extends Coffee{
}
public class Mocha extends Coffee{
}
现在,我们可以编写一个类,实现Generator接口,它能够随机生成不同类型的 Coffee对象:
public class CoffeeGenerator implements Generator<Coffee>, Iterable<Coffee>{
private Class[] types = { Latte.class, Mocha.class,
Cappuccino.class, Americano.class, Breve.class, };
private static Random rand = new Random(47);
// 参数化的Generator接口确保nextO的返回值是参数的类型。CoffeeGenerator同时还实现了
// Iterable接口,所以它可以在循环语句中使用。不过,它还要一个“末端哨兵”来判断何时停止,
// 这正是第二个构造器的功能。
public CoffeeGenerator() {}
// For iteration:
private int size = 0;
public CoffeeGenerator(int sz) { size = sz; }
public Coffee next() {
try {
return (Coffee)
types[rand.nextInt(types.length)].newInstance();
// Report programmer errors at run time:
} catch(Exception e) {
throw new RuntimeException(e);
}
}
class CoffeeIterator implements Iterator<Coffee> {
int count = size;
public boolean hasNext() { return count > 0; }
public Coffee next() {
count--;
return CoffeeGenerator.this.next();
}
public void remove() { // Not implemented
throw new UnsupportedOperationException();
}
};
public Iterator<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);
}
}
下面的类是Generator接口的另一个实现,它负贲生成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() + " ");
}
}
如果还想更进一步,编写一个实现了Iterable的Fibonacci生成器。
我们的一个选择是重写这个类,令其实现Iterable接口。不过,你并不是总能拥有源代码的控制权,并且,除非必须这么做,否则,我们也不愿意重写一个类。而且我们还有另一种选择,就是创建一个适配器(adapter) 来实现所需的接口
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();
}
};
}
}
泛型方法
到目前为止,我们看到的泛型,都是应用在整个类上。但同样可以在类中包含参数化方法, 而这个方法所在的类可以是泛型类,也可以不是泛型类。
也就是说,是否拥有泛型方法,与其所在的类是否是泛型没有关系。
泛型方法使得该方法能够独立于类而产生变化。以下是一个基本的指导原则:无论何时,只要你能做到,你就应该尽址使用泛型方法。也就是说,如果使用泛型方法可以取代将整个类 泛型化,那么就应该只使用泛型方法,因为它可以使事情更清楚明白。
另外,对于一个static的方法而言,无法访间泛型类的类型参数,所以,如果static方法需要使用泛型能力,就必须使其成为泛型方法。
public class GenericMethods {
public <T> void f(T x) {
System.out.println(x.getClass().getName());
}
// 注意,当使用泛型类时,必须在创建对象的时候指定类型参数的值,而使用泛型方法的时候,
// 通常不必指明参数类型,因为编译器会为我们找出具体的类型。
// 这称为类型参数推断(type argument inference)。因此,我们可以像调用普通方法一样调用f(),
// 而且就好像是ro被无限次地重载过。它甚至可以接受GenericMethods作为其类型参数。
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);
}
}
类型参数推断
使用泛型有时候需要向程序中加人更多的代码,如果要创建一个持有List的Map, 就要像下面这样:
Map<Person, List<? extends Pet>> petPeople =
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>();
}
// 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();
}
}
类型推断只对赋值操作有效,其他时候并不起作用。如果你将一个泛型方法调用的结果
(例如Ncw.mapO)作为参数,传递给另一个方法,这时编译器并不会执行类型推断。在这种情
况下,编译器认为:调用泛型方法后,其返回值被赋给一个Object类型的变扯。下面的例子证明了这一点:
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的方法,必须在点操作符之前加 上类名。使用这种语法,可以解决LimitsOflnference.java中的问题
public class ExplicitTypeSpecification {
static void f(Map<Person, List<Pet>> petPeople) {}
public static void main(String[] args) {
f(New.<Person, List<Pet>>map());
}
}
当然,这种语法抵消了New类为我们带来的好处(即省去了大证的类型说明),不过,只有在编写非赋值语旬时,我们才需要这样的额外说明。
可变参数与泛型方法
泛型方法与可变参数列表能很好共存
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("ABCDEFFHIJKLMNOPQRSTUVWXYZ".split(""));
System.out.println(ls);
}
}
用千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);
System.out.println("_______________________________________________");
Collection<Integer> fnumbers = fill(
new ArrayList<Integer>(), new Fibonacci(), 12);
for(int i : fnumbers)
System.out.print(i + ", ");
}
}
一个通用的Generator
下面的程序可以为任何类构造一个Generator, 只要该类具有默认的构造器。 为了减少类型
声明, 它提供了一个泛型方法, 用以生成BasicGenerator:
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 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());
}
}
简化元组的使用
有了类型参数推断, 再加上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 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);
}
public static <A,B,C,D,E>
FiveTuple<A,B,C,D,E> tuple(A a, B b, C c, D d, E e) {
return new FiveTuple<A,B,C,D,E>(a, b, c, d, e);
}
}
下面是修改后的TupleTest.java, 用来测试Tuple.java:
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);
}
static FourTuple<Vehicle,Amphibian,String,Integer> h() {
return tuple(new Vehicle(), new Amphibian(), "hi", 47);
}
static FiveTuple<Vehicle,Amphibian,String,Integer,Double> k() {
return tuple(new Vehicle(), new Amphibian(),
"hi", 47, 11.1);
}
public static void main(String[] args) {
TwoTuple<String,Integer> ttsi = f();
System.out.println(ttsi);
System.out.println(f2());
System.out.println(g());
System.out.println(h());
System.out.println(k());
}
}
一个Set实用工具
作为泛型方法的另一个示例,我们看看如何用Set来表达数学中的关系式。通过使用泛型方法,可以很方便地做到这一点,而且可以应用千多种类型:
public class Sets {
// 在前三个方法中,都将第一个参数Set复制了一份,将Set中的所有引用都存入一个新的
// HashSet对象中,因此,我们并未直接修改参数中的Set。返回的值是一个全新的Set对象。
//这四个方法表达了如下的数学集合操作:union()返回一个Set,它将两个参数合并在一起,
// intersection()返回的Set只包含两个参数共有的部分,difference()
// 方法从superset中移除subset包含的元素1complement()返回的Set包含除了交集之外的所有元素。
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;
}
// Subtract subset from superset:
public static <T> Set<T>
difference(Set<T> superset, Set<T> subset) {
Set<T> result = new HashSet<T>(superset);
result.removeAll(subset);
return result;
}
// Reflexive--everything not in the intersection:
public static <T> Set<T> complement(Set<T> a, Set<T> b) {
return difference(union(a, b), intersection(a, b));
}
}
下面提供了一个enum,它包含各种水彩画的颜色。我们将用它来演示以上这些方法的功能和效果。
public enum Watercolors {
ZINC, LEMON_YELLOW, MEDIUM_YELLOW, DEEP_YELLOW, ORANGE,
BRILLIANT_RED, CRIMSON, MAGENTA, ROSE_MADDER, VIOLET,
CERULEAN_BLUE_HUE, PHTHALO_BLUE, ULTRAMARINE,
COBALT_BLUE_HUE, PERMANENT_GREEN, VIRIDIAN_HUE,
SAP_GREEN, YELLOW_OCHRE, BURNT_SIENNA, RAW_UMBER,
BURNT_UMBER, PAYNES_GRAY, IVORY_BLACK
}
下面的示例以static的方式引人Watercolors。
这个示例使用了EnumSet,这是JavaSE5中的新工具,用来从enum直接创建Set。
在这里,我们向static方法EnumSet.range()传入某个范围的第一个元素与最后一个元素,然后它将返回一个Set,其中包含该范围内的所有元素:
public class WatercolorSets {
public static void main(String[] args) {
Set<Watercolors> set1 =
EnumSet.range(BRILLIANT_RED, VIRIDIAN_HUE);
Set<Watercolors> set2 =
EnumSet.range(CERULEAN_BLUE_HUE, BURNT_UMBER);
print("set1: " + set1);
print("set2: " + set2);
print("union(set1, set2): " + union(set1, set2));
Set<Watercolors> subset = intersection(set1, set2);
print("intersection(set1, set2): " + subset);
print("difference(set1, subset): " +
difference(set1, subset));
print("difference(set2, subset): " +
difference(set2, subset));
print("complement(set1, set2): " +
complement(set1, set2));
}
}
下面的示例使用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); // Don’t show ‘Object’ methods
System.out.println(comp);
interfaces(superset);
}
public static void main(String[] args) {
System.out.println("Collection: " +
methodSet(Collection.class));
interfaces(Collection.class);
difference(Set.class, Collection.class);
difference(HashSet.class, Set.class);
difference(LinkedHashSet.class, HashSet.class);
difference(TreeSet.class, Set.class);
difference(List.class, Collection.class);
difference(ArrayList.class, List.class);
difference(LinkedList.class, List.class);
difference(Queue.class, Collection.class);
difference(PriorityQueue.class, Queue.class);
System.out.println("Map: " + methodSet(Map.class));
difference(HashMap.class, Map.class);
difference(LinkedHashMap.class, HashMap.class);
difference(SortedMap.class, Map.class);
difference(TreeMap.class, Map.class);
}
}
匿名内部类
泛型还可以应用千内部类以及匿名内部类。下面的示例使用匿名内部类实现了Generator 接口
public 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(); }
};
}
}
public 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);
}
// Customer和Teller类都只有private的构造器,这可以强制你必须使用Generator对象。
// Customer有一个generator()方法,每次执行它都会生成一个新的Generator<Customer>对象。
// 我们其实不蒂要多个Generator对象,Teller就只创建了一个public的generator对象。
// 在main()方法中可以看到,这两种创建Generator的方式都在fill()中用到了。
//由于Customer中的generator()方法,以及Teller中的Generator对象都声明成了static的,
// 所以它们无法作为接口的一部分,因此无法用接口这种特定的惯用法来泛化这二者。尽管如此,
// 它们在血O方法中都工作得很好。
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);
}
}
构建复杂模型
泛型的一个重要好处是能够简单而安全地创建复杂的模型。例如,我们可以很容易地创建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> i: tl)
System.out.println(i);
}
}
下面是另一个示例,它展示了使用泛型类型来构建复杂模型是多么的简单。即使每个类都是作为一个构建块创建的,但是其整个还是包含许多部分。在本例中,构建的模型是一个零售店,它包含走廊、货架和商品:
public class Aisle extends ArrayList<Shelf> {
public Aisle(int nShelves, int nProducts) {
for(int i = 0; i < nShelves; i++)
add(new Shelf(nProducts));
}
}
public class CheckoutStand {
}
public class Office {
}
public class Shelf extends ArrayList<Product> {
public Shelf(int nProducts) {
Generators.fill(this, Product.generator, nProducts);
}
}
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("\n");
}
return result.toString();
}
public static void main(String[] args) {
System.out.println(new Store(14, 5, 10));
}
}
public 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);
}
};
}
擦除的神秘之处
当你开始更深入地钻研泛型时, 会发现有大量的东西初看起来是没有意义的。 例如, 尽管 可以声明ArrayList.class, 但是不能声明ArrayList< lnteger >.class。 请考虑下面的情况:
public class ErasedTypeEquivalence {
public static void main(String[] args) {
Class c1 = new ArrayList<String>().getClass();
Class c2 = new ArrayList<Integer>().getClass();
System.out.println(c1 == c2);
}
// ArrayList<.Strin贮和ArrayList<lnteger>很容易被认为是不同的类型。 不同的类型在行为方面肯定不同, 例如, 如果尝试若将一个Integer放入ArrayList<.String>, 所得到的行为(将失败)
//与把一个Integer放入ArrayList<lnteger> (将成功)所得到的行为完全不同。 但是上面的程序会
//认为它们是相同的类型。
}
下面是的示例是对这个谜题的一个补充:
public class Frob {
}
public class Fnorkle {
}
public class Particle<POSITION,MOMENTUM> {
}
public class Quark<Q> {
}
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()));
}
}
根据JDK文档的描述, Class.getTypeParameters()将 “返回一个TypeVariable对象数组,表示有泛型声明所声明的类型参数…···"这好像是在暗示你可能发现参数类型的信息, 但是,正如你从输出中所看到的, 你能够发现的只是用作参数占位符的标识符, 这并非有用的信息。
因此, 残酷的现实是:在泛型代码内部, 无法荻得任何有关泛型参数类型的信息。
因此, 你可以知道诸如类型参数标识符和泛型类型边界这类的信息—一你却无法知道用来创建某个特定实例的实际的类型参数。
Java泛型是使用擦除来实现的, 这意味若当你在使用泛型时, 任何具体的类型信息都被擦除了, 你唯一知道的就是你在使用一个对象。 因此List< String >和List< lnteger >在运行时事实上 是相同的类型。 这两种形式都被擦除成它们的 “原生” 类型, 即List
擦除的问题
掠除的代价是显著的。泛型不能用千显式地引用运行时类型的操作之中,例如转型
、 instanceo瑁作和new表达式。因为所有关千参数的类型信息都丢失了,无论何时,当你在编写 泛型代码时,必须时刻提醒自己,你只是看起来好像拥有有关参数的类型信息而已。因此,如果你编写了下面这样的代码段:
public class Foo<T> {
T var;
}
那么,看起来当你在创建Foo的实例时:
Foo<Cat> f = new Foo<Cat>();
class Foo中的代码应该知道现在工作千Cat之上,而泛型语法也在强烈暗示:在整个类中的各个地方,类型T都在被替换。但是事实井非如此,无论何时,当你在编写这个类的代时,必须知道它只是一个object
正是因为有了擦除,我发现泛型最令人困惑的方面源自这样一个事实,即可以表示没有任何意义的辜物。例如:
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));
}
// 即使kind被存储为Class<f>, 擦除也意味若它实际将被存储为Class,
// 没有任何参数。 因此,当你在使用它时,例如在创建数组时,
// Array.newlnstanceO实际上并未拥有kind所蕴含的类型倌
}
如果我们要创建一个容器而不是数组, 情况就有些不同了:
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()内部的newArrayList 一中的被移除了 在运行时,这个类的内部没有任何, 因此这看起来亳无意义。 但是如果你遵从这种思路,并将这个表达式改为new Array List(), 编译器就会给出警告。
如果返回list之前, 将某些对象放入其中, 就像下面这样,情况又会如何呢?
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) {} // Error
// T var = new T(); // Error
// T[] array = new T[SIZE]; // Error
// T[] array = (T)new Object[SIZE]; // Unchecked warning
// }
//
}
偶尔可以绕过这些问题来编程,但是有时必须通过引人类型标签来对擦除进行补偿。这意味着你蒂要显式地传递你的类型的Class对象,以便你可以在类型表达式中使用它。
public class Building {
}
public 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()));
}
}
创建类型实例
在Erased.java中对创建一个newTO的尝试将无法实现,部分原因是因为擦除,而另一部分
原因是因为编译器不能验证T具有默认(无参)构造器。但是在C++中, 这种操作很自然观,井且很安全(它是在编译期受到检查的)
Java中的解决方案是传递一个工厂对象,井使用它来创建新的实例。 最便利的工广对象就是 Class对象, 因此如果使用类型标签, 那么你就可以使用newInstance()来创建这个类型的新对象:
public class ClassAsFactory<T> {
T x;
public ClassAsFactory(Class<T> kind) {
try {
x = kind.newInstance();
} catch(Exception e) {
throw new RuntimeException(e);
}
}
}
public 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");
}
}
}
这可以编译,但是会因ClassAsFactory< lnteger >而失败,因为Integer没有任何默认的构造器。
public interface FactoryI<T> {
T create();
}
public class Foo2<T> {
private T x;
public <F extends FactoryI<T>> Foo2(F factory) {
x = factory.create();
}
//
}
public class IntegerFactory implements FactoryI<Integer>{
public Integer create() {
return new Integer(0);
}
}
public 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());
}
}
泛型数组
在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); }
}
有时,你仍旧希望创建泛型类型的数组(例如,ArrayList内部使用的是数组)。有趣的是, 可以按照编译器喜欢的方式来定义一个引用,例如:
public class ArrayOfGenericReference {
static Generic<Integer>[] gia;
}
编译器将接受这个程序,而不会产生任何警告。但是,永远都不能创建这个确切类型的数 组(包括类型参数),.因此这有一点令人困惑。既然所有数组无论它们持有的类型如何,都具有相同的结构(每个数组槽位的尺寸和数组的布局),那么看起来你应该能够创建一个Objec数组,并将其转型为所希望的数组类型。事实上这可以编译,但是不能运行,它将产生ClassCaseException:
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< lnteger >[], 但是这个信息只存在千编译期(并且如果没有@Suppress Warnings注解, 你将得到有关这个转型的警告)。 在运行时, 它仍旧是Objec岱组, 而这将引发间题。 成功创建泛型数组的唯方式就是创建一个被擦除类型的新数组, 然后对其转型。
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□。如果在注释掉@SuppressWarnings注解之后再编译GenericArray.java, 编译器就会产生 警告:
因为有了掠除,数组的运行时类型就只能是Object□。如果我们立即将其转型为T[ ],那么在编译期该数组的实际类型就将丢失,而编译器可能会错过某些潜在的错误检查。正因为这样,最好是在集合内部使用Object[],然后当你使用数组元素时,添加一个对T的转型。让我们看看这是如何作用于GenericArray.java示例的:
public class GenericArray2<T> {
private Object[] array;
public GenericArray2(int sz) {
array = new Object[sz];
}
public void put(int index, T item) {
array[index] = item;
}
@SuppressWarnings("unchecked")
public T get(int index) { return (T)array[index]; }
@SuppressWarnings("unchecked")
public T[] rep() {
return (T[])array; // Warning: unchecked cast
}
public static void main(String[] args) {
GenericArray2<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.print(gai.get(i) + " ");
System.out.println();
try {
Integer[] ia = gai.rep();
} catch(Exception e) { System.out.println(e); }
}
}
初看起来,这好像没多大变化,只是转型挪了地方。如果没有@SuppressWarnings注解,
你仍旧会得到unchecked警告。但是,现在的内部表示是Object[]而不是T[J。当get()被调用时, 它将对象转型为T,这实际上是正确的类型,因此这是安全的。然而,如果你调用rep(),它还是尝试若将Object[ ]转型为T[ ],这仍旧是不正确的,将在编译期产生警告,在运行时产生异常。因此,没有任何方式可以推翻底层的数组类型,它只能是Object□。在内部将array当作Object[] 而不是T[]处理的优势是:我们不太可能忘记这个数组的运行时类型,从而意外地引人缺陷(尽管大多数也可能是所有这类缺陷都可以在运行时快速地探测到)。
对于新代码,应该传递一个类型标记。在这种情况下,GenericArray看起来会像下面这样:
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> gai =
new GenericArrayWithTypeToken<Integer>(
Integer.class, 10);
// This now works:
Integer[] ia = gai.rep();
}
}