限制未必都是有损于自由的,通常却是为了更好的保障自由,比如红绿灯,又比如Generics的Bound
——Fantasy Dog
![](/fckeditor/editor/images/smiley/msn/embaressed_smile.gif)
面向对象最大的好处就是向后兼容性,比如你写俄罗斯方块的时候,用Block类表示方块,然后继承之表示各种不同的方块(长条,正方,品字形等),再然后你发现想加入一个新品种的方块,仅仅需要再定义这个方块的特性——即从Block再继承一个新类就可以了,游戏代码中大量的关于Block的操作都不必修改,因为新的方块也被抽象成了一个Block,其特征通过多态性表现。
可惜Generics里面的parameter要求太苛刻了,List<String>根本不是List<Object>,所以寒数method(List<Object> lo)根本不能接受List<String>为参数,这似乎抹杀了面向对象的最大优势。其实,苛刻的Generics却引入了更合理的机制来运用面向对象的特性。
1. Raw Type
raw的意思是未加工的,这里的意思却是未指定类型的。java要向前兼容,因此
List l = new ArrayList();
这样的代码是要编译通过的。
这是个很让人头疼的东西(如果c++不是为了保证和C兼容,它也不会像今天这么令人头疼)。因为我们都知道List内部的元素是按照Object存储的,但它却不等价于List<Object>。它更自由,因为你可以像以前一样往raw type里加入任何一个东西:
List l = new ArrayList();
l.add("hello");
l.add(new Integer(123));
Object o = l.get(0);
1.5的编译器充其量会给出一个编译警告(需指定-XLint选项),说是在add函数处,add进的元素是unchecked的。
并且,List的对象可以任意指向某个Parameterized Type的对象,如:
List<String> ls = new ArrayList<String>();
List l = ls;
这样的代码是完全合法的。可惜如果你为了向List<Base>里加入Derived,就把List<Base>的对象cast成List,那等于抛弃了Generics的好处——你又必须自己面对类型检查的问题,任何疏忽仍然会导致莫名其妙的错误。而且,正如1.5之前的java那样,List里装入某个东西,之后在某个未知的地点cast出来,万一发生错误,定位到装入错误的数据类型的地方是很麻烦很痛苦的,因为RunTime Exception只能在cast的地方抛出异常。
幸运的是,1.5提供了一种方法来克服这个问题:
List<Number> ln = new ArrayList<Number>();
List<Number> cln = Collections.checkedList(ln,Number.class);
List l = ln;
通过checkedList方法,cln就变成了一个CheckedList<Number>,同样l也是。结果虽然在编译时无法还是查错(向l加入任何类型都是合法的),但在运行时会有ClassCastException,并且错误会被定位在add错误的类型的位置(应该是通过多态的方式实现的,而多态只能在运行时起作用)——于是乎既保证了类型安全,又能很自由的利用继承机制:
method(List l)//可以接受任何List<T>,只要是由checkedList得到就有一定程度的安全保障
这个东东被称为Runtime type safety。显而易见,这种方法复杂,累赘,并且颇有些亡羊补牢的意思——我们当然希望在编译时就能发现错误,否则就得设计完全覆盖程序路径的测试用例,很是愚公移山般吃力不讨好且折磨人的身心健康的笨方法。更糟的是,这必须有额外的类库支持(Collections),很难普及到大多数Generics的情形——看看Collections的源代码就知道,实现这样的类库是多么复杂累人的苦力活。
2. Type Parameter Wildcards
当然,负责的Generics不可能让raw type完成其不可能完成的使命,于是它自己定义了更加优秀也当然更加复杂的规则:
public static void printList(PrintWriter out, List<?> list)
是的,?
这个?号就叫做Wildcards。一般人都知道问号是通配符的一种,在dos下表示任何一个字符。这里,它就表示任何一个类。
任何一个类是什么类?你当然不可以胡乱的估计它是Number还是什么其他的,只能作为Object对待。于是似乎只能这样操作这个List:
For(Object o : list)
那这和List<T> list有什么差别呢?T也是指代了任何一个什么类啊?
当然有差别,很实质性的。比如你可以用T声明一个变量(但不能new T),却不能用?声明一个变量;你可以用T做参数调用另一个函数,却不能用?;你可以在其它类空间里声明List<?>表示任意一个parameterized的List,却不能用T,因为T未定义……
很多很多,但都是皮毛的问题。真正的问题是,我该什么时候用T,什么时候用?
呵呵,当?只是Wildcards时,它似乎一点价值也没有……
3. Bound
晕了晕了,我们想干吗来着?
哦,我们要的是Generics里面的那个type参数也能实现多态的功能。即我们想定义这样的一个函数:
method(List<Number> l)
这个函数可以操作一个List对象,且其类型参数为Number,或者Integer,或者Double。虽然List<Integer>和List<Number> 不是继承关系,但是我们却希望利用Integer和Number的继承关系实现多态。
是啊,是Integer和Number的继承关系,那就别再折磨List啦:
method(List<? extends Number> ln)
嗯,初看复杂,其实蛮漂亮。尖括号里的意思是“任何一个类型,继承自Number”。结果,这个修改过的看似怪胎的method就可以接受一系列的List,只要其类型参数是Number或其子类。
注:这里的继承和其它的继承差不多,都是可以至多一个类加无数个接口。但是一方面extends和implements都写麻烦了点,一方面逗号(,)在类型参数这个地方是用来隔离两个参数的,因此完整形式如下:
List<? extends BaseClass & Interface1 & Interface2>
回到List<? extends Number>,这个Number就叫做Bound,意思是该类型参数的“上界”。像之前那个List<?>其实就和List<? extends Object>等价了。
哦,有了Bound,这List就好用多了,我就可以当List<Number>用啦~
不!!!Generics的复杂就在于它处处有陷阱,稍不留神就会犯错。
看看List<? extends Number>,你知道这个List里面肯定装着一个Number,但是你并不知道List到底是什么。如果它是List<Short>呢?你能装一个Integer吗?
所谓的多态指的是参数类型的多态,而非List本身的多态。
Generics里面最容易混淆的就是parameter和parameterized type。前者是Number,后者是List<Number>,他们是不同的,有着各自的类层次。而通常情况下parameter的层次是独立的(废话!),parameterized type的类层次也是独立的!ArrayList<E>的父类就是List<E>(List本身是接口,但这里用来描述类层次没有多大差别)。因此,parameterized type的多态就是:
method(List<E> le)
可以接受ArrayList<E>类型的参数。这个概念很好理解。而parameter的多态性前面已经解决了。那我们这个add的问题又该怎么样解决呢?
Add的问题抽象化一点,就是,我希望有一个List<E>,可以向里面装一些东西,而保证我向里面装的东西是这个List<E>可以接受的:
public static void store(List<? super Number>)
super可以理解为extends的反义词。这个类型参数的意思是:任何一个Number的父类或Number自己。符合这个条件的List当然可以装Number,以及Number的子类。
因此,如果你是想要一个List,然后向里面倒Number,Integer之流的东西,用super吧。
结论:
Bound其实就分两种,extends表示上限,super表示下限。用法:
a. 如果你想把parameter弄出来用,extends将符合这个逻辑
b. 如果你是想对parameterized type进行操作,如add或setValue,用super吧。
注:
数组。毫无疑问,数组和类的层次会导致意想不到的灾难后果——因此java不允许声明一个parameterized type的数组。不过没关系,你大可以利用Collections里面的工具。更激进一些的看法是,数组应该尽量的少用了,多靠Collections替代,因为Generics可以提供更好的类型安全检查。