你可以说某某对象是属于某个类的,但某某类是属于?Java之前偷懒说所有的类其实都是个Object,咱Java不像C,C是没有唯一的基类的,所以必须要Template。可是用Object来包容所有类毕竟是继承的概念,与泛型(把类抽象化)的概念有很大差别,所以,Generic Types最终还是不可避免的来到了
——Fantasy Dog
Generic引进的最大原因是Type Safe(类型安全)。之前用Object表示所有类型的时候,必须用cast的方法将所需的类转换回来:
List wordList = new List();
wordList.add("Generic");
String word = (String)wordList.get(0);
这种强制转换(cast)的方式是不安全的,因为cast跳开了编译器的类型检查,编译时(Compile Time)无法发现错误,有也只能在运行的时候(RunTime)抛出一个ClassCastException。而更大的麻烦是错误源可能很难找——你有可能是在几百行代码之前错误的插入了一个其他类型的对象,慢慢找去吧。
靠Object转换来转换去的方法似乎可以做为Generic Types的替代方案,但这样一来一个致命的弱点就暴露了:Object等于完全跨越了类型安全这个C系语言引以为豪的核心功能,而java.util包下泛滥的Object参数更是对所谓完全面向对象的强类型语言Java的极大讽刺。
不明白C#这个东西为什么一开始也要学Java舍弃如此重要的Generic Types,就像中国在酝酿着一场和美国二三十年代如出一辙的经济大萧条一样,是出于无知还是无奈?
好在,该来的终究还是来了…………
1.简单的Generics
public interface Iterator<E>{
E next();
boolean hasNext();
void remove();
}
这是一个泛型类,E表示任意的一个类型。与c的不同有以下几点:
a.形式上看,声明简化了,直接在类名后面加<E>(或多个类型参数如<E,F,G>),而不用写什么template <class Type>了。
b.Generics和c的template是有本质的区别的。在c里,你用template时,声明一个类如:Iterator<MyClass>,编译器实际上是要生成一个相应的新类的(Iterator_MyClass?),而Generics里面,就只有一个类Iterator。<E>的官方名字叫做Type parameter,就是说这个E是Iterator类的一个参数,使用者使用的时候具体指定。
c.Generics只接受类,即E必须是个类,而不能是primative的东西(如int)。因此如果你要个Iterator<int>时,只能用Iterator<Integar>。而此时,automatic boxing才真算是派上了用场——你大可以把primative的东西拿来用,细节问题编译器帮你处理。
Type Parameter的名字是有讲究的,倒不是必须,而是约定俗成的一个习惯——用完全大写的字母表示,越简单越好,最好是一个,如E。
这样的习惯当然有好处:成员是小写的,类名是大写打头后跟小写的,final值的变量是完全大写并且比较长的,Generics只能挑个比较有性格的表示方法来名字防止冲突,并借此保证代码的整洁。如Collections的Type Parameter通常都是E——element。
2.Generics与Subtyping
这是一个很恼人的问题,非常非常恼人(annoying)。
List<String> ls = new ArrayList<String>();
List<Object> lo = ls;
首先这说明了Generics怎么声明。然后,这样子好像很正确,因为String 就是个 Object,那我装String的东西当然也应该可以upcast成一个装Object的东西了?
lo.add(20);
这下完蛋了,因为lo虽然是个List<Object>,但它指向的是个List<String>,Integar的东西当然不能放在String的容器里。
这却不仅仅是Generics的问题:
String[] s = new String[2];
Object[] o = s;
o[0] = new Object();
这段代码编译通过,却在运行时给出一个ArrayStoreException。是啊,又是String的东西不能装Object。
深究一下:当我们把String[] upcast成一个Object[]时,这个Object[]仍然是一个String[],因此,它只能装String,而不是像其声明一样装Object。这么说来,面向对象的核心:多态原来是不安全的?
当然,发现棉花和铁块不同时落地就断定伽利略在比萨斜塔的实验是完全错误的是一种很幼稚的想法。看起来很惊世骇俗可以颠覆权威的发现往往总是因为其自身问题造成的(往往不代表全部,但是往往代表绝大多数)。
看看s和o。实际上,构成继承关系的是String和Object,而非String[] 和 Object[]。编译器之所以允许o=s,是因为它认为s的每一个元素都可以upcast成一个Object,结果却造成了类型安全的漏洞(当然,可以在RunTime查出来)。
再来看看什么叫多态。多态是指子类具有父类的全部功能,因此可以用一个对父类的引用来使用某个对象,而不必关心该对象是属于该父类的哪一个子类。
而我们的例子是另外一回事。当涉及到数组时,upcast后,可以做两件事:1,调用某个元素的Object功能如toString,此曰多态;2,在某个元素的位置插入一个Object或其子类的对象,这个……就是错误了。
同样的道理,在泛型里,String是Object的子类,但List<String>却不是List<Object>的子类,他们是相同的类,具有不同的Type parameter而已。编译器当然无法保证某个Type parameter在类内部是如何操作的,如果有如同上面数组那样的操作,就必然会产生非常隐蔽的错误,而此错误很难被跟踪。因此,编译器干脆就把List<Object> lo = ls;这条语句当作非法的。
窃以为,不同数组之间的相互转换(o=s)也应该被禁止。但估计在泛型出来之前,这个问题没被怎么重视。现在改也不行了,因为要保证向前兼容。咱们只能自己注意了。
然则之后Java干了件令人咂舌的事:
ArrayList<String>[] wordList = new ArrayList<String>[10];
这样的代码是被禁止的。原因是它无法保证你不会乱用这个数组:
Object[] lo = wordList;//不是继承的关系,但是这种upcast是被允许的(且为了向前兼容,不能被撤销)
ArrayList<Integar> li = new ArrayList<Integar>();
lo[0] = li;//插入一个ArrayList<Integar>,这种cast当然也被接受
li.add(123);
String ls = wordList[0].get(0);//错,运行时错,叫ClassCastException,不能把Integar cast成 String。
所以……java干脆就禁止你声明parameterized type的数组!(ArrayList<String>这样给了type参数的就叫做parameterized type)这不是典型的因噎废食嘛~~~没办法,问题的关键在于对数组的cast没有做限制,可是“向前兼容”这四个字牢牢地压在了java身上。唉~~~