1. 程式人生 > >Java泛型深入理解

Java泛型深入理解

此外 都沒有 操作 方法調用 length 整形 推薦 如何使用 連接



泛型之前


在面向對象編程語言中,多態算是一種泛化機制。例如,你可以將方法的參數類型設置為基類,那麽該方法就可以接受從這個基類中導出的任何類作為參數,這樣的方法將會更具有通用性。此外,如果將方法參數聲明為接口,將會更加靈活。

在Java增加泛型類型之前,通用程序的設計就是利用繼承實現的,例如,ArrayList類只維護一個Object引用的數組,Object為所有類基類。


[java] view plain copy
public class BeforeGeneric {
static class ArrayList{//泛型之前的通用程序設計
private Object[] elements=new Object[0];

public Object get(int i){
return elements[i];
}
public void add(Object o){
//這裏的實現,只是為了演示,不具有任何參考價值
int length=elements.length;
Object[] newElments=new Object[length+1];
for(int i=0;i<length;i++){
newElments[i]=elements[i];
}
newElments[length]=o;
elements=newElments;
}
}
public static void main(String[] args) {
ArrayList stringValues=new ArrayList();
stringValues.add(1);//可以向數組中添加任何類型的對象
//問題1——獲取值時必須強制轉換
String str=(String) stringValues.get(0);
//問題2——上述強制轉型編譯時不會出錯,而運行時報異常java.lang.ClassCastException
}
}
這樣的實現面臨兩個問題:

1、當我們獲取一個值的時候,必須進行強制類型轉換。

2、假定我們預想的是利用stringValues來存放String集合,因為ArrayList只是維護一個Object引用的數組,我們無法阻止將Integer類型(Object子類)的數據加入stringValues。然而,當我們使用數據的時候,需要將獲取的Object對象轉換為我們期望的類型(String),如果向集合中添加了非預期的類型(如Integer),編譯時我們不會收到任何的錯誤提示。但當我們運行程序時卻會報異常:

Exception in thread "main" java.lang.ClassCastException: java.lang.Integer cannot be cast to java.lang.String
at generic.BeforeGeneric.main(BeforeGeneric.java:24)

這顯然不是我們所期望的,如果程序有潛在的錯誤,我們更期望在編譯時被告知錯誤,而不是在運行時報異常。

泛型

針對利用繼承來實現通用程序設計所產生的問題,泛型提供了更好的解決方案:類型參數。例如,ArrayList類用一個類型參數來指出元素的類型。

[java] view plain copy
ArrayList<String> stringValues=new ArrayList<String>();
這樣的代碼具有更好的可讀性,我們一看就知道該集合用來保存String類型的對象,而不是僅僅依賴變量名稱來暗示我們期望的類型。

[java] view plain copy
public class GenericType {
public static void main(String[] args) {
ArrayList<String> stringValues=new ArrayList<String>();
stringValues.add("str");
stringValues.add(1); //編譯錯誤
}
}
現在,如果我們向ArrayList<String>添加Integer類型的對象,將會出現編譯錯誤。
Exception in thread "main" java.lang.Error: Unresolved compilation problem:
The method add(int, String) in the type ArrayList<String> is not applicable for the arguments (int)
at generic.GenericType.main(GenericType.java:8)

編譯器會自動幫我們檢查,避免向集合中插入錯誤類型的對象,從而使得程序具有更好的安全性。

總之,泛型通過類型參數使得我們的程序具有更好的可讀性和安全性。


Java泛型的實現原理

擦除


[java] view plain copy
public class GenericType {
public static void main(String[] args) {
ArrayList<String> arrayString=new ArrayList<String>();
ArrayList<Integer> arrayInteger=new ArrayList<Integer>();
System.out.println(arrayString.getClass()==arrayInteger.getClass());
}
}
輸出:
true

在這個例子中,我們定義了兩個ArrayList數組,不過一個是ArrayList<String>泛型類型,只能存儲字符串。一個是ArrayList<Integer>泛型類型,只能存儲整型。最後,我們通過arrayString對象和arrayInteger對象的getClass方法獲取它們的類信息並比較,發現結果為true。

這是為什麽呢,明明我們定義了兩種不同的類型?因為,在編譯期間,所有的泛型信息都會被擦除,List<Integer>和List<String>類型,在編譯後都會變成List類型(原始類型)。Java中的泛型基本上都是在編譯器這個層次來實現的,這也是Java的泛型被稱為“偽泛型”的原因。

原始類型

原始類型就是泛型類型擦除了泛型信息後,在字節碼中真正的類型。無論何時定義一個泛型類型,相應的原始類型都會被自動提供。原始類型的名字就是刪去類型參數後的泛型類型的類名。擦除類型變量,並替換為限定類型(T為無限定的類型變量,用Object替換)。

[java] view plain copy
//泛型類型
class Pair<T> {
private T value;
public T getValue() {
return value;
}
public void setValue(T value) {
this.value = value;
}
}
[java] view plain copy
//原始類型
class Pair {
private Object value;
public Object getValue() {
return value;
}
public void setValue(Object value) {
this.value = value;
}
}
因為在Pair<T>中,T是一個無限定的類型變量,所以用Object替換。如果是Pair<T extends Number>,擦除後,類型變量用Number類型替換。


[java] view plain copy
public class ReflectInGeneric {
public static void main(String[] args) throws IllegalArgumentException,
SecurityException, IllegalAccessException, InvocationTargetException, NoSuchMethodException {
ArrayList<Integer> array=new ArrayList<Integer>();
array.add(1);//這樣調用add方法只能存儲整形,因為泛型類型的實例為Integer
array.getClass().getMethod("add", Object.class).invoke(array, "asd");
for (int i=0;i<array.size();i++) {
System.out.println(array.get(i));
}
}
}
輸出:

1
asd

為什麽呢?我們在介紹泛型時指出向ArrayList<Integer>中插入String類型的對象,編譯時會報錯。現在為什麽又可以了呢?

我們在程序中定義了一個ArrayList<Integer>泛型類型,如果直接調用add方法,那麽只能存儲整形的數據。不過當我們利用反射調用add方法的時候,卻可以存儲字符串。這說明ArrayList<Integer>泛型信息在編譯之後被擦除了,只保留了原始類型,類型變量(T)被替換為Object,在運行時,我們可以行其中插入任意類型的對象。

但是,並不推薦以這種方式操作泛型類型,因為這違背了泛型的初衷(減少強制類型轉換以及確保類型安全)。當我們從集合中獲取元素時,默認會將對象強制轉換成泛型參數指定的類型(這裏是Integer),如果放入了非法的對象這個強制轉換過程就會出現異常。

泛型方法的類型推斷
在調用泛型方法的時候,可以指定泛型類型,也可以不指定。

在不指定泛型類型的情況下,泛型類型為該方法中的幾種參數類型的共同父類的最小級,直到Object。

在指定泛型類型的時候,該方法中的所有參數類型必須是該泛型類型或者其子類。


[java] view plain copy
public class Test {
public static void main(String[] args) {
/**不指定泛型的時候*/
int i=Test.add(1, 2); //這兩個參數都是Integer,所以T替換為Integer類型
Number f=Test.add(1, 1.2);//這兩個參數一個是Integer,另一個是Float,所以取同一父類的最小級,為Number
Object o=Test.add(1, "asd");//這兩個參數一個是Integer,另一個是String,所以取同一父類的最小級,為Object

/**指定泛型的時候*/
int a=Test.<Integer>add(1, 2);//指定了Integer,所以只能為Integer類型或者其子類
int b=Test.<Integer>add(1, 2.2);//編譯錯誤,指定了Integer,不能為Float
Number c=Test.<Number>add(1, 2.2); //指定為Number,所以可以為Integer和Float
}

//這是一個簡單的泛型方法
public static <T> T add(T x,T y){
return y;
}
}
正確的運轉

既然說類型變量會在編譯的時候擦除掉,那為什麽定義了ArrayList<Integer>泛型類型,而不允許向其中插入String對象呢?不是說泛型變量Integer會在編譯時候擦除變為原始類型Object嗎,為什麽不能存放別的類型呢?既然類型擦除了,如何保證我們只能使用泛型變量限定的類型呢?
java是如何解決這個問題的呢?java編譯器是通過先檢查代碼中泛型的類型,然後再進行類型擦除,再進行編譯的。以如下代碼為例:

[java] view plain copy
Pair<Integer> pair=new Pair<Integer> ();
pair.setValue(3);
Integer integer=pair.getValue();
System.out.println(integer);
擦除getValue()的返回類型後將返回Object類型,編譯器自動插入Integer的強制類型轉換。也就是說,編譯器把這個方法調用翻譯為兩條字節碼指令:
1、對原始方法Pair.getValue的調用

2、將返回的Object類型強制轉換為Integer

此外,存取一個泛型域時,也要插入強制類型轉換。因此,我們說Java的泛型是在編譯器層次進行實現的,被稱為“偽泛型”,相對於C++。

泛型相關問題

1、泛型類型引用傳遞問題

在Java中,像下面形式的引用傳遞是不允許的:


[java] view plain copy
ArrayList<String> arrayList1=new ArrayList<Object>();//編譯錯誤
ArrayList<Object> arrayList1=new ArrayList<String>();//編譯錯誤
我們先看第一種情況,將第一種情況拓展成下面的形式:

[java] view plain copy
ArrayList<Object> arrayList1=new ArrayList<Object>();
arrayList1.add(new Object());
arrayList1.add(new Object());
ArrayList<String> arrayList2=arrayList1;//編譯錯誤
實際上,在第4行代碼處,就會有編譯錯誤。那麽,我們先假設它編譯沒錯。那麽當我們使用arrayList2引用用get()方法取值的時候,返回的都是String類型的對象,可是它裏面實際上已經被我們存放了Object類型的對象,這樣,就會有ClassCastException了。所以為了避免這種極易出現的錯誤,Java不允許進行這樣的引用傳遞。(這也是泛型出現的原因,就是為了解決類型轉換的問題,我們不能違背它的初衷)。
在看第二種情況,將第二種情況拓展成下面的形式:

[java] view plain copy
ArrayList<String> arrayList1=new ArrayList<String>();
arrayList1.add(new String());
arrayList1.add(new String());
ArrayList<Object> arrayList2=arrayList1;//編譯錯誤
沒錯,這樣的情況比第一種情況好的多,最起碼,在我們用arrayList2取值的時候不會出現ClassCastException,因為是從String轉換為Object。可是,這樣做有什麽意義呢,泛型出現的原因,就是為了解決類型轉換的問題。我們使用了泛型,到頭來,還是要自己強轉,違背了泛型設計的初衷。所以java不允許這麽幹。再說,你如果又用arrayList2往裏面add()新的對象,那麽到時候取得時候,我怎麽知道我取出來的到底是String類型的,還是Object類型的呢?
所以,要格外註意泛型中引用傳遞問題。
2、泛型類型變量不能是基本數據類型
就比如,沒有ArrayList<double>,只有ArrayList<Double>。因為當類型擦除後,ArrayList的原始類中的類型變量(T)替換為Object,但Object類型不能存儲double值。
3、運行時類型查詢
舉個例子:

[java] view plain copy
ArrayList<String> arrayList=new ArrayList<String>();
因為類型擦除之後,ArrayList<String>只剩下原始類型,泛型信息String不存在了。那麽,運行時進行類型查詢的時候使用下面的方法是錯誤的
[java] view plain copy
if( arrayList instanceof ArrayList<String>)
java限定了這種類型查詢的方式,?為通配符,也即非限定符。
[java] view plain copy
if( arrayList instanceof ArrayList<?>)
4、泛型在靜態方法和靜態類中的問題
泛型類中的靜態方法和靜態變量不可以使用泛型類所聲明的泛型類型參數
[java] view plain copy
public class Test2<T> {
public static T one; //編譯錯誤
public static T show(T one){ //編譯錯誤
return null;
}
}
因為泛型類中的泛型參數的實例化是在定義泛型類型對象(例如ArrayList<Integer>)的時候指定的,而靜態變量和靜態方法不需要使用對象來調用。對象都沒有創建,如何確定這個泛型參數是何種類型,所以當然是錯誤的。
但是要註意區分下面的一種情況:
[java] view plain copy
public class Test2<T> {
public static <T >T show(T one){//這是正確的
return null;
}
}
因為這是一個泛型方法,在泛型方法中使用的T是自己在方法中定義的T,而不是泛型類中的T。

泛型相關面試題

1. Java中的泛型是什麽 ? 使用泛型的好處是什麽?
泛型是一種參數化類型的機制。它可以使得代碼適用於各種類型,從而編寫更加通用的代碼,例如集合框架。

泛型是一種編譯時類型確認機制。它提供了編譯期的類型安全,確保在泛型類型(通常為泛型集合)上只能使用正確類型的對象,避免了在運行時出現ClassCastException。

2、Java的泛型是如何工作的 ? 什麽是類型擦除 ?
泛型的正常工作是依賴編譯器在編譯源碼的時候,先進行類型檢查,然後進行類型擦除並且在類型參數出現的地方插入強制轉換的相關指令實現的。

編譯器在編譯時擦除了所有類型相關的信息,所以在運行時不存在任何類型相關的信息。例如List<String>在運行時僅用一個List類型來表示。為什麽要進行擦除呢?這是為了避免類型膨脹。

3. 什麽是泛型中的限定通配符和非限定通配符 ?
限定通配符對類型進行了限制。有兩種限定通配符,一種是<? extends T>它通過確保類型必須是T的子類來設定類型的上界,另一種是<? super T>它通過確保類型必須是T的父類來設定類型的下界。泛型類型必須用限定內的類型來進行初始化,否則會導致編譯錯誤。另一方面<?>表示了非限定通配符,因為<?>可以用任意類型來替代。

4. List<? extends T>和List <? super T>之間有什麽區別 ?
這和上一個面試題有聯系,有時面試官會用這個問題來評估你對泛型的理解,而不是直接問你什麽是限定通配符和非限定通配符。這兩個List的聲明都是限定通配符的例子,List<? extends T>可以接受任何繼承自T的類型的List,而List<? super T>可以接受任何T的父類構成的List。例如List<? extends Number>可以接受List<Integer>或List<Float>。在本段出現的連接中可以找到更多信息。

5. 如何編寫一個泛型方法,讓它能接受泛型參數並返回泛型類型?
編寫泛型方法並不困難,你需要用泛型類型來替代原始類型,比如使用T, E or K,V等被廣泛認可的類型占位符。泛型方法的例子請參閱Java集合類框架。最簡單的情況下,一個泛型方法可能會像這樣:


[java] view plain copy
public V put(K key, V value) {
return cache.put(key, value);
}
6. Java中如何使用泛型編寫帶有參數的類?
這是上一道面試題的延伸。面試官可能會要求你用泛型編寫一個類型安全的類,而不是編寫一個泛型方法。關鍵仍然是使用泛型類型來代替原始類型,而且要使用JDK中采用的標準占位符。
7. 編寫一段泛型程序來實現LRU緩存?
對於喜歡Java編程的人來說這相當於是一次練習。給你個提示,LinkedHashMap可以用來實現固定大小的LRU緩存,當LRU緩存已經滿了的時候,它會把最老的鍵值對移出緩存。LinkedHashMap提供了一個稱為removeEldestEntry()的方法,該方法會被put()和putAll()調用來刪除最老的鍵值對。
8. 你可以把List<String>傳遞給一個接受List<Object>參數的方法嗎?
對任何一個不太熟悉泛型的人來說,這個Java泛型題目看起來令人疑惑,因為乍看起來String是一種Object,所以List<String>應當可以用在需要List<Object>的地方,但是事實並非如此。真這樣做的話會導致編譯錯誤。如果你再深一步考慮,你會發現Java這樣做是有意義的,因為List<Object>可以存儲任何類型的對象包括String, Integer等等,而List<String>卻只能用來存儲Strings。

[java] view plain copy
List<Object> objectList;
List<String> stringList;

objectList = stringList; //compilation error incompatible types
9. Array中可以用泛型嗎?
這可能是Java泛型面試題中最簡單的一個了,當然前提是你要知道Array事實上並不支持泛型,這也是為什麽Joshua Bloch在Effective Java一書中建議使用List來代替Array,因為List可以提供編譯期的類型安全保證,而Array卻不能。
10. 如何阻止Java中的類型未檢查的警告?
如果你把泛型和原始類型混合起來使用,例如下列代碼,Java 5的javac編譯器會產生類型未檢查的警告
,例如List<String> rawList = new ArrayList()
註意: Hello.java使用了未檢查或稱為不安全的操作;
這種警告可以使用@SuppressWarnings("unchecked")註解來屏蔽。
11、Java中List<Object>和原始類型List之間的區別?
原始類型和帶參數類型<Object>之間的主要區別是,在編譯時編譯器不會對原始類型進行類型安全檢查,卻會對帶參數的類型進行檢查,通過使用Object作為類型,可以告知編譯器該方法可以接受任何類型的對象,比如String或Integer。這道題的考察點在於對泛型中原始類型的正確理解。它們之間的第二點區別是,你可以把任何帶參數的泛型類型傳遞給接受原始類型List的方法,但卻不能把List<String>傳遞給接受List<Object>的方法,因為會產生編譯錯誤。

12、Java中List<?>和List<Object>之間的區別是什麽?
這道題跟上一道題看起來很像,實質上卻完全不同。List<?> 是一個未知類型的List,而List<Object>其實是任意類型的List。你可以把List<String>, List<Integer>賦值給List<?>,卻不能把List<String>賦值給List<Object>。


[java] view plain copy
List<?> listOfAnyType;
List<Object> listOfObject = new ArrayList<Object>();
List<String> listOfString = new ArrayList<String>();
List<Integer> listOfInteger = new ArrayList<Integer>();

listOfAnyType = listOfString; //legal
listOfAnyType = listOfInteger; //legal
listOfObjectType = (List<Object>) listOfString; //compiler error - in-convertible types
13、List<String>和原始類型List之間的區別.
該題類似於“原始類型和帶參數類型之間有什麽區別”。帶參數類型是類型安全的,而且其類型安全是由編譯器保證的,但原始類型List卻不是類型安全的。你不能把String之外的任何其它類型的Object存入String類型的List中,而你可以把任何類型的對象存入原始List中。使用泛型的帶參數類型你不需要進行類型轉換,但是對於原始類型,你則需要進行顯式的類型轉換。

[java] view plain copy
List listOfRawTypes = new ArrayList();
listOfRawTypes.add("abc");
listOfRawTypes.add(123); //編譯器允許這樣 - 運行時卻會出現異常
String item = (String) listOfRawTypes.get(0); //需要顯式的類型轉換
item = (String) listOfRawTypes.get(1); //拋ClassCastException,因為Integer不能被轉換為String

List<String> listOfString = new ArrayList();
listOfString.add("abcd");
listOfString.add(1234); //編譯錯誤,比在運行時拋異常要好
item = listOfString.get(0); //不需要顯式的類型轉換 - 編譯器自動轉換
通配符

通配符上界

常規使用
[java] view plain copy
public class Test {
public static void printIntValue(List<? extends Number> list) {
for (Number number : list) {
System.out.print(number.intValue()+" ");
}
System.out.println();
}
public static void main(String[] args) {
List<Integer> integerList=new ArrayList<Integer>();
integerList.add(2);
integerList.add(2);
printIntValue(integerList);
List<Float> floatList=new ArrayList<Float>();
floatList.add((float) 3.3);
floatList.add((float) 0.3);
printIntValue(floatList);
}
}
輸出:
2 2
3 0
非法使用
[java] view plain copy
public class Test {
public static void fillNumberList(List<? extends Number> list) {
list.add(new Integer(0));//編譯錯誤
list.add(new Float(1.0));//編譯錯誤
}
public static void main(String[] args) {
List<? extends Number> list=new ArrayList();
list.add(new Integer(1));//編譯錯誤
list.add(new Float(1.0));//編譯錯誤
}
}
List<? extends Number>可以代表List<Integer>或List<Float>,為什麽不能像其中加入Integer或者Float呢?
首先,我們知道List<Integer>之中只能加入Integer。並且如下代碼是可行的:
[java] view plain copy
List<? extends Number> list1=new ArrayList<Integer>();
List<? extends Number> list2=new ArrayList<Float>();
假設前面的例子沒有編譯錯誤,如果我們把list1或者list2傳入方法fillNumberList,顯然都會出現類型不匹配的情況,假設不成立。
因此,我們得出結論:不能往List<? extends T> 中添加任意對象,除了null。

那為什麽對List<? extends T>進行叠代可以呢,因為子類必定有父類相同的接口,這正是我們所期望的。

通配符下界

常規使用
[java] view plain copy
public class Test {
public static void fillNumberList(List<? super Number> list) {
list.add(new Integer(0));
list.add(new Float(1.0));
}
public static void main(String[] args) {
List<? super Number> list=new ArrayList();
list.add(new Integer(1));
list.add(new Float(1.1));
}
}
可以添加Number的任何子類,為什麽呢?
List<? super Number>可以代表List<T>,其中T為Number父類,(雖然Number沒有父類)。如果說,T為Number的父類,我們想List<T>中加入Number的子類肯定是可以的。
非法使用
對List<? superT>進行叠代是不允許的。為什麽呢?你知道用哪種接口去叠代List嗎?只有用Object類的接口才能保證集合中的元素都擁有該接口,顯然這個意義不大。其應用場景略。
無界通配符

知道了通配符的上界和下界,其實也等同於知道了無界通配符,不加任何修飾即可,單獨一個“?”。如List<?>,“?”可以代表任意類型,“任意”也就是未知類型。
List<Object>與List<?>並不等同,List<Object>是List<?>的子類。還有不能往List<?> list裏添加任意對象,除了null。
常規使用
1、當方法是使用原始的Object類型作為參數時,如下:
[java] view plain copy
public static void printList(List<Object> list) {
for (Object elem : list)
System.out.println(elem + "");
System.out.println();
}
可以選擇改為如下實現:
[java] view plain copy
public static void printList(List<?> list) {
for (Object elem: list)
System.out.print(elem + "");
System.out.println();
}
這樣就可以兼容更多的輸出,而不單純是List<Object>,如下:
[java] view plain copy
List<Integer> li = Arrays.asList(1, 2, 3);
List<String> ls = Arrays.asList("one", "two", "three");
printList(li);
printList(ls);

參考:

《Java核心技術 卷一》

http://blog.csdn.net/lonelyroamer/article/details/7868820

http://www.oschina.net/translate/10-interview-questions-on-java-generics

http://www.linuxidc.com/Linux/2013-10/90928.htm

Java泛型深入理解