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

建議98:建議的採用順序是List中泛型順序依次爲T、?、Object

  List<T>、List<?>、List<Object>這三者均可以容納全部的對象,但使用的順序應該是首選List<T>,次之List<?>,最後選擇List<Object>,緣由以下:java

(1)、List<T>是肯定的某一個類型數組

  List<T>表示的是List集合中的元素都爲T類型,具體類型在運行期決定;List<?>表示的是任意類型,與List<T>相似,而List<Object>則表示List集合中的全部元素爲Object類型,由於Object是全部類的父類,因此List<Object>也能夠容納全部的類類型,從這一字面意義上分析,List<T>更符合習慣:編碼者知道它是某一個類型,只是在運行期才肯定而已。安全

(2)List<T>能夠進行讀寫操做ide

  List<T>能夠進行諸如add,remove等操做,由於它的類型是固定的T類型,在編碼期不須要進行任何的轉型操做。函數

  List<T>是隻讀類型的,不能進行增長、修改操做,由於編譯器不知道List中容納的是什麼類型的元素,也就沒法校驗類型是否安全了,並且List<?>讀取出的元素都是Object類型的,須要主動轉型,因此它常常用於泛型方法的返回值。注意List<?>雖然沒法增長,修改元素,可是卻能夠刪除元素,好比執行remove、clear等方法,那是由於它的刪除動做與泛型類型無關。編碼

  List<Object> 也能夠讀寫操做,可是它執行寫入操做時須要向上轉型(Up cast),在讀取數據的時候須要向下轉型,而此時已經失去了泛型存在的意義了。spa

  打個比方,有一個籃子用來容納物品,好比西瓜,番茄等.List<?>的意思是說,「嘿,我這裏有一個籃子,能夠容納固定類別的東西,好比西瓜,番茄等」。List<?>的意思是說:「嘿,我有一個籃子,我能夠容納任何東西,只要是你想獲得的」。而List<Object>就更有意思了,它說" 嘿,我也有一個籃子,我能夠容納全部物質,只要你認爲是物質的東西均可以容納進來 "。code

  推而廣之,Dao<T>應該比Dao<?>、Dao<Object>更先採用,Desc<Person>則比Desc<?>、Desc<Object>更優先採用。對象

建議99:嚴格限定泛型類型採用多重界限

  從哲學來講,很難描述一個具體的人,你能夠描述他的長相、性格、工做等,可是人都是由多重身份的,估計只有使用多個And(與操做)將全部的描述串聯起來才能描述一個完整的人,好比我,上班時我是一個職員,下班了坐公交車我是一個乘客,回家了我是父母的孩子,是兒子的父親......角色時刻在變換。那若是咱們要使用Java程序來對一類人進行管理,該如何作呢?好比在公交車費優惠系統中,對部分人員(如工資低於2500元的上班族而且是站立的乘客)車費打8折,該如何實現呢?blog

  注意這裏的類型參數有兩個限制條件:一個爲上班族;二爲乘客。具體到咱們的程序中就應該是一個泛型參數具備兩個上界(Upper Bound),首先定義兩個接口及實現類,代碼以下: 

 1 interface Staff {
 2     // 工資
 3     public int getSalary();
 4 }
 5 
 6 interface Passenger {
 7     // 是不是站立狀態
 8     public boolean isStanding();
 9 }
10 //定義我這個類型的人
11 class Me implements Staff, Passenger {
12 
13     @Override
14     public boolean isStanding() {
15         return true;
16     }
17 
18     @Override
19     public int getSalary() {
20         return 2000;
21     }
22 
23 }

  "Me"這種類型的人物有不少,好比系統分析師也是一個職員,也坐公交車,但他的工資實現就和我不一樣,再好比Boss級的人物,偶爾也坐公交車,對大老闆來講他也只是一個職員,他的實現類也不一樣,也就是說若是咱們使用「T extends Me」是限定不了需求對象的,那該怎麼辦呢?能夠考慮使用多重限定,代碼以下:  

public class Client99 {
    //工資低於2500的而且站立的乘客車票打8折
    public static <T extends Staff & Passenger> void discount(T t) {
        if (t.getSalary() < 2500 && t.isStanding()) {
            System.out.println(" 恭喜您,您的車票打八折!");
        }
    }
    public static void main(String[] args) {
        discount(new Me());
    }
}

  使用「&」符號設定多重邊界,指定泛型類型T必須是Staff和Passenger的共有子類型,此時變量t就具備了全部限定的方法和屬性,要再進行判斷就一如反掌了。在Java的泛型中,可使用"&"符號關聯多個上界並實現多個邊界限定,並且只有上界纔有此限定,下界沒有多重限定的狀況。想一想你就會明白:多個下界,編碼者可自行推斷出具體的類型,好比「? super Integer」 和 「? extends Double」,能夠更細化爲Number類型了,或者Object類型了,無需編譯器推斷了。

  爲何要說明多重邊界?是由於編碼者太少使用它了,好比一個判斷用戶權限的方法,使用的是策略模式(Strategy Pattern) ,示意代碼以下:

 1 class UserHandler<T extends User> {
 2     // 判斷用戶是否有權限執行操做
 3     public boolean permit(T user, List<Job> jobs) {
 4         List<Class<?>> iList = Arrays.asList(user.getClass().getInterfaces());
 5         // 判斷 是不是管理員
 6         if (iList.indexOf(Admin.class) > -1) {
 7             Admin admin = (Admin) user;
 8             // 判斷管理員是否有此權限
 9         } else {
10             // 判斷普通用戶是否有此權限
11         }
12         return false;
13     }
14 }
15 
16 class User {}
17 
18 class Job {}
19 
20 class Admin extends User {}

  此處進行了一次泛型參數類別判斷,這裏不只僅違背了單一職責原則(Single Responsibility Principle),並且讓泛型很「汗顏」 :已經使用了泛型限定參數的邊界了,還要進行泛型類型判斷。事實上,使用多重邊界能夠很方便的解決此問題,並且很是優雅,建議你們 在開發中考慮使用多重限定。

建議100:數組的真實類型必須是泛型類型的子類型

  List接口的toArray方法能夠把一個集合轉化爲數組,可是使用不方便,toArray()方法返回的是一個Object數組,因此須要自行轉變。toArray(T[] a)雖然返回的是T類型的數組,可是還須要傳入一個T類型的數組,這也挺麻煩的,咱們指望輸入的是一個泛型化的List,這樣就能轉化爲泛型數組了,來看看能不能實現,代碼以下:

    public static <T> T[] toArray(List<T> list) {
        T[] t = (T[]) new Object[list.size()];
        for (int i = 0, n = list.size(); i < n; i++) {
            t[i] = list.get(i);
        }
        return t;
    }

  上面要輸出的參數類型定義爲Object數組,而後轉型爲T類型數組,以後遍歷List賦值給數組的每一個元素,這與ArrayList的toArray方法很相似(注意只是相似),客戶端的調用以下:

public static void main(String[] args) {
        List<String> list = Arrays.asList("A","B");
        for(String str :toArray(list)){
            System.out.println(str);
        }
    }

  編譯沒有任何問題,運行後出現以下異常:  

Exception in thread "main" java.lang.ClassCastException: [Ljava.lang.Object; cannot be cast to [Ljava.lang.String;
    at com.study.advice100.Client100.main(Client100.java:16)

  類型轉換異常,也就是說不能把一個Object數組轉換爲String數組,這段異常包含了兩個問題:

  • 爲何Object數組不能向下轉型爲String數組:數組是一個容器,只有確保容器內的全部元素類型與指望的類型有父子關係時才能轉換,Object數組只能保證數組內的元素時Object類型,卻不能確保它們都是String的父類型或子類,因此類型轉換失敗。
  • 爲何是main方法拋出異常,而不是toArray方法:其實,是在toArray方法中進行的類型向下轉換,而不是main方法中。那爲何異常會在main方法中拋出,應該在toArray方法的「 T[] t = (T[]) new Object[list.size()];」這段代碼纔對呀?那是由於泛型是類型擦除的,toArray方法通過編譯後與以下代碼相同:  
    public static Object[] toArrayTwo(List list) {
        // 此處的強制類型轉換不必存在,只是爲了與源代碼對比
        Object[] t = (Object[]) new Object[list.size()];
        for (int i = 0, n = list.size(); i < n; i++) {
            t[i] = list.get(i);
        }
        return t;
    }

    public static void main(String[] args) {
        List<String> list = Arrays.asList("A", "B");
        for (String str : (String [])toArrayTwo(list)) {
            System.out.println(str);
        }
    }

  閱讀完此段代碼後就很清楚了:toArray方法返回後進行一次類型轉換,Object數組轉換成了String數組,因而就報ClassCastException異常了。

  Object數組不能轉爲String數組,T類型又沒法在運行期得到,那該如何解決這個問題呢?其實,要想把一個Object數組轉換爲String數組,只要Object數組的實際類型也就是String就能夠了,例如: 

       // objArray的實際類型和表面類型都是String數組
        Object[] objArray = { "A", "B" };
        // 拋出ClassCastException
        String[] strArray = (String[]) objArray;

        String[] ss = { "A", "B" };
        //objs的真實類型是String數組,顯示類型爲Object數組
        Object objs[] =ss;
        //順利轉換爲String數組
        String strs[]=(String[])objs;

  明白了這個問題,咱們就把泛型數組聲明爲泛型的子類型吧!代碼以下:

    public static <T> T[] toArray(List<T> list,Class<T> tClass) {
        //聲明並初始化一個T類型的數組
        T[] t = (T[])Array.newInstance(tClass, list.size());
        for (int i = 0, n = list.size(); i < n; i++) {
            t[i] = list.get(i);
        }
        return t;
    }

  經過反射類Array聲明瞭一個T類型的數組,因爲咱們沒法在運行期得到泛型類型的參數,所以就須要調用者主動傳入T參數類型。此時,客戶端再調用就不會出現任何異常了。

  在這裏咱們看到,當一個泛型類(特別是泛型集合)轉變爲泛型數組時,泛型數組的真實類型不能是泛型的父類型(好比頂層類Object),只能是泛型類型的子類型(固然包括自身類型),不然就會出現類型轉換異常。

建議101:注意Class類的特殊性

  Java語言是先把Java源文件編譯成後綴爲class的字節碼文件,而後再經過ClassLoader機制把這些類文件加載到內存中,最後生成實例執行的,這是Java處理的基本機制,可是加載到內存中的數據的如何描述一個類的呢?好比在Dog.class文件中定義一個Dog類,那它在內存中是如何展示的呢?

  Java使用一個元類(MetaClass)來描述加載到內存中的類數據,這就是Class類,它是一個描述類的類對象,好比Dog.class文件加載到內存中後就會有一個class的實例對象描述之。由於是Class類是「類中類」,也就有預示着它有不少特殊的地方:

  1. 無構造函數:Java中的類通常都有構造函數,用於建立實例對象,可是Class類卻沒有構造函數,不能實例化,Class對象是在加載類時由Java虛擬機經過調用類加載器中的difineClass方法自動構造的。
  2. 能夠描述基本類型:雖然8個基本類型在JVM中並非一個對象,它們通常存在於棧內存中,可是Class類仍然能夠描述它們,例如可使用int.class表示int類型的類對象。
  3. 其對象都是單例模式:一個Class的實例對象描述一個類,而且只描述一個類,反過來也成立。一個類只有一個Class實例對象,以下代碼返回的結果都爲true: 
        // 類的屬性class所引用的對象與實例對象的getClass返回值相同
        boolean b1=String.class.equals(new String().getClass());
        boolean b2="ABC".getClass().equals(String.class);
        // class實例對象不區分泛型
        boolean b3=ArrayList.class.equals(new ArrayList<String>().getClass());

  Class類是Java的反射入口,只有在得到了一個類的描述對象後才能動態的加載、調用,通常得到一個Class對象有三種途徑:

  1. 類屬性方式:如String.class
  2. 對象的getClass方法,如new String().getClass()
  3. forName方法加載:如Class.forName(" java.lang.String")

   得到了Class對象後,就能夠經過getAnnotations()得到註解,經過getMethods()得到方法,經過getConstructors()得到構造函數等,這位後續的反射代碼鋪平了道路。

相關文章
相關標籤/搜索