建議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類是“類中類”,也就有預示着它有很多特殊的地方:
- 無構造函數:Java中的類一般都有構造函數,用於創建實例對象,但是Class類卻沒有構造函數,不能實例化,Class對象是在加載類時由Java虛擬機通過調用類加載器中的difineClass方法自動構造的。
- 可以描述基本類型:雖然8個基本類型在JVM中並不是一個對象,它們一般存在於棧內存中,但是Class類仍然可以描述它們,例如可以使用int.class表示int類型的類對象。
- 其對象都是單例模式:一個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對象有三種途徑:
- 類屬性方式:如String.class
- 對象的getClass方法,如new String().getClass()
- forName方法加載:如Class.forName(" java.lang.String")
獲得了Class對象后,就可以通過getAnnotations()獲得注解,通過getMethods()獲得方法,通過getConstructors()獲得構造函數等,這位后續的反射代碼鋪平了道路。