簡介
泛型的意思就是參數化類型,通過使用參數化類型創建的接口、類、方法,可以指定所操作的數據類型。比如:可以使用參數化類型創建操作不同類型的類。操作參數化類型的接口、類、方法成為泛型,比如泛型類、泛型方法。
泛型還提供缺失的類型安全性,我們知道Object是所有類的超類,在泛型前通過使用Object操作各種類型的對象,然后在進行強制類型轉換。而通過使用泛型,這些類型轉換都是自動或隱式進行的了。因此提高了代碼重用能力,而且可以安全、容易的重用代碼。
泛型類
<pre>
public class Generic<T> {
T ob;
Generic(T o){
this.ob = o;
}
T getOb(){
return ob;
}
void showType(){
System.out.println("T type:" + ob.getClass().getName());
}
}
</pre>
<pre>
class Generic<T>
</pre>
T是類型參數名稱,使用<>括上,這個名稱是實際類型的占位符。當創建一個Generic對象的時候,會傳遞一個實際類型,因為Generic使用了類型參數,所以該類是泛型類。類中只要需要使用類型參數的地方就使用T,當傳遞實際類型后,會自動改變成實際類型。
比如:
T的類型就是Integer。
<pre>
Generic<Integer> gen1 = new Generic<Integer>(100);
</pre>
T的類型就是String。
<pre>
Generic<String> gen2 = new Generic<String>(“test”);
</pre>
使用泛型類
當調用泛型構造方法時候,仍然需要指定參數類型,因為為構造函數賦值的是Generic<String>。
需要注意上述這個過程,就像Java編譯器創建了不同版本的Generic類,但實際編譯器并沒有那樣做,而是將所有泛型類型移除,進行類型轉換,從而看似是創建了一個個Generic類版本。移除泛型的過程稱為擦除。
泛型只能使用引用類型
當聲明泛型實例的時候,傳遞過來的類型參數必須引用類型。不能是基本類型,比如int、char等。其實可以通過類型封裝器封裝基本類型,所以這個限制并不嚴格。
<pre>
Generic<int> gen3 = new Generic<int>();
</pre>
基于不同類型的泛型類是不同的,比如Generic<Integer> gen1和Generic<String> gen2雖然都是Generic<T>類型,但是它們是不同的類型引用,所以gen1 != gen2。這個就是泛型添加類型安全以及防止錯誤的一部分。
泛型類型安全的原理
上面我們說過,其實泛型的實現完全可以通過使用Object類型替換,將Genneric中所有T轉換成Object類型,然后在使用時候通過強制類型轉換獲取值。但是這有許多風險的,比如手動輸入強制類型轉換、進行類型檢查。而實用泛型它會將這些操作將是隱式完成的,泛型能夠保證自動確保類型安全。可以將運行時錯誤轉換成編譯時錯誤,比如如果實用Object替代泛型,對于之前Generic<Integer> gen1 和Generic<String> gen2,將gen1 = gen2這樣在泛型中直接編譯錯誤,如果使用Object替代,則不會產生編譯錯誤,因為它們本身都是Generic類型,但是在執行相關代碼時候會出錯,比如getOb()將String類型直接賦值給int類型。
多個類型參數的泛型類
當需要聲明多個參數類型時,只需要使用逗號分隔參數列表即可。
<pre>
public class Generic<T,V> {
T ob1;
V ob2;
Generic(T ob1,V ob2){
this.ob1 = ob1;
this.ob2 = ob2;
}
T getOb1(){
return ob1;
}
V getOb2(){
return ob2;
}
void showType(){
System.out.println("T type:" + ob1.getClass().getName());
System.out.println("V type:" + ob2.getClass().getName());
}
}
</pre>
這樣在創建Generic實例時候,需要分別給出參數類型。
<pre>
Generic<String,Integer> generic = new Generic<String,Integer>(“test”,123);
</pre>
泛型類定語法:
<pre>
class class-name<type-param-list>{
//….
}
</pre>
泛型類引用語法:
<pre>
class-name<type-param-list> var-name = new class-name<type-param-list>(con-arg-list);
</pre>
有界類型(bounded type)
前面討論的泛型,可以被任意類型替換。對于絕大多數情況是沒問題的,但是一些特殊場景需要對傳遞的類型進行限制,比如一個泛型類只能是數字,不希望使用其它類型。我們知道無論Integer還是Double都是Number的子類,所以可以限制只有Number及其子類可以使用,定義的泛型類的時候,在泛型類中可以使用Number中定義的方法(否則無法使用,比如使用Number類中的doubleValue(),如果直接使用會無法通過編譯,因為T泛型,并不知道你這個參數類型是什么)。
<pre>
public class Generic<T extends Number> {
T[] array;
Generic(T[] array){
this.array = array;
}
double average(){
double sum = 0;
for(int i=0;i<array.length;i++){
sum += array[i].doubleValue();
}
return sum / array.length;
}
}
Generic<T extends Number>
</pre>
這樣T只能被Number及其子類代替,這時候java編譯器也知道T類型的對象都可以調用dobuleValue()方法,因為這個方法是Number中定義的。
除了可以使用類作為邊界,也可以使用接口作為邊界,使用方式與上面相同。同時也可以同時使用一個類和一個接口或多個接口邊界,對于這種情況,需要先指定類類型。如果指定接口類型,那么實現了這個接口的類型參數是合法的。
<pre>
class class-name<T extends MyClass & MyInterface>
</pre>
使用通配符參數
我們繼續擴展上面這個類,當需要一個sameAvg()方法用來比較兩個對象的average()接口是否相同,這個sameAvg()接口怎么寫?
第一種方式:
<pre>
boolean sameAvg(Generic<T> ob){
if(average() == ob.average())
return true;
return false;
}
</pre>
這種方式有一個弊端,就是Generic<Integer>只能和Generic<Integer>比較(上面說了),而我們比較相同平均數并care類型。這時我們可以使用通配符“?”來解決。
第二種方式:
<pre>
boolean sameAvg(Generic<?> ob){
//...
}
</pre>
使用通配符需要理解一點,它本身不會影響創建什么類型的Generic對象,通配符只是簡單匹配所有有效的(有界類型下的)Generic對象。
有界通配符
使用有界通配符,可以為參數類型指定上界和下界,從而能夠限制方法能夠操作的對象類型。最常用的是指定有界通配符上界,使用extends子句創建。
<pre>
<? extends superclass>
</pre>
這樣直有superclass類及其子類可以使用。也可以指定下界:
<pre>
<? super subclass>
</pre>
這樣subclass的超類是可接受的參數類型。
有界通配符的應用場景一般是操作類層次的泛型(C 繼承 B,B繼承A),控制層次類型。
創建泛型方法
之前討論泛型類中的泛型方法都是使用創建實例傳遞過來的類型,其實方法可以本身使用一個或多個類型參數的泛型方法。并且,可以在非泛型類中創建泛型方法。
<pre>
class GenericDemo {
<T extends Comparator<T>, V extends T> boolean isIn(T x, V[] y) {
for (int i = 0; i < y.length; i++) {
if (x.equals(y[i]))
return true;
}
return false;
}
}
</pre>
<pre>
<T extends Comparator<T>, V extends T> boolean isIn(T x, V[] y)
</pre>
泛型參數在返回類型之前,T擴展了類型Comparator<T>,所以只有實現了Comparator<T>接口的類才可以使用。同時V設置了T為上界,這樣V必須是T或者其子類。通過強制參數,達到相互兼容。
調用isIn()時候一般可以直接使用,不需要指定類型參數,類型推斷就可以自動完成。當然你也可以指定類型:
<pre>
<Integer,Integer>isIn(3,nums);
</pre>
泛型方法語法:
<pre>
<type-param-list> ret-type meth-name(param-list){
//..
}
</pre>
也可以為構造方法泛型化,即便類不是泛型類,但是構造方法是。所以在構造該實例時候需要根據泛型類型給出。
<pre>
<T extends Number> Generic(T a){
//..
}
</pre>
泛型接口
泛型接口與定義泛型類是類似的
<pre>
interface MyInterface<T extends Comparable<T>>{
//...
}
</pre>
當類實現接口時候,因為接口指定界限,所以實現類也需要指定相同的界限。并且接口一旦建立這個界限,那么在實現他的時候就不需要在指定了。
<pre>
class MyClass<T extends Compareable<T>> implements MyInterface<T>{
//...
}
</pre>
如果類實現了具體類型的泛型接口,實現類可以不指出泛型類型。
<pre>
class MyClass implements MyInterface<Integer>{
//...
}
</pre>
使用泛型接口,可以針對不同類型數據進行實現;使用泛型接口也為實現類設置了類型限制條件。
定義泛型接口語法:
<pre>
interface interface-name<type-param-list>{
//...
}
</pre>
實現泛型接口
<pre>
class class-name<type-params-list> implements interface-name<type-arg-list>{
//...
}
</pre>
遺留代碼中的原始類型
泛型是在JDK 5之后提供的,在JDK 5之前是不支持的泛型的。所以這些遺留代碼即需要保留功能,又要和泛型兼容。可以使用混合編碼,還比如上面的例子。Generic<T> 類是一個泛型類,我們可以使用原始類型(不指定泛型類型),來創建Generic類。
<pre>
Generic gen1 = new Generic(new Double(9.13));
double gen2 = (Double)gen1.getOb();
</pre>
java是支持這種原始類型,然后通過強制類型轉換使用的。但是正如我們上面說的,這就繞過了泛型的類型檢查,它是類型不安全的,有可能導致運行時異常(RunTime Exception)。
泛型類層次
泛型類也可以是層次的一部分,就像非泛型類那樣。泛型類可以作為超類或子類。泛型和非泛型的區別在于,泛型類層次中的所有子類會將類型向上傳遞給超類。
<pre>
class Gen<T>{
T ob;
Gen(T ob){
this.ob = ob;
}
T genOb(){
return ob;
}
}
class Gen2<T> extends Gen<T>{
Gen2(T o){
super(o);//向上傳遞
}
}
</pre>
<pre>
Gen2<Integer> gen = new Gen2<Integer>();
</pre>
創建Gen2傳入Integer類型,Integer類型也會傳入超類Gen中。子類可以根據自己的需求,任意添加參數類型。
<pre>
class Gen2<T,V> extends Gen<T>{
Gen2(T a,V b){
super(a);//一定要有
}
}
</pre>
超類也可以不是泛型,子類在繼承的時候,就不需要有特殊的條件了。
<pre>
class Gen{
Gen(int a){
}
}
class Gen2<T,V> extends Gen{
Gen2(T a,int b){
super(b);
}
Gen2(T a,V b,int c){
super(c);
}
}
</pre>
需要注意的:
- 泛型類型強制類型轉換,需要兩個泛型實例的類型相互兼容并且它們的類型參數也相同。
- 可以向重寫其它方法那樣重寫泛型的方法。
- 從JDK 7起泛型可以使用類型推斷在創建實例時候省略類型,因為在參數聲明的時候已經指定過一次了,所以可以根據聲明的變量進行類型推斷。
List<String,Integer> list = new ArrayList<>();
擦除
泛型為了兼容以前的代碼(JDK 5之前的),使用了擦除實現泛型。具體就是,當編譯java代碼的時候,所有泛型信息被移除(擦除)。會使用它們的界定類型替換,如果沒有界定類型,會使用Object,然后進行適當的類型轉換。
模糊性錯誤
泛型引入后,也增加了一種新類型錯誤-模糊性錯誤的可能,需要進行防范。當擦除導致兩個看起來不同的泛型聲明,在擦除之后可能變成相同類型,從而導致沖突。
<pre>
class Gen<T,V>{
T ob1;
V ob2;
void setOb(T ob){
this.ob1 = ob;
}
void setOb(V ob){
this.ob2 = ob;
}
}
</pre>
這種是無法編譯的,因為當擦除后可能會導致類型相同,這樣的方法重載是不對的。
<pre>
Gen<String,String> gen = new Gen<String,String>();
</pre>
這樣T和V都是String類型,明顯代碼是不對的。
可以通過指定一個類型邊界,比如:
<pre>
class Test1{
public static void main(String[] args){
//沒問題
Gen<String,Integer> gen = new Gen<String, Integer>();
gen.setOb(1);
//這樣在調用setOb的時候也會編譯失敗,因為都為Integer類型,方法重載錯誤
Gen<Integer,Integer> gen1 = new Gen<Integer, Integer>();
gen1.setOb(1);
}
}
</pre>
所以在解決這種模糊錯誤時候,最好使用獨立的方法名,而不是去重載。
使用泛型的限制
- 不能實例化類型參數,因為編譯器不知道創建哪種類型,T只是類型占位符。
<pre>
class Gen<T>{
T ob;
Gen(){
ob = new T();
}
}
</pre> - 靜態成員不能使用類中聲明的類型參數。
<pre>
class Gen<T>{
//錯誤的,不能聲明靜態成員
static T ob;
//錯誤的,靜態方法不能使用參數類型T
static T getGen(){
return ob;
}
//正確的,靜態方法不是參數類型
static void printXXX(){
System.out.println();
}
}
</pre> - 不能實例化類型參數數組
<pre>
//沒問題
T[] vals;
//不能實例化類型參數數組
vals = new T[10];
</pre> - 不能創建特性類型的泛型應用數組
<pre>
//這是不允許的
Gen<Integer> gen = new Gen<Integer>[10];
但是可以使用通配符,并且比使用原始類型好,因為進行了類型檢查。
Gen<?> gen = new Gen<?>[10];
</pre> - 泛型類不能擴展Throwable,這就意味著不嗯滾創建泛型異常類。
關注我
歡迎關注我的公眾號,會定期推送優質技術文章,讓我們一起進步、一起成長!
公眾號搜索:data_tc
或直接掃碼:??