java 基礎 泛型

Fysddsw_lc發表於2019-02-26

泛型是什麼?

泛型,即“引數化型別”。一提到引數,最熟悉的就是定義方法時有形參,然後呼叫此方法時傳遞實參。那麼引數化型別怎麼理解呢?顧名思義,就是將型別由原來的具體的型別引數化,類似於方法中的變數引數,此時型別也定義成引數形式(可以稱之為型別形參),然後在使用/呼叫時傳入具體的型別(型別實參)。

泛型的本質是為了引數化型別(在不建立新的型別的情況下,通過泛型指定的不同型別來控制形參具體限制的型別)。也就是說在泛型使用過程中,操作的資料型別被指定為一個引數,這種引數型別可以用在類、介面和方法中,分別被稱為泛型類、泛型介面、泛型方法。

在集合中儲存物件並在使用前進行型別轉換是多麼的不方便。泛型防止了那種情況的發生。它提供了編譯期的型別安全,確保你只能把正確型別的物件放入 集合中,避免了在執行時出現ClassCastException。

優點

泛型的引入可以解決之前的集合類框架在使用過程中通常會出現的執行時刻型別錯誤,因為編譯器可以在編譯時刻就發現很多明顯的錯誤。

型別擦除

泛型實在編譯器層次來實現的

在生成的java位元組程式碼中是不包含泛型中的型別資訊的。使用泛型的時候加上的型別引數,會被編譯器在編譯的時候去掉,這個就叫型別擦除。

如在程式碼中定義的List(Object)和List(String)等型別,在編譯之後都會變成List。JVM看到的只是List,而由泛型附加的型別資訊對JVM來說是不可見的,java編譯器會在編譯是儘可能發現出錯的地方,但是仍然無法避免在執行時刻出現型別轉換異常的情況。

泛型的奇怪特性都與型別擦除有關

  1. 泛型類並沒有自己獨有的Class物件,比如並不存在List(String).class,z只有List.class
  2. 靜態變數是被泛型類的所有例項被共享的。對於宣告為MyClass(T)的類,訪問其中的靜態變數的方法仍然是 MyClass.myStaticVar。不管是通過new MyClass(String)還是new MyClass(Integer)建立的物件,都是共享一個靜態變數。
  3. 泛型的型別引數不能用在java異常處理的catch語句中。因為異常處理是由JVM在執行時刻來進行的。由於型別資訊被擦除,JVM是無法區分兩個異常型別MyException(String)和MyException(Integer)的。對於JVM來說,它們都是 MyException型別的。也就無法執行與異常對應的catch語句。

型別擦除過程

找到用來替換型別引數的具體類。這個具體類一般是Object。如果指定了型別引數的上界的話,則使用上界。把程式碼中的型別引數都替換成具體類。同時去掉同時出現的型別宣告,即去掉<>的內容。比如Tget()方法宣告就變成了Object get();List(String)就變成了List。接下來就可能需要生辰改一些橋接方法。就是由於擦除了型別之後的類可能缺少默寫某些必須的方法。這樣做的目的,是確保能和Java 5之前的版本開發二進位制類庫進行相容。你無法在執行時訪問到型別引數,因為編譯器已經把泛型型別轉換成了原始型別。

例項分析

瞭解了型別擦除機制之後,就會明白編譯器承擔了全部的型別檢查工作。編譯器禁止某些泛型的使用方式,正是為了確保型別的安全性。以上面提到的List(Object)和List(String)為例來具體分析:

public void insert(List(Object) list) {    
    for (Object obj : list) {        
        System.out.println(obj);    
    }    
    list.add(1); //這個操作在當前方法的上下文是合法的。 
}
public void test() {    
    List(String) strs = new ArrayList(String)();    
    inspect(strs); //編譯錯誤 
}複製程式碼

這段程式碼中,insert方法接受List(Object)作為引數,當在test方法中試圖傳入List(String)的時候,會出現編譯錯誤。假設這樣的做法是允許的,那麼在insert方法就可以通過list.add(1)來向集合中新增一個數字。這樣在test方法看來,其宣告為List(String)的集合中卻被新增了一個Integer型別的物件。這顯然是違反型別安全的原則的,在某個時候肯定會丟擲ClassCastException。因此,編譯器禁止這樣的行為。編譯器會盡可能的檢查可能存在的型別安全問題。對於確定是違反相關原則的地方,會給出編譯錯誤。當編譯器無法判斷型別的使用是否正確的時候,會給出警告資訊。

泛型類和泛型方法

泛型類

public class Som<T> {
    private T value;

    public T getValue() {
        return value;
    }

    public void setValue(T value) {
        this.value = value;
    }
}
複製程式碼

Som就是一個泛型類,value的型別是T,而T是引數化的。如果有多個型別引數,使用分號隔開,如<U,V>。在編譯期,是無法知道U和V具體是什麼型別,只有在執行時才會真正根據型別來構造和分配記憶體。

public class Main {
    public static void main(String[] args) {
        Son<String, String> c1 = new Son<String, String>("name", "findingsea");
        Son<String, Integer> c2 = new Son<String, Integer>("age", 24);
        Son<Double, Double> c3 = new Son<Double, Double>(1.1, 2.2);
        System.out.println(c1.getKey() + " : " + c1.getValue());
        System.out.println(c2.getKey() + " : " + c2.getValue());
        System.out.println(c3.getKey() + " : " + c3.getValue());
    }
}輸出:

name : findingsea
age : 24
1.1 : 2.2複製程式碼

可以看一下現在Som類對於不同型別的支援情況:
使用泛型類:

Som<String> som = new Som<>();
som.setValue("Hi");
//som.setValue(123);編譯不通過
String str = som.getValue();
複製程式碼

在使用中指定具體的型別實參。

泛型介面

public interface Generator<T> {
    public T next();
}
然後定義一個生成器類來實現這個介面:

public class FruitGenerator implements Generator<String> {

    private String[] fruits = new String[]{"Apple", "Banana", "Pear"};

    @Override
    public String next() {
        Random rand = new Random();
        return fruits[rand.nextInt(3)];
    }
}
呼叫:

public class Main {

    public static void main(String[] args) {
        FruitGenerator generator = new FruitGenerator();
        System.out.println(generator.next());
        System.out.println(generator.next());
        System.out.println(generator.next());
        System.out.println(generator.next());
    }
}
輸出:

Apple
Banana
Pear
Pear複製程式碼

泛型方法

在java中,泛型類的定義非常簡單,但是泛型方法就比較複雜了。無論何時,只要你能做到,你就應該儘量使用泛型方法。也就是說,如果使用泛型方法可以取代將整個類泛化,那麼應該有限採用泛型方法。

泛型類,是在例項化類的時候指明泛型的具體型別;泛型方法,是在呼叫方法的時候指明泛型的具體型別

如果你定義了一個泛型(類、介面),那麼Java規定,你不能在所有的靜態方法、靜態初塊等所有靜態內容中使用泛型的型別引數。例如:

public class A<T> {
    public static void func(T t) {
    //報錯,編譯不通過
    }
}複製程式碼

泛型的萬用字元

泛型的萬用字元增強了方法的靈活性但也容易讓人困惑。

Java中有無限定萬用字元<?>,上界限定萬用字元<? extends E>,下界限定萬用字元<? super E>這三種萬用字元。

無限定萬用字元

需求:列印List中的元素。List是一個泛型類,有List<String>,List<Number>,List<Object>等可能。使用List<?>萬用字元,可以匹配任意List泛型。
程式碼如下:

public static void printList(List<?> list) {
    for (int i = 0; i < list.size(); i++) {
        System.out.println(list.get(i));
    }
}
複製程式碼

看起來很簡單,但是此時的list是無法進行add操作的,因為List的型別是未知的。這就是<?>的只讀性,稍後會有介紹。

在使用泛型類的時候,既可以指定一個具體的型別,如List(String)就宣告瞭具體的型別是String;也可以用萬用字元?來表示未知型別,如List<?>就宣告瞭List中包含的元素型別是未知的。 萬用字元所代表的其實是一組型別,但具體的型別是未知的。List<?>所宣告的就是所有型別都是可以的。但是List<?>並不等同於List(Object)。List(Object)實際上確定了List中包含的是Object及其子類,在使用的時候都可以通過Object來進行引用。而List<?>則其中所包含的元素型別是不確定。其中可能包含的是String,也可能是 Integer。如果它包含了String的話,往裡面新增Integer型別的元素就是錯誤的。正因為型別未知,就不能通過new ArrayList(?)()的方法來建立一個新的ArrayList物件。因為編譯器無法知道具體的型別是什麼。但是對於 List(?)中的元素確總是可以用Object來引用的,因為雖然型別未知,但肯定是Object及其子類。

有限萬用字元

同樣是一個列印List元素的例子,但是隻接受型別引數是Number及其子類。

public static void printList(List<? extends Number> list) {
    for (int i = 0; i < list.size(); i++) {
        System.out.println(list.get(i));
    }
}
複製程式碼

和<?>一樣,<? extends E>也具有隻讀性。

萬用字元<?>和<? extends E>具有隻讀性,即可以對其進行讀取操作但是無法進行寫入。

public static void printList(List<?> list) {
    for (int i = 0; i < list.size(); i++) {
        System.out.println(list.get(i));
    }
    //一下操作不可以
    list.add(1);
    list.add("123");
}
複製程式碼

原因在於:?就是表示型別完全無知,? extends E表示是E的某個子型別,但不知道具體子型別,如果允許寫入,Java就無法確保型別安全性。假設我們允許寫入,如果我們傳入的引數是List<Integer>,此時進行add操作,可以新增任何型別元素,就無法保證List<Integer>的型別安全了。

因為對於List(?)中的元素只能用Object來引用,在有些情況下不是很方便。在這些情況下,可以使用上下界來限制未知型別的範圍。 如List(? extends Number)說明List中可能包含的元素型別是Number及其子類。而List(? super Number)則說明List中包含的是Number及其父類。當引入了上界之後,在使用型別的時候就可以使用上界類中定義的方法。比如訪問 List(? extends Number)的時候,就可以使用Number類的intValue等方法。

超型別

超型別萬用字元允許寫入,例子如下:

public static void printList(List<? super String> list) {
    for (int i = 0; i < list.size(); i++) {
        System.out.println(list.get(i));
    }
    list.add("123");
    list.add("456");
}
複製程式碼

這個很好理解,list的引數型別是String的上界,必然可以新增String型別的元素。

泛型與陣列

Java不能建立泛型陣列,以Som泛型類為例,以下程式碼編譯報錯:

Som<String> [] soms = new Som<String>[8];
複製程式碼

原因是像Integer[]和Number[]之間有繼承關係,而List<Integer>和List<Number>沒有,如果允許泛型陣列,那麼編譯時無法發現,執行時也不是立即就能發現的問題會出現。參看以下程式碼:

Som<Integer>[] soms = new Som<Integer>[3];
Object[] objs = soms;
objs[0] = new Som<String>();
複製程式碼

那我們怎麼存放泛型物件呢?可以使用原生陣列或者泛型容器。

泛型的命名規範

為了更好地去理解泛型,我們也需要去理解java泛型的命名規範。為了與java關鍵字區別開來,java泛型引數只是使用一個大寫字母來定義。各種常用泛型引數的意義如下:
E — Element,常用在java Collection裡,如:List(E),Iterator(E),Set(E)
K,V — Key,Value,代表Map的鍵值對
N — Number,數字
T — Type,型別,如String,Integer等等

相關文章