程序師世界是廣大編程愛好者互助、分享、學習的平台,程序師世界有你更精彩!
首頁
編程語言
C語言|JAVA編程
Python編程
網頁編程
ASP編程|PHP編程
JSP編程
數據庫知識
MYSQL數據庫|SqlServer數據庫
Oracle數據庫|DB2數據庫
 程式師世界 >> 編程語言 >> JAVA編程 >> JAVA綜合教程 >> 編寫高質量代碼:改善Java程序的151個建議(第7章:泛型和反射___建議93~97),java151

編寫高質量代碼:改善Java程序的151個建議(第7章:泛型和反射___建議93~97),java151

編輯:JAVA綜合教程

編寫高質量代碼:改善Java程序的151個建議(第7章:泛型和反射___建議93~97),java151


  泛型可以減少強制類型的轉換,可以規范集合的元素類型,還可以提高代碼的安全性和可讀性,正式因為有這些優點,自從Java引入泛型後,項目的編碼規則上便多了一條:優先使用泛型。

  反射可以“看透” 程序的運行情況,可以讓我們在運行期知曉一個類或實例的運行狀況,可以動態的加載和調用,雖然有一定的性能憂患,但它帶給我們的遍歷遠遠大於其性能缺陷。

建議93:Java的泛型是可以擦除的

  Java泛型(Generic) 的引入加強了參數類型的安全性,減少了類型的轉換,它與C++中的模板(Temeplates) 比較類似,但是有一點不同的是:Java的泛型在編譯器有效,在運行期被刪除,也就是說所有的泛型參數類型在編譯後會被清除掉,我們來看一個例子,代碼如下:

 1 public class Foo {
 2     //arrayMethod接收數組參數,並進行重載
 3     public void arrayMethod(String[] intArray) {
 4 
 5     }
 6 
 7     public void arrayMethod(Integer[] intArray) {
 8 
 9     }
10     //listMethod接收泛型List參數,並進行重載
11     public void listMethod(List<String> stringList) {
12 
13     }
14     public void listMethod(List<Integer> intList) {
15         
16     }
17 }

  程序很簡單,編寫了4個方法,arrayMethod方法接收String數組和Integer數組,這是一個典型的重載,listMethod接收元素類型為String和Integer的list變量。現在的問題是,這段程序是否能編譯?如果不能?問題出在什麼地方?

  事實上,這段程序時無法編譯的,編譯時報錯信息如下:

  

  這段錯誤的意思:簡單的的說就是方法簽名重復,其實就是說listMethod(List<Integer> intList)方法在編譯時擦除類型後是listMethod(List<E> intList)與另一個方法重復。這就是Java泛型擦除引起的問題:在編譯後所有的泛型類型都會做相應的轉化。轉換規則如下:

  • List<String>、List<Integer>、List<T>擦除後的類型為List
  • List<String>[] 擦除後的類型為List[].
  • List<? extends E> 、List<? super E> 擦除後的類型為List<E>.
  • List<T extends Serializable & Cloneable >擦除後的類型為List< Serializable>.

  明白了這些規則,再看如下代碼:

public static void main(String[] args) {
        List<String> list = new ArrayList<String>();
        list.add("abc");
        String str = list.get(0);
    }

  進過編譯後的擦除處理,上面的代碼和下面的程序時一致的:

public static void main(String[] args) {
        List list = new ArrayList();
        list.add("abc");
        String str = (String) list.get(0);
    }

  Java編譯後字節碼中已經沒有泛型的任何信息了,也就是說一個泛型類和一個普通類在經過編譯後都指向了同一字節碼,比如Foo<T>類,經過編譯後將只有一份Foo.class類,不管是Foo<String>還是Foo<Integer>引用的都是同一字節碼。Java之所以如此處理,有兩個原因:

  • 避免JVM的大換血。C++泛型生命期延續到了運行期,而Java是在編譯期擦除掉的,我們想想,如果JVM也把泛型類型延續到運行期,那麼JVM就需要進行大量的重構工作了。
  • 版本兼容:在編譯期擦除可以更好的支持原生類型(Raw Type),在Java1.5或1.6...平台上,即使聲明一個List這樣的原生類型也是可以正常編譯通過的,只是會產生警告信息而已。

  明白了Java泛型是類型擦除的,我們就可以解釋類似如下的問題了:

public static void main(String[] args) {
        List<String> list = new ArrayList<String>();
        List<Integer> list2 = new ArrayList<Integer>();
        System.out.println(list.getClass()==list2.getClass());
    }

  以上代碼返回true,原因很簡單,List<String>和List<Integer>擦除後的類型都是List,沒有任何區別。

  2.泛型數組初始化時不能聲明泛型,如下代碼編譯時通不過: 

List<String>[] listArray = new List<String>[];

  原因很簡單,可以聲明一個帶有泛型參數的數組,但不能初始化該數組,因為執行了類型擦除操作,List<Object>[]與List<String>[] 就是同一回事了,編譯器拒絕如此聲明。

  3.instanceof不允許存在泛型參數

    以下代碼不能通過編譯,原因一樣,泛型類型被擦除了:   

    List<String> list = new ArrayList<String>();
    System.out.println(list instanceof List<String>);

建議94:不能初始化泛型參數和數組

  泛型類型在編譯期被擦除,我們在類初始化時將無法獲得泛型的具體參數,比如這樣的代碼: 

class Test<T> {
    private T t = new T();
    private T[] tArray = new T[5];
    private List<T> list = new ArrayList<T>();
}

  這段代碼有神麼問題呢?t、tArray、list都是類變量,都是通過new聲明了一個類型,看起來非常相似啊!但這段代碼是編譯不過的,因為編譯器在編譯時需要獲得T類型,但泛型在編譯期類型已經被擦除了,所有new T()和 new T[5]都會報錯(有人可能會有疑問,泛型類型可以擦除為頂級Object,那T類型擦除成Object不就可以編譯了嗎?這樣也不行,泛型只是Java語言的一部分,Java語言畢竟是一個強類型、編譯型的安全語言,要確保運行期的穩定性和安全性就必須要求在編譯器上嚴格檢查)。可為什麼new ArrayList<T>()卻不會報錯呢?

  這是因為ArrayList表面是泛型,其實已經在編譯期轉為Object了,我們來看一下ArrayList的源代碼就清楚了,代碼如下: 

 1 public class ArrayList<E> extends AbstractList<E> implements List<E>,
 2         RandomAccess, Cloneable, java.io.Serializable {
 3     // 容納元素的數組
 4     private transient Object[] elementData;
 5 
 6     // 構造函數
 7     public ArrayList() {
 8         this(10);
 9     }
10 
11     // 獲得一個元素
12     public E get(int index) {
13         rangeCheck(index);
14         // 返回前強制類型轉換
15         return elementData(index);
16     }
17     /* 其它代碼略 */
18 
19 }

  注意看elementData的定義,它容納了ArrayList的所有元素,其類型是Object數組,因為Object是所有類的父類,數組又允許協變(Covariant),因此elementData數組可以容納所有的實例對象。元素加入時向上轉型為Object類型(E類型轉換為Object),取出時向下轉型為E類型,如此處理而已。

  在某些情況下,我們需要泛型數組,那該如何處理呢?代碼如下:

 1 class Test<T> {
 2     // 不再初始化,由構造函數初始化
 3     private T t;
 4     private T[] tArray;
 5     private List<T> list = new ArrayList<T>();
 6 
 7     // 構造函數初始化
 8     public Test() {
 9         try {
10             Class<?> tType = Class.forName("");
11             t = (T) tType.newInstance();
12             tArray = (T[]) Array.newInstance(tType, 5);
13         } catch (Exception e) {
14             e.printStackTrace();
15         }
16     }
17 }

  此時,運行就沒有什麼問題了,剩下的問題就是怎麼在運行期獲得T的類型,也就是tType參數,一般情況下泛型類型是無法獲取的,不過,在客戶端調用時多傳輸一個T類型的class就會解決問題。

  類的成員變量是在類初始化前初始化的,所以要求在初始化前它必須具有明確的類型,否則就只能聲明,不能初始化。

建議95:強制聲明泛型的實際類型

  Arrays工具類有一個方法asList可以把一個變長參數或數組轉變為列表,但是它有一個缺點:它所生成的list長度是不可變的,而這在我們的項目開發中有時會很不方便。如果你期望生成的列表長度可變,那就需要自己來寫一個數組的工具類了,代碼如下:

1 class ArrayUtils {
2     // 把一個變長參數轉化為列表,並且長度可變
3     public static <T> List<T> asList(T... t) {
4         List<T> list = new ArrayList<T>();
5         Collections.addAll(list, t);
6         return list;
7     }
8 }

  這很簡單,與Arrays.asList的調用方式相同,我們傳入一個泛型對象,然後返回相應的List,代碼如下:

public static void main(String[] args) {
        // 正常用法
        List<String> list1 = ArrayUtils.asList("A", "B");
        // 參數為空
        List list2 = ArrayUtils.asList();
        // 參數為整型和浮點型的混合
        List list3 = ArrayUtils.asList(1, 2, 3.1);
    }

  這裡有三個變量需要說明:

(1)、變量list1:變量list1是一個常規用法,沒有任何問題,泛型實際參數類型是String,返回結果就是一個容納String元素的List對象。

(2)、變量list2:變量list2它容納的是什麼元素呢?我們無法從代碼中推斷出list2列表到底容納的是什麼元素(因為它傳遞的參數是空,編譯器也不知道泛型的實際參數類型是什麼),不過,編譯器會很聰明地推斷出最頂層類Object就是其泛型類型,也就是說list2的完整定義如下:

List<Object> list2 = ArrayUtils.asList();

    如此一來,編譯器就不會給出" unchecked "警告了。現在新的問題又出現了:如果期望list2是一個Integer類型的列表,而不是Object列表,因為後續的邏輯會把Integer類型加入到list2中,那該如何處理呢?

    強制類型轉換(把asList強制轉換成List<Integer>)?行不通,雖然Java泛型是編譯期擦出的,但是List<Object>和List<Integer>沒有繼承關系,不能強制轉換。  

    重新聲明一個List<Integer>,然後讀取List<Object>元素,一個一個地向下轉型過去?麻煩,而且效率又低。

        最好的解決辦法是強制聲明泛型類型,代碼如下: 

List<Integer> intList = ArrayUtils.<Integer>asList();

  就這麼簡單,asList方法要求的是一個泛型參數,那我們就在輸入前定義這是一個Integer類型的參數,當然,輸出也是Integer類型的集合了。

(3)、變量list3:變量list3有兩種類型的元素:整數類型和浮點類型,那它生成的List泛型化參數應該是什麼呢?是Integer和Float的父類Number?你太高看編譯器了,它不會如此推斷的,當它發現多個元素的實際類型不一致時就會直接確認泛型類型是Object,而不會去追索元素的公共父類是什麼,但是對於list3,我們更期望它的泛型參數是Number,都是數字嘛,參照list2變量,代碼修改如下:

List<Number> list3 = ArrayUtils.<Number>asList(1, 2, 3.1);

  Number是Integer和Float的父類,先把三個輸入參數、輸出參數同類型,問題是我們要在什麼時候明確泛型類型呢?一句話:無法從代碼中推斷出泛型的情況下,即可強制聲明泛型類型。

建議96:不同的場景使用不同的泛型通配符

  Java泛型支持通配符(Wildcard),可以單獨使用一個“?”表示任意類,也可以使用extends關鍵字表示某一個類(接口)的子類型,還可以使用super關鍵字表示某一個類(接口)的父類型,但問題是什麼時候該用extends,什麼該用super呢?

(1)、泛型結構只參與 “讀” 操作則限定上界(extends關鍵字)

  閱讀如下代碼,想想看我們的業務邏輯操作是否還能繼續:

public static <E> void read(List<? super E> list) {
        for (Object obj : list) {
            // 業務邏輯操作
        }
    }

  從List列表中讀取元素的操作(比如一個數字列表中的求和計算),你覺得方法read能繼續寫下去嗎?

  答案是:不能,我們不知道list到底存放的是什麼元素,只能推斷出E類型是父類,但問題是E類型的父類又是什麼呢?無法再推斷,只有運行期才知道,那麼編碼器就無法操作了。當然,你可以把它當做是Object類來處理,需要時再轉換成E類型---這完全違背了泛型的初衷。在這種情況下,“讀” 操作如果期望從List集合中讀取數據就需要使用extends關鍵字了,也就是要界定泛型的上界,代碼如下:

public static <E> void read(List<? extends E> list) {
        for (E e : list) {
            // 業務邏輯操作
        }
    }

  此時,已經推斷出List集合中取出的元素時E類型的元素。具體是什麼類型的元素就要等到運行期才確定了,但它一定是一個確定的類型,比如read(Arrays.asList("A"))調用該方法時,可以推斷出List中的元素類型是String,之後就可以對List中的元素進行操作了。如加入到另外的List<E>中,或者作為Map<E,V>的鍵等。

(2)、泛型結構只參與“寫” 操作則限定下界(使用super關鍵字)

  先看如下代碼能否編譯:

public static <E> void write(List<? extends Number> list){
        //加入一個元素
        list.add(123);
    }

  編譯失敗,失敗的原因是list中的元素類型不確定,也就是編譯器無法推斷出泛型類型到底是什麼,是Integer類型?是Double?還是Byte?這些都符合extends關鍵字的定義,由於無法確定實際的泛型類型,所以編譯器拒絕了此類操作。

  在此種情況下,只有一個元素時可以add進去的:null值,這是因為null是一個萬用類型,它可以是所有類的實例對象,所以可以加入到任何列表中。

  Object是否可以?不可以,因為它不是Number子類,而且即使把List變量修改為List<? extends Object> 類型也不能加入,原因很簡單,編譯器無法推斷出泛型類型,加什麼元素都是無效的。

  在這種“寫”的操作的情況下,使用super關鍵字限定泛型的下界才是正道,代碼如下:

public static <E> void write(List<? super Number> list){
        //加入元素
        list.add(123);
        list.add(3.14);
    }

  甭管它是Integer的123,還是浮點數3.14,都可以加入到list列表中,因為它們都是Number的類型,這就保證了泛型類的可靠性。

  對於是要限定上界還是限定下界,JDK的Collections.copy方法是一個非常好的例子,它實現了把源列表的所有元素拷貝到目標列表中對應的索引位置上,代碼如下:

 1     public static <T> void copy(List<? super T> dest, List<? extends T> src) {
 2         int srcSize = src.size();
 3         if (srcSize > dest.size())
 4             throw new IndexOutOfBoundsException("Source does not fit in dest");
 5 
 6         if (srcSize < COPY_THRESHOLD ||
 7             (src instanceof RandomAccess && dest instanceof RandomAccess)) {
 8             for (int i=0; i<srcSize; i++)
 9                 dest.set(i, src.get(i));
10         } else {
11             ListIterator<? super T> di=dest.listIterator();
12             ListIterator<? extends T> si=src.listIterator();
13             for (int i=0; i<srcSize; i++) {
14                 di.next();
15                 di.set(si.next());
16             }
17         }
18     }

  源列表是用來提供數據的,所以src變量需要界定上界,要有extends關鍵字。目標列表是用來寫數據的,所以dest變量需要界定下界,帶有super關鍵字。

  如果一個泛型結構既用作 “讀” 操作又用作“寫操作”,那該如何進行限定呢?不限定,使用確定的泛型類型即可,如List<E>.

建議97:警惕泛型是不能協變和逆變的

  什麼叫協變和逆變?

  在編程語言的類型框架中,協變和逆變是指寬類型和窄類型在某種情況下(如參數、泛型、返回值)替換或交換的特性,簡單的說,協變是一個窄類型替換寬類型,而逆變則是用寬類型覆蓋窄類型。其實,在Java中協變和逆變我們已經用了很久了,只是我們沒發覺而已,看如下代碼:

class Base {
    public Number doStuff() {
        return 0;
    }
}

class Sub extends Base {
    @Override
    public Integer doStuff() {
        return 0;
    }
}

  子類的doStuff方法返回值的類型比父類方法要窄,此時doStuff方法就是一個協變方法,同時根據Java的覆寫定義來看,這又屬於覆寫。那逆變是怎麼回事呢?代碼如下: 

class Base {
    public void doStuff(Integer i) {
        
    }
}

class Sub extends Base {
    @Override
    public void doStuff(Number n) {
      
    }
}

   子類的doStuff方法的參數類型比父類要寬,此時就是一個逆變方法,子類擴大了父類方法的輸入參數,但根據覆寫的定義來看,doStuff不屬於覆寫,只是重載而已。由於此時的doStuff方法已經與父類沒有任何關系了,只是子類獨立擴展出的一個行為,所以是否聲明為doStuff方法名意義不大,逆變已經不具有特別的意義了,我們重點關注一下協變,先看如下代碼是否是協變:

    public static void main(String[] args) {
        Base base = new Sub();
    }

  base變量是否發生了協變?是的,發生了協變,base變量是Base類型,它是父類,而其賦值卻是在子類實例,也就是用窄類型覆蓋了寬類型。這也叫多態,兩者同含義。

  說了這麼多,下面再再來想想泛型是否支持協變和逆變呢,答案是:泛型既不支持協變,也不支持逆變。為什麼會不支持呢?

(1)、泛型不支持協變:數組和泛型很相似,一個是中括號,一個是尖括號,那我們就以數組為參照對象,看如下代碼:

    public static void main(String[] args) {
        //數組支持協變
        Number [] n = new Integer[10];
        //編譯不通過,泛型不支持協變
        List<Number> list = new ArrayList<Integer>();
    }

  ArrayList是List的子類型,Integer是Number的子類型,裡氏替換原則在此行不通了,原因就是Java為了保證運行期的安全性,必須保證泛型參數的類型是固定的,所以它不允許一個泛型參數可以同時包含兩種類型,即使是父子類關系也不行。

  泛型不支持協變,但可以使用通配符模擬協變,代碼如下:

        //Number子類型(包括Number類型) 都可以是泛型參數類型
        List<? extends Number> list = new ArrayList<Integer>();

 " ? extends Number " 表示的意思是,允許Number的所有子類(包括自身) 作為泛型參數類型,但在運行期只能是一個具體類型,或者是Integer類型,或者是Double類型,或者是Number類型,也就是說通配符只在編碼期有效,運行期則必須是一個確定的類型。

(2)、泛型不支持逆變

  java雖然允許逆變存在,但在對類型賦值上是不允許逆變的,你不能把一個父類實例對象賦給一個子類類型變量,泛型自然也不允許此種情況發生了。但是它可以使用super關鍵字來模擬實現,代碼如下:

        //Integer的父類型(包括Integer)都可以是泛型參數類型
        List<? super Integer> list = new ArrayList<Number>();

  " ? super Integer " 的意思是可以把所有的Integer父類型(自身、父類或接口) 作為泛型參數,這裡看著就像是把一個Number類型的ArrayList賦值給了Integer類型的List,其外觀類似於使用一個寬類型覆蓋一個窄類型,它模擬了逆變的實現。

  泛型既不支持協變,也不支持逆變,帶有泛型參數的子類型定義與我們經常使用的類類型也不相同,其基本類型關系如下表所示:

泛型通配符QA Integer是Number的子類型? 正確 ArrayList<Integer> 是List<Integer> 的子類型? 正確 Integer[]是 Number[]的子類型? 正確 List<Integer> 是 List<Number> 的子類型? 錯誤 List<Integer> 是 List<? extends  Integer> 的子類型? 錯誤 List<Integer> 是 List<? super  Integer> 的子類型? 錯誤                                                      Java的泛型是不支持協變和逆變的,只是能夠實現逆變和協變

  1. 上一頁:
  2. 下一頁:
Copyright © 程式師世界 All Rights Reserved