为编写更通用的代码,要使代码能够应用于N种不具体的类型,而不仅仅为几个接口或类服务。这时候引入了泛型的概念,它的意思是"适用于丰富的类型",旨在解除类或方法与所使用的类型之间的约束。使用别人已经构建好的泛型类型会相当容易,但要自己创建一个泛型实例,会遇到许多令人惊讶的事情。掌握C++当中的模板概念,将帮助我们去理解泛型的本质以及java泛型的局限。
容器类
在创造具备重用性的容器类的过程中,泛型应运而生。有些情况下,确实希望容器能够同时持有多种类型的对象。但通常而言,只会使用容器来存储某种类型的对象。而泛型的主要目的之一便是指定容器类要持有什么类型的对象,由编译器来保证类型的正确性。类型参数可推迟决定具体使用什么类型,用<>括住,置于类名后面。而在使用这个类的时侯,必须指明想持有什么类型的对象。
public class Page<T>{
private T a;
public Page(T a){ this.a=a;}
public void set(T a){this.a=a;}
public T get(){return a;}
}
元组
常常需要一次返回多个对象,而元组能够一次性地解决这样的需求,它允许读取其中元素,但是不允许向其中存放新的对象(依赖final机制实现)。元组可以含有任意个不同类型
的对象。下面的程序是一个2维元组,它持有两个对象。public class TwoTuple<A,B>{
//final保证无法将其它值赋予first和second,但可以任意使用这两个对象
public final A first;
public final B second;
public TwoTuple(A a,B b){first=a;second=b;}
}
以上设计形式是考虑到有时确实希望编程人员改变first或second所引用的对象,他们可创建一个新的TwoTuple对象,达到使用具有不同元素的元组。还可以利用继承实现长度更长的元组。使用方式如下:
static TwoTuple<String,Integer> f() {
return new TwoTuple<String,Integer>("hello",88);
}
泛型接口
还能够使用泛型实现堆栈LinkedStack<T>、一个可以应用于各种类型对象的工具列表RandomList<T>
泛型接口,比如生成器就应用了此机制,它与工厂方法相相比,无需额外的信息(参数)就知道如何创建对象,一般只定义一个无参方法next()
public interface Generator<T>{
T next();
}
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);
}
}
现在要获得一个实现了Iterable的Fibonacci生成器,且无法重写Fibonacci这个类的时候,可选择创建一个适配器类来实现所需接口,组合和继承均能够实现适配器设计模式。
public class IterableFibonacci extends Fibonacci implements Iterable<Integer>{
private int size;
public IterableFibonacci(int count){
size=count;
}
public Iterator<Integer> iterator(){
return new Interator<Integer>(){
public boolean hasNext(){ return size>0;}
public Integer next(){
size--;
return IterableFibonacci.this.next();
}
public void remove(){
//懒得实现
}
};
}
public static void main(String[] args){
for(int i:new IterableFibonacci(20))
System.out.print(i+" ");
}
}
泛型方法
拥有泛型方法的类可以是泛型类,也可以不是泛型类。无论何时,若只使用泛型方法可以取代将整个类泛型化的话,那就应该只使用泛型方法。定义时只需将泛型参数列表置于返回类型之前。使用泛型方法时,通常不必指明参数类型,类型参数推断避免了重复的泛型参数列表的输写,但它只对赋值操作有效。
public class Tool{
public static <K,V> Map<K,V> map(){
return new HashMap<K,V>();
}
public static <T> LinkedList<T> lList(){
return new LinkedList<T>();
}
public <T> void f(T x){
System.out.println(x.getClass().getName());
}
public static void main(String[] args){
Tool tool=new Tool();
tool.f("");tool.f(1.0);
Map<Person,List<? extends Pet>> petPeople=tool.map();
LinkedList<String> lls=tool.lList();
}
}
使用泛型方法来构建一个Set实用工具
public class Sets{
public static <T> Set<T> union(Set<T> a,Set<T> b){
//将Set中的所有引用都存入一个新的HashSet对象,并不直接修改参数中的Set
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));
}
}
当然也可使用Sets.difference()方法求得差异Set<String>。
使用匿名内部类来实现Generator泛型接口
class Customer{
private static long counter=1;
private final long id=counter++;
private Customer(){}
public static Generator<Customer> generator(){
return new Generator<Customer>(){
public Customer next(){
return new Customer();
}
};
}
}
擦除概念的理解
在泛型机制内部,无法获得任何有关泛型参数类型的信息,这是通过擦除来实现的。意味着你可以了解到类型参数标识符和泛型类型边界这类信息,却无法知道用来创建某个特定实例时实际使用的类型参数。
Class c1=new ArrayList<Integer>().getClass();
Class c2=new ArrayList<String>().getClass();
程序会认为c1==c2,即是相同的类型,List<String>和List<Integer>都被擦除成它们的"原生"类型,即List。所以在使用泛型时,任何具体的类型信息都被擦除了,而唯一知道的就是你在使用一个对象。
边界--处理擦除的方式
给定泛型类的边界,以此告知编译器只能接受遵循这个边界的类型,这里要运用Extends关键字。
class Manipulator2<T extends Hex>{
private T obj;
public Manipulator2(T x){
obj=x;
}
public void manipulate(){
obj.f();
}
}
编译器实际上会把类型参数替换为它的擦除,就像上面的示例一样,T擦除到了Hex。只有当你希望使用的类型参数比某个具体类型(及它的所有子类型)更加泛化时,也就是说,当你希望代码能够跨多个类工作时,使用泛型才有所帮助,始终应该记住这一点。