Java和C++在細節(jié)上的差異:泛型程序設(shè)計
繼上篇文章:Java和C++在細節(jié)上的差異:接口與內(nèi)部類
八、泛型程序設(shè)計:
1. 泛型類的定義,見如下代碼:
- public class Pair<T,U> {
- public Pair() { first = null; second = null; }
- public Pair(T first,U second) { this.first = first; this.second = second; }
- public T getFirst() { return first; }
- public U getSecond() { return second; }
- public void setFirst(T first) { this.first = first; }
- public void setSecond(U second) { this.second = second; }
- private T first;
- private U second;
- }
以上代碼中的T,U都是泛型類Pair的類型參數(shù)。以下為C++中模板類的定義方式:
- template<typename T,typename U>
- class Pair {
- public:
- Pair(T first,U second): _first(first),_second(second) {}
- ~Pair() {}
- public:
- T getFirst() { return _first; }
- U getSecond() { return _second; }
- void setFirst(T frist) { _first = first; }
- void setSecond(U second) { _second = second; }
- private:
- T _first;
- U _second;
- }
2. 泛型方法的定義,在Java中泛型方法不一定聲明在泛型類中,可以聲明在普通類中,見如下代碼:
- public class MyFirst {
- public static void main(String[] args) throws Exception {
- String[] names = {"john","Q.","Public"};
- String middle = ArrayAlgo.<String>getMiddle(names);
- System.out.println(middle);
- }
- }
- class ArrayAlgo {
- public static <T> T getMiddle(T[] a) {
- return a[a.length/2];
- }
- }
在以上代碼中可以看出getMiddle方法為靜態(tài)泛型方法,類型變量位于修飾符"public static" 的后面,返回值的前面。調(diào)用的時候在方法名的前面給出了參數(shù)類型。由于Java的編譯器提供類型推演的功能,既類型參數(shù)可以通過函數(shù)參數(shù)的類型進行推演,因此也可以直接調(diào)用泛型函數(shù),如String middle = ArrayAlgo.getMiddle(names)。如果編譯器無法通過函數(shù)參數(shù)的類型推演出類型參數(shù)的實際類型,這樣將會導(dǎo)致編譯錯誤。在C++中同樣存在模板函數(shù),也同樣存在模板函數(shù)的類型推演,在這一點上主要的差異來自于函數(shù)聲明的語法,見如下C++代碼:
- class ArrayAlgo {
- public:
- template<typename T>
- static T getMiddle(T* a,size_t len) {
- return a[len/2];
- }
- };
- int main()
- {
- int v[] = {1,2,3};
- int ret = ArrayAlgo::getMiddle(v,3);
- printf("This value is %d.\n",ret);
- return 0;
- }
3. 類型參數(shù)的限定:有些泛型函數(shù)在使用類型參數(shù)變量時,經(jīng)常會用到該類型的特殊方法,如在進行數(shù)組元素比較時,要求數(shù)組中的元素必須是Comparable接口的實現(xiàn)類,見如下代碼:
- public static <T> T min(T[] a) {
- if (a == null || a.length == 0)
- return null;
- T smallest = a[0];
- for (int i = 0; i < a.length; ++i) {
- if (smallest.compareTo(a[i]) > 0)
- smallest = a[i];
- }
- return smallest;
- }
在以上代碼中,數(shù)組元素的類型為T,如果該類型并未提供compareTo域方法,將會導(dǎo)致編譯錯誤,如何確保類型參數(shù)確實提供了compareTo方法呢?如果T是Comparable接口的實現(xiàn)類,那么該方法一定會被提供,因此可以通過Java語法中提供的類型參數(shù)限定的方式來確保這一點。見如下修訂代碼:
- public static <T extends Comparable> T min(T[] a) {
- if (a == null || a.length == 0)
- return null;
- T smallest = a[0];
- for (int i = 0; i < a.length; ++i) {
- if (smallest.compareTo(a[i]) > 0)
- smallest = a[i];
- }
- return smallest;
- }
其中的<T extends Comparable>語法保證了類型參數(shù)必須是Comparable接口的實現(xiàn)類,否則將會導(dǎo)致編譯錯誤。Java中可以支持多接口的限定,之間用&分隔,如<T extends Comparable & Serializable>和之前的例子一樣,盡管同樣都會導(dǎo)致編譯錯誤,但是后者不僅會產(chǎn)生更為明確的編譯錯誤信息,同樣也使使用者能夠更加清晰的看到該方法的使用規(guī)則。在標(biāo)準(zhǔn)C++中并未提供這樣的限定,但是在C++中對該種方式有另外一種稱謂,叫做"類型綁定",在Boost等開源庫中通過更復(fù)雜的模板技巧模仿了該功能的實現(xiàn)。然而,就泛型的該功能而言,C#的支持也是相當(dāng)不錯的,可參考C#泛型中的which關(guān)鍵字。
在標(biāo)準(zhǔn)C++中,其模板的實現(xiàn)較Java而言更為靈活和強大。對于***個例子中的代碼,只是要求模參必須提供compareTo方法即可通過編譯。
- template<typename T>
- static T min(T* a,size_t len) {
- T smallest = a[0];
- for (int i = 0; i < len; ++i) {
- if (smallest.compareTo(a[i]) > 0)
- smallest = a[i];
- }
- return smallest;
- }
注:C++中的模板是在引用時才編譯的,因此如果在模板類型中出現(xiàn)任何語法錯誤,但此時尚未有任何引用時,編譯器是不會報錯的。
4. 泛型代碼中的類型擦除:記得在我閱讀Thinking in Java 4th 的時候,書中給出了一些比較明確的解釋,為什么Java會這樣實現(xiàn)泛型,其最主要的原因是為了考慮向前兼容,也承認這樣的實現(xiàn)方式有著很多的缺陷和弊病,希望Java在今后的版本中予以補足。
簡單的說類型擦除,就是幾乎所有的泛型相關(guān)的行為都是由編譯器通過暗插各種各樣的代碼,或者是暗自修訂部分代碼的聲明,然后再將修訂后的代碼(基本不再包含泛型信息)生成字節(jié)碼后交給JVM去執(zhí)行,因此可以據(jù)此判斷在JVM中對我們的泛型類型是一無所知的。C++也是同樣的道理,只是編譯器完成的工作被定義為類型展開或類型實例化,因此,同樣的模板類,如果實例化的類型參數(shù)不同,那么用他們聲明出來的類對象也同樣不屬于相同類型的對象,其限制主要表現(xiàn)為,不能通過缺省copy constructor或者缺省賦值操作符來完成對象之間的復(fù)制,除非其中某個類型實例化后的對象專門針對另外一種類型實例化后的類型進行了copy constructor和賦值等于的重載。
1) 類型擦除:將類型參數(shù)替換為限定類型,如果沒有限定類型則替換為Object,見如下代碼:
- public class Pair<T> {
- public Pair(T first,T second) { this.first = first; this.second = second; }
- public T getFirst() { return first; }
- public T getSecond() { return second; }
- public void setFirst(T first) { this.first = first; }
- public void setSecond(T second) { this.second = second; }
- private T first;
- private T second;
- }
由于Pair中的類型參數(shù)T沒有限定類型,因此類型擦除后將會變成如下代碼:
- public class Pair {
- public Pair(Object first,Object second) { this.first = first; this.second = second; }
- public Object getFirst() { return first; }
- public Object getSecond() { return second; }
- public void setFirst(Object first) { this.first = first; }
- public void setSecond(Object second) { this.second = second; }
- private Object first;
- private Object second;
- }
因此盡管在調(diào)用Pair時,傳遞的類型參數(shù)有所不同,如String、Date,但是在類型擦除之后,他們將成為相同的類型。如果類型參數(shù)存在多個限定類型,則取***個限定類型作為擦除后的類型參數(shù),見如下代碼:
- public class Interval<T extends Comparable & Serializable> implements Serializable {
- public Interval(T first, T second) {
- if (first.compareTo(second) <= 0) {
- lower = first;
- upper = second;
- } else {
- lower = second;
- uppper = first;
- }
- }
- private T lower;
- private T upper;
- }
擦除類型信息后的原始類型如下:
- public class Interval implements Serializable {
- public Interval(Comparable first, Comparable second) {
- if (first.compareTo(second) <= 0) {
- lower = first;
- upper = second;
- } else {
- lower = second;
- uppper = first;
- }
- }
- private Comparable lower;
- private Comparable upper;
- }
5. 泛型類向遺留代碼的兼容:由于編譯器自動完成了類型信息的擦除,因此在原有調(diào)用原始類型的地方,可以直接傳入等價的泛型類,只要保證該泛型類在類型擦除后可以符合被調(diào)用函數(shù)參數(shù)的語法要求即可,見如下代碼:
- public class TestMain {
- public static void test(MyClass t) {
- System.out.println(t.getValue());
- }
- public static void main(String[] args) {
- MyClass<Integer> v = new MyClass<Integer>(5);
- test(v);
- }
- }
- class MyClass<T> {
- public MyClass(T t) {
- this.t = t;
- }
- public T getValue() { return t;}
- private T t;
- }
6. 約束與局限性:
1) 不能使用原始類型作為類型參數(shù),如int、double等,因為他們和Object之間沒有直接的繼承關(guān)系,因此在需要時只能使用包裝類,如Integer、Double分別予以替換,不能這樣的替換確實也帶來了效率上的折損,C++中沒有這樣的限制,因此模板類的增多只會影響編譯的效率和不會影響運行時的效率。
2) 運行時的類型查詢只適用于原始類型,即if (a instanceof Pair<String>) 等價于 if (a instanceof Pair)。
3) 泛型類對象調(diào)用getClass()方法返回的Class對象都是擦除類型信息的原始Class類型,因此在做比較時,他們將為真,見如下代碼:
- public class TestMain {
- public static void main(String[] args) {
- MyClass<Integer> i = new MyClass<Integer>(5);
- MyClass<Double> d = new MyClass<Double>(5.0);
- //返回的均為MyClass.Class
- if (d.getClass() == i.getClass())
- System.out.println("Type info will be ignored here");
- }
- }
- class MyClass<T> {
- public MyClass(T t) {
- this.t = t;
- }
- public T getValue() { return t;}
- private T t;
- }
- /* 輸入結(jié)果:
- Type info will be ignored here
- */
4) 泛型類不能實現(xiàn)Throwable接口,換言之泛型類不能成為異常類,否則會導(dǎo)致編譯錯誤。
5) 不能聲明參數(shù)化類型的數(shù)組,如Pair<String>[] table = new Pair<String>[10]; 在擦除類型后將會變?yōu)镻air[] table = new Pair[10]; 因此可以執(zhí)行該轉(zhuǎn)換:Objec[] objarray = table; 由于數(shù)組可以記住元素的類型,如果此時試圖插入錯誤的類型元素,將會導(dǎo)致異常ArrayStoreException的拋出。C++中沒有該限制。
6) 不能實例化泛型類型的變量,如public Pair() { first = new T(); second = new T();},C++中不存在這樣的限制,針對以上寫法,類型T只要存在缺省的構(gòu)造函數(shù)即可。如果確實需要實例化類型參數(shù)的對象,見如下代碼:
- public static <T> Pair<T> makePair(Class<T> c1) {
- return new Pair<T>(c1.newInstance(),c1.newInstance());
- }
- public static void main(String[] args) {
- //String.class的類型為Class<String>
- Pair<String> p = Pair.makePair(String.class);
- }
這里主要是利用Class類型本身也是泛型類型,可以利用Class<T>的類型參數(shù)推演出Pair<T>中T的類型。同樣的道理帶有類型參數(shù)的數(shù)組對象也不能直接創(chuàng)建,需要利用Array的反射機制來輔助完成,見如下代碼:
- public static <T extends Comparable> T[] minmax(T[] a) {
- T[] mm = (T[])Array.newInstance(a.getClass().getComponentType(),a.length);
- //do something here based on mm
- return mm;
- }
7) 泛型類不能應(yīng)用于靜態(tài)上下文中,見如下代碼:
- public class Singleton<T> {
- public static T getInstance() { //Compilation ERROR
- return singleInstance;
- }
- private T singleInstance; //Compilation ERROR
- }
因為這樣的寫法在定義Singleton<String>和Singleton<Date>之后,由于類型擦除,將會生成唯一一個Singleton原始共享對象,事實上這并不是我們所期望的結(jié)果,在C++中沒有這樣的限制,甚至有的時候還可以利用這樣的機制針對不同類型的對象作聲明計數(shù)器用,見如下代碼:
- template<typename T>
- class MyClassCounter {
- public:
- MyClassCounter(T t) {
- _t = t;
- _counter++;
- }
- operator T() {
- return _t;
- }
- T* operator->() {
- return &_t;
- }
- int getCount() const { return _counter; }
- private:
- T _t;
- static int _counter;
- }
8) 泛型類不能同時繼承或?qū)崿F(xiàn)只是擁有不同參數(shù)類型的同一泛型類,如 public class MyClass implements Comparable<String>, Comparable<Date> {}
7. 泛型類型的繼承規(guī)則:
1) 如果 public class Manager extends Employee {},那么Pair<Employee> pe = new Pair<Manager>()將會是非常的賦值操作,會導(dǎo)致編譯錯誤。試想如下代碼引發(fā)的運行時問題。在C++中這種賦值方式同樣會導(dǎo)致編譯錯誤,因為他們在類型實例化之后就被視為完全無關(guān)的兩個類型。
- public void test() {
- Pair<Manager> manager = new Pair<Manager>();
- Pair<Employee> employee = manager; //compilation error
- employee.setFirst(otherEmployeeButNotManager); //employee的另外一個子類,但不是Manager。
- }
2) 數(shù)組由于在運行時會記住元素的類型,因此數(shù)組可以完成這樣的賦值,如Manager[] manager = {}; Employee[] employee = manager;如果賦值之后出現(xiàn)錯誤的元素賦值將會引發(fā)ArrayStoreException異常。
3) 泛型類型可以直接賦值給擦除類型后的原始類型,但是同樣也會出現(xiàn)2)中數(shù)組賦值的問題,只是觸發(fā)的異常改為ClassCastException,見如下代碼:
- public void test() {
- Pair<Manager> manager = new Pair<Manager>();
- Pair rawType = manager;
- rawType.setFirst("Hello"); //only compilation warning, but will encounter runtime error.
- }
4) 如果 public class ArrayList<Manager> extends List<Manager> {}, 那么從ArrayList<Manager>到List<Manager>的賦值是允許的,這一點和普通類型是一致的,該規(guī)則同樣適用于C++。
8. 泛型類型的通配符類型:該泛型特征在標(biāo)準(zhǔn)C++中完全不被支持,C#中存在類似的特征。見以下代碼:
1) 子類型限定:
- public class Manager extends Employee {}
- public static void printBuddies(Pair<Employee> p) {
- }
- public static void main(String[] args) {
- printBuddies(new Pair<Employee>()); //legal
- printBuddies(new Pair<Manager>()); //illegal;
- }
但是如果將printBuddies改為:void printBuddies(Pair<? extends Employee> p),上例中main函數(shù)將可以通過編譯。<? extends Employee>的語義為所有Employee的子類都可以做printBuddies函數(shù)參數(shù)的類型參數(shù)。對于7-1)中的示例代碼,如果改為通配符類型將可以通過編譯并正常運行,但是仍然存在一定的限制,見如下代碼:
- public void test() {
- Pair<Manager> manager = new Pair<Manager>();
- Pair<? extends Employee> employee = manager; //legal here.
- //由于otherEmployeeButNotManager雖為Employee子類,但可能并非Manager類,
- //由于setFirst的參數(shù)將會聲明為void setFirst(? extends Employee),由于
- //編譯器無法確定setFirst參數(shù)的實際類型,因此將會直接報告編譯錯誤。
- employee.setFirst(otherEmployeeButNotManager); //compilation error
- }
和setFirst相比,getFirst將會正常編譯并運行,因為返回值無論是什么子類型,都不會帶來影響和破壞。
2) 超類型限定:Pair<? super Manager>表示參數(shù)類型一定是Manager的超類。因此和子類型限定剛好相反,setFirst將是合法的,而getFirst將會產(chǎn)生編譯錯誤。
3) 無限定通配符,如Pair<?>,該泛型類型的setFirst(?)方法不能被調(diào)用,即便傳入的參數(shù)是Object,這樣是Pair<?>和Pair之間***的差異。該方法還有一個比較重要的作用就是用于提示泛型函數(shù)的調(diào)用者,該泛型函數(shù)更期望參數(shù)是帶有類型參數(shù)的泛型類型,而不是原始類型,即便原始類型也可能正常的工作,見如下代碼:
- public class TestMain {
- @SuppressWarnings("unchecked")
- //public static <T> T print(MyClass myclass),同樣可以正常的工作,
- //但是會有編譯警告產(chǎn)生。
- public static <T> T print(MyClass<?> myclass) {
- myclass.print();
- return (T)myclass.get();
- }
- public static void main(String[] args) {
- Integer ii = new Integer(5);
- print(new MyClass<Integer>(ii));
- }
- }
- class MyClass<T> {
- public MyClass(T t) {
- _t = t;
- }
- T get() { return _t;}
- public void print() {
- System.out.println(_t);
- }
- private T _t;
- }
原文鏈接:http://www.cnblogs.com/stephen-liu74/archive/2011/08/15/2136859.html
【系列文章】