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

阿赫瓦里發表於2016-10-08

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

  List<T>、List<?>、List<Object>這三者都可以容納所有的物件,但使用的順序應該是首選List<T>,次之List<?>,最後選擇List<Object>,原因如下:

(1)、List<T>是確定的某一個型別

  List<T>表示的是List集合中的元素都為T型別,具體型別在執行期決定;List<?>表示的是任意型別,與List<T>類似,而List<Object>則表示List集合中的所有元素為Object型別,因為Object是所有類的父類,所以List<Object>也可以容納所有的類型別,從這一字面意義上分析,List<T>更符合習慣:編碼者知道它是某一個型別,只是在執行期才確定而已。

(2)List<T>可以進行讀寫操作

  List<T>可以進行諸如add,remove等操作,因為它的型別是固定的T型別,在編碼期不需要進行任何的轉型操作。

  List<T>是隻讀型別的,不能進行增加、修改操作,因為編譯器不知道List中容納的是什麼型別的元素,也就無法校驗型別是否安全了,而且List<?>讀取出的元素都是Object型別的,需要主動轉型,所以它經常用於泛型方法的返回值。注意List<?>雖然無法增加,修改元素,但是卻可以刪除元素,比如執行remove、clear等方法,那是因為它的刪除動作與泛型型別無關。

  List<Object> 也可以讀寫操作,但是它執行寫入操作時需要向上轉型(Up cast),在讀取資料的時候需要向下轉型,而此時已經失去了泛型存在的意義了。

  打個比方,有一個籃子用來容納物品,比如西瓜,番茄等.List<?>的意思是說,“嘿,我這裡有一個籃子,可以容納固定類別的東西,比如西瓜,番茄等”。List<?>的意思是說:“嘿,我有一個籃子,我可以容納任何東西,只要是你想得到的”。而List<Object>就更有意思了,它說" 嘿,我也有一個籃子,我可以容納所有物質,只要你認為是物質的東西都可以容納進來 "。

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

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

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

  注意這裡的型別引數有兩個限制條件:一個為上班族;二為乘客。具體到我們的程式中就應該是一個泛型引數具有兩個上界(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()獲得建構函式等,這位後續的反射程式碼鋪平了道路。

相關文章