為什麼需要泛型? 試想你需要一個簡單的容器類,或者說句柄類,比如要存放一個蘋果的籃子,那你可以這樣簡單的實現: 這樣一個簡單的籃子就實現了,但問題是它只能存放蘋果,之後又出現了另外的一大堆水果類,那你就不得不為這些水果類分別實現容器: 然後你發現你其實在做大量的重覆勞動。所以你幻想你的語言編譯器要是 ...
為什麼需要泛型?
試想你需要一個簡單的容器類,或者說句柄類,比如要存放一個蘋果的籃子,那你可以這樣簡單的實現:
class Fruit{}
class Apple extends Fruit{}
class Bucket{
private Apple apple;
public void set(Apple apple){
this.apple = apple;
}
java學習群669823128
public Apple get(){
return this.apple;
}
}
這樣一個簡單的籃子就實現了,但問題是它只能存放蘋果,之後又出現了另外的一大堆水果類,那你就不得不為這些水果類分別實現容器:
class Fruit{}
class Apple extends Fruit{}
class Banana extends Fruit{}
class Orange extends Fruit{}
class BucketApple{
private Apple apple;
public void set(Apple apple){
this.apple = apple;
}
public Apple get(){
return this.apple;
}
}
class BucketBanana{
private Banana banana;
public void set(Banana banana){
this.banana = banana;
}
public Banana get(){
return this.banana;
}
}
class BucketOrange{
private Orange orange;
java學習群669823128
public void set(Orange orange){
this.orange = orange;
}
public Orange get(){
return this.orange;
}
}
然後你發現你其實在做大量的重覆勞動。所以你幻想你的語言編譯器要是支持某一種功能,能夠幫你自動生成這些代碼就好了。
不過在祈求讓編譯器幫你生成這些代碼之前,你突然想到了Object能夠引用任何類型的對象,所以你只要寫一個Object類型的Bucket就可以存放任何類型了。
class Bucket{
private Object object;
public void set(Object object){
this.object = object;
}
public Object get(){
return this.object;
}
}
但是問題是這種容器的類型丟失了,你不得不在輸出的地方加入類型轉換:
Bucket appleBucket = new Bucket();
bucket.set(new Apple());
Apple apple = (Apple)bucket.get();
而且你無法保證被放入容器的就是Apple,因為Object可以指向任何引用類型。
這個時候你可能又要祈求編譯器來幫你完成這些類型檢查了。
說道這裡,你應該明白了泛型要保證兩件事,第一:我只需要定義一次類,就可以被“任何”類型使用,而不是對每一種類型定義一個類。第二:我的泛型只能保存我指明的類型,而不是放一堆object引用。
實際上很多語言的泛型就是基於以上兩點而實現的,下麵我就將分別介紹c++,java,c# 的泛型是如何實現的。對比的原因是為了說明為什麼它要這麼實現,這樣實現的優點和缺點是什麼。
c++
巨集
大部分人在大學都學過c語言,你一定記得c語言中有一種被稱為巨集的東西,巨集能夠在預編譯期來“替換”代碼。因為c++是相容c的,所以巨集在c++中同樣可以使用。
比如下麵這段代碼:
#define square(x) x*x
int a = square(5);
在預處理期之後,你代碼中所有的 square(5);
,都被替換成了 5*5
。
同理,有人把c++中的模板稱為高級巨集,當我們在c++中定義一個Bucket模板類之後。可以分別去聲明不同類型的模板實現。
#include <iostream>
template<class T> class Bucket{
private:
T stuff;
public:
void set(T t){
this->stuff = t;
}
T get(){
return this->stuff;
}
};
class Fruit{};
class Apple : public Fruit{};
class Banana : public Fruit{};
class Orange : public Fruit{};
int main() {
std::cout << "Hello, World!" << std::endl;
Bucket<Apple> appleBucket;
appleBucket.set(Apple());
Bucket<Banana> bananaBucket;
bananaBucket.set(Banana());
return 0;
}
而當你在編譯之前,c++的模板會進行展開,變成類似這個樣子:
class Bucket_Apple {
private:
Apple stuff;
public:
void set(Apple t){
this->stuff = t;
}
Apple get(){
return this->stuff;
}
};
這樣你就明白為什麼c++的模板能夠實現泛型了吧,因為它幫你生成了不同類型的代碼。
實際上c++的模板又稱為:編譯時多態技術,功能遠比泛型強大。我們常聽到的:“c++元編程”,即所謂的用代碼來生成代碼的技術。就是基於它的模板。
但是你發現這種技術有一個弊端,就是如果我要聲明瞭100個不同類型的水果容器,那它可能會生成100份代碼。那大量使用模板的c++代碼,編譯後的文件將非常大。(猜測某些編譯器可能會做優化處理,這部分我並不是很清楚,歡迎指正。)
java
java的泛型在底層實現上使用了Object引用,也就是我們之前所提到的第二種方式,但是為了防止你往一個Apple的Bucket添加一個Banana。編譯器會先根據你聲明的泛型類型進行靜態類型檢查,然後再進行類型擦出,擦除為Object。而所謂的類型檢查,就是在邊界(對象進入和離開的地方)處,檢查類型是否符合某種約束,簡單的來說包括:
- 賦值語句的左右兩邊類型必須相容。
- 函數調用的實參與其形參類型必須相容。
- return的表達式類型與函數定義的返回值類型必須相容。
- 還有多態類型檢查,既向上轉型可以直接通過,但是向下轉型必須強制類型轉換(前提是有繼承關係)
Number n = new Integer(1);
Integer b = (Integer)n;
但是要註意的一點是,編譯器只會檢查繼承關係是否符合。強轉本身如果有問題,在運行時才會發現。所以下麵這行代碼在運行期才會拋異常。
// n is Integer
Double d = (Double)n;
所以你不能在一個ArrayList 中插入一個String對象, 但在運行是列印泛型類的類型卻是一樣的:
ArrayList<Integer> arrayListInt = new ArrayList<Integer>();
ArrayList<String> arrayListString = new ArrayList<String>();
ArrayList arrayList = new ArrayList();
System.out.println(arrayListInt.getClass().getName());
System.out.println(arrayListString.getClass().getName());
System.out.println(arrayList.getClass().getName());
# all print java.util.ArrayList
但這種技術也有一個弊端,就是既然擦成object了,那麼在運行的時候,你根本不能確定這個對象到底是什麼類型,雖然你可以通過編譯器幫你插入的checkcast來獲得此對象的類型。但是你並不能把T真正的當作一個類型使用:比如這條語句在java中是非法的。
// error
T a = new T();
同理,因為都被擦成了Object,你就不能根據類型來做某種區分。比如異常繼承:
// error
try {
} catch (SomeException<Integer> e) {
} catch (SomeException<String> e) {
}
比如重載:
// error
void f(List<T> v);
void f(List<W> v);
還有因為基本類型int並不屬於oop,所以它不能被擦除為Object,那麼java的泛型也不能用於基本類型。
// error
List<int> a;
類型擦出到底指什麼?
首先你要明白一點,一個對象的類型永遠不會被擦出的,比如你用一個Object去引用一個Apple對象,你還是可以獲得到它的類型的。比如用RTTI。
Object object = new Apple();
System.out.println(object.getClass().getName());
# will print Apple
哪怕它是放到泛型里的。
class Bucket<T>{
private T t;
public void set(T t){
this.t = t;
}
public T get(){
return this.t;
}
public void showClass(){
System.out.println(t.getClass().getName());
}
}
Bucket<Apple> appleBucket = new Bucket<Apple>();
appleBucket.set(new Apple());
appleBucket.showClass();
# will print Apple too
為啥?因為引用就是一個用來訪問對象的標簽而已,對象一直在堆上放著呢。
所以不要斷章取義認為類型擦出就是把容器內對象的類型擦掉了,所謂的類型擦出,是指容器類 Bucket<Apple>
,對於Apple的類型聲明在編譯期的類型檢查之後被擦掉,變為和 Bucket<Object>
等同效果,也可以說是 Bucket<Apple>
和 Bucket<Banana>
被擦為和 Bucket<Object>
等價,而不是指裡面的對象本身的類型被擦掉!
c#
c#結合了c++的展開和java的代碼共用。
首先在編譯時,c#會將泛型編譯成元數據,即生成.net的IL Assembly代碼。併在CLR運行時,通過JIT(即時編譯), 將IL代碼即時編譯成相應類型的特化代碼。
這樣的好處是既不會像c++那樣生成多份代碼,又不會像java那樣,丟失了泛型的類型。基本做到了兩全其美。
所以總結一下c++,java,c#的泛型。c++的泛型在編譯時完全展開,類型精度高,共用代碼差。java的泛型使用類型擦出,僅在編譯時做類型檢查,在運行時擦出,共用代碼好,但是類型精度不行。c#的泛型使用混合實現方式,在運行時展開,類型精度高,代碼共用不錯。
為什麼java要用類型擦除?
看到這裡你可能會問,為什麼java要用類型擦除這樣的技術來實現泛型,而不是像c#那樣高大上,難道是因為sun的那群人技術水平遠比不上微軟的那群人麽?
原因是為了向後相容。
你去查查歷史就會知道,c#和java在一開始都是不支持泛型的。為了讓一個不支持泛型的語言支持泛型,只有兩條路可以走,要麼以前的非泛型容器保持不變,然後平行的增加一套泛型化的類型。要麼直接把已有的非泛型容器擴展為泛型。不添加任何新的泛型版本。
當時c#從1.1升級到了2.0,代碼並不是很多,而且都在微軟.net的可控範圍,所以選擇了第一種實現方式,其實你可以發現,c#中有兩種寫法,非泛型寫法和泛型寫法:
// 非泛型
ArrayList array = new ArrayList();
// 泛型
List<int> list = new List<int>();
而java的非泛型容器,已經從1.4.2占有到5.0,市面上已經有大量的代碼,不得已選擇了第二種方法。(之所以是從1.4.2開始,是因為java以前連collection都沒有,是一種vector的寫法。),而且有一個更重要的原因就是之前提到的向後相容。所謂的向後相容,是保證1.5的程式在8.0上還可以運行。(當然指的是二進位相容,而非源碼相容。)所以本質上是為了讓非泛型的java程式在後續支持泛型的jvm上還可以運行。
那麼為什麼使用類型擦除就能保持向後相容呢?
在《java編程思想》中講到了這樣一個例子,下麵兩種代碼在編譯成java虛擬機彙編碼是一樣的,所以無論是函數的返回類型是T,還是你自己主動寫強轉,最後都是插入一條checkcast語句而已:
class SimpleHolder{
private Object obj;
public Object getObj() {
return obj;
}
public void setObj(Object obj) {
this.obj = obj;
}
}
SimpleHolder holder = new SimpleHolder();
holder.setObj("Item");
String s = (String)holder.getObj();
class GenericHolder<T>{
private T obj;
public T getObj() {
return obj;
}
public void setObj(T obj) {
this.obj = obj;
}
}
GenericHolder<String> holder = new GenericHolder<String>();
holder.setObj("Item");
String s = holder.getObj();
aload_1
invokevirtual // Method get: ()Object
checkcast // class java/lang/String
astore_2
return
我形象的理解為,之前非泛型的寫法,編譯成的虛擬機彙編碼塊是A,之後的泛型寫法,只是在A的前面,後面“插入”了其它的彙編碼,而並不會破壞A這個整體。這才算是既把非泛型“擴展為泛型”,又相容了非泛型。
這下你應該理解“java為什麼要用類型擦除實現泛型?”和這樣實現的優劣了吧
java學習群669823128