Java中的泛型
所谓泛型,就是指编程语言中能够编写出更加“通用”、“泛化”的代码。希望能利用泛型,编写出一个能处理各种各样类型对象实例的处理过程代码。
首先,考虑下面一段通用代码:
public class SimpleHolder { //一个简单的通用的容器 private Object obj; //利用Object来达到通用性 public void set(Object obj){ this.obj = obj; } public Object get(){ return obj; } public static void main(String[] args) { SimpleHolder holder = new SimpleHolfer(); holder.set("Item"); //holder.set(3); //向其中放入Integer也是可以的,即没有类型检查机制 String s = (String) holder.get(); //从容器中取出元素时,要自己负责转型 } }
在java se5之前,很多代码可能就是这样编写的,但java引入了泛型之后:
public class GenericHolder<T> { //一个泛型的通用的容器 private T obj; //利用泛型来达到通用性 public void set(T obj){ this.obj = obj; } public T get(){ return obj; } public static void main(String[] args) { GenericHolder<String> holder = new GenericHolder<String>(); holder.set("Item"); //holder.set(3); //不可以,编译期会进行类型检查 String s = holder.get(); //取得元素时,由泛型机制自动转型为String } }
对比上面的两段代码,发现使用泛型时,java主要多做了两件事:
(1)编译器的类型检查
(2)自动转型
java为了向后兼容,在java se5引入泛型时,选择使用擦除机制来实现泛型。这也被称为伪泛型,与C++中的模板真泛型相比,是存在一些缺陷的。泛型类型只有在java进行静态类型检查期间才出现,在这之后,程序中的所有泛型类型都将被擦除,替换为它的非泛型边界。当然,也正是这种擦除,提供了程序以泛化能力,例如指定泛型类型为<T extends Person>,则在编译之后,类型被擦除到边界Person,那么这段处理代码对Person及其子类都是通用的,而不管所处理的对象是男人、女人或儿童。在编译时,java将泛型类型参数擦除到它的第一个边界。如List<T>被擦除为List,<T
extends Cat>被擦除为Cat,Class<T>被擦除为Class,未指定边界的T被擦除为Object。
擦除是有代价的,任何在运行时需要知道确定类型信息的操作都将无法工作。例:
class Erased<T>{ public static void f (Object arg) { if(arg instanceof T){ //编译出错:尽管使用Erased<String>来创建对象,好像T即为String型 //do some //但是经过擦除,java已经不再知道T是String了 } T var = new T(); //出错,同样是因为擦除 //而在C++中,只要保证T具有无参构造,就能执行 } }
但是如果在Erased类中添加一个表示T的Class对象的属性,且在构造Erased的实例时通过构造器传入这个对应的Class对象,就能在被擦除之后,获得T的实际类型,甚至用来创建T的对象。
Class<T> kind; //新增一个Class属性 Erased<MyClass> test = new Erased<String>(MyClass.class); //在构造对象时传如参数类型T所对应的Class对象 T x = kind.newInstance(); //通过传入的class对象,间接的创建类型T的对象(需要保证MyClass有无参构造器) if (kind.isInstance(arg)){ //通过传入的class对象,间接的取得T的实际类型 //do some }
但是在实际使用中,上面的方式并不常用,且java也不推荐,java建议使用显示的工厂(Factory)。
不能创建泛型数组。但是能够通过创建一个被擦除类型的新数组,然后在转型的方式来创建。
T[] array = new T[size]; //不可行 T[] array = (T) new Object[size]; //可行,但是实际运行类型仍是Object
(2017.5.15补充,数组是协变的,List是不可变的。协变的数组是不合理的[见effective Java])
然而一般在想要创建泛型数组时,会选择用ArrayList来代替。
边界:当对泛型参数类型未加边界时,那么类型T会被擦除为Object,则在处理流程中,想要调用T的方法时就只能是哪些Object所拥有的方法,而无法调用T中实际拥有的方法。而当有了边界之后<T extends HasMethod>,擦除只会擦除到边界处,那么就可以调用边界类所拥有的方法或域了。<T extends HasMethod & HasField>
通配符:
首先明确一个比较绕的问题:<? extends T> 与 <? super T>,此二者都代表某种特定的类型
<? extends T>:说明此类型是T的子类的一种,注意是单个的不确定的一种,不是T的子类的集合!
<? super T> :说明此类型是T的超类的一种(不是说以T为超类!)
这块有个比较绕的概念:能放入父类的容器,一定能放入其子类(多态),但是能放入子类的容器不一定能放入其父类(向下转型不安全)。
class Fruit{ } class Apple extends Fruit{ } class Orange extends Fruit{ } class RedApple extends Apple { } List<Fruit> flist = new ArrayList<Apple>(); //编译错误,类型不兼容,一个apple的list,并不是一个fruit的list List<? extends Fruit> flist = new ArrayList<Apple>(); //声明是正确的,但是没什么意义,见下面的几行 // flist.add(new Apple()); //编译出错 // flist.add(new Fruit()); //编译出错 // flist.add(new Object()); //编译出错 flist.add(null); //唯一能放入的只有null Fruit fruit = flist.get(0); //正确
理解:指明<? extends Fruit>说明这个list中能放入的是Fruit的一种子类(注意是一种),具体是哪一种不知道,正是因为这种未知,所以无法安全的向其中添加任何的对象,除了null。由于<? extends Fruit>代表的是Fruit的某种子类,当它代表RedApple时,其父类apple及祖父类fruit都不能放入(水果不一定是苹果,苹果又不一定是红的)。而当<? extends Fruit>表示RedApple的子类时导致连RedApple都不能放入,循环下去,最终就导致连object都不能放入了,因为不安全。而list中的元素时Fruit的某种子类,那一定是一种Fruit,所以用get取出一个Fruit是安全的。
List<? super Fruit> flist = new ArrayList<Apple>(); //编译出错,Apple不是Fruit的超类 List<? super Fruit> flist = new ArrayList<Fruit>(); //正确 flist.add(new Fruit()); //正确 flist.add(new Apple()); //正确 flist.add(new Object()); //编译出错 Fruit item = flist.get(0); //编译出错,向下转型不安全
理解:<? super Fruit>表示的是Fruit的超类型的一种,具体是哪一种也不知道,但是!!,能放入超类的容器一定也能放入其子类,故而apple或fruit向上转型一定能转型为这个超类类型,但是object转型到这个超类就是向下转型了,不安全。且从容器中取出一个元素时,这个元素只知道是Fruit的超类,但是不一定是Fruit