java泛型詳解

來源:互聯網
上載者:User

標籤:java   泛型   generic   

為什麼引入泛型

bug是編程的一部分,我們只能盡自己最大的能力減少出現bug的幾率,但是誰也不能保證自己寫出的程式不出現任何問題。

錯誤可分為兩種:編譯時間錯誤與執行階段錯誤。編譯時間錯誤在編譯時間可以發現並排除,而執行階段錯誤具有很大的不確定性,在程式運行時才能發現,造成的後果可能是災難性的。

使用泛型可以使錯誤在編譯時間被探測到,從而增加程式的健壯性。

來看一個例子:

public class Box{    private Object object;     public void set(Object object) {            this.object= object;       }    public Object get() {             return object;       }}

按照聲明,其中的set()方法可以接受任何java對象作為參數(任何對象都是Object的子類),假如在某個地方使用該類,set()方法預期的輸入對象為Integer類型,但是實際輸入的卻是String類型,就會拋出一個執行階段錯誤,這個錯誤在編譯階段是無法檢測的。例如:

Box box = new Box; box.set(“abc”); Integer a =(Integer)box.get();  //編譯時間不會報錯,但是運行時會報ClassCastException
運用泛型改造上面的代碼:

public class Box<T>{    private T t;     public void set(T t) {            this.t= t;       }    public T get() {             return t;       }}

當我們使用該類時會指定T的具體類型,該型別參數可以是類、介面、數組等,但是不能是基本類型。

比如:

Box<Integer> box = new Box<Integer>; //指定了類型類型為Integer//box.set(“abc”);  該句在編譯時間就會報錯box.set(new Integer(2));Integer a = box.get();  //不用轉換類型

可以看到,使用泛型還免除了轉換操作。

在引入泛型機制之前,要在方法中支援多個資料類型,需要對方法進行重載,在引入範型後,可以更簡潔地解決此問題,更進一步可以定義多個參數以及返回值之間的關係。

例如

public void write(Integer i, Integer[] ia);public void write(Double  d, Double[] da);public void write(Long l, Long[] la);

的範型版本為

public <T> void write(T t, T[] ta);
總體來說,泛型機制能夠在定義類、介面、方法時把“類型”當做參數使用,有點類似於方法聲明中的形式參數,如此我們就能通過不同的輸入參數來實現程式的重用。不同的是,形式參數的輸入是值,而泛型參數的輸入是類型。


命名規則

型別參數的命名有一套預設規則,為了提高代碼的維護性和可讀性,強烈建議遵循這些規則。JDK中,隨處可見這些命名規則的應用。

E - Element (通常代表集合類中的元素)

K - Key

N - Number

T - Type

V - Value

S,U,V etc. – 第二個,第三個,第四個型別參數……

注意,父類定義的型別參數不能被子類繼承。

也可以同時聲明多個類型變數,用逗號分割,例如:

public interface Pair<K, V> {    public K getKey();    public V getValue();} public class OrderedPair<K, V> implements Pair<K, V> {     private K key;    private V value;     public OrderedPair(K key, V value) {      this.key = key;      this.value = value;    }     public K getKey()   { return key; }    public V getValue() { return value; }}

下面的兩行代碼建立了OrderedPair對象的兩個執行個體。

Pair<String,Integer> p1 = new OrderedPair<String, Integer>("Even", 8);Pair<String,String>  p2 = new OrderedPair<String, String>("hello", "world"); //也可以將new後面的型別參數省略,簡寫為://Pair<String,Integer> p1 = new OrderedPair<>("Even", 8); //也可以在角括弧內使用帶有類型變數的類型變數,例如:OrderedPair<String,Box<Integer>> p = new OrderedPair<>("primes", new Box<Integer>(...));
泛型是JDK 5.0之後才引入的,為了相容性,允許不指定泛型參數,但是如此一來,編譯器就無法進行類型檢查,在編程時,最好明確指定泛型參數。

同樣,在方法中也可是使用泛型參數,並且該參數的使用範圍僅限於方法體內。例如:

public class Util {//該方法用於比較兩個Pair對象是否相等。//泛型參數必須寫在方法傳回型別boolean之前    public static <K, V> boolean compare(Pair<K,V> p1, Pair<K, V> p2) {        return p1.getKey().equals(p2.getKey())&&              p1.getValue().equals(p2.getValue());    }} Pair<Integer,String> p1 = new Pair<>(1, "apple");Pair<Integer,String> p2 = new Pair<>(2, "pear");boolean same = Util.<Integer, String>compare(p1, p2);//實際上,編譯器可以通過Pair當中的類型來推斷compare需要使用的類型,所以可以簡寫為:// boolean same= Util. compare(p1, p2);
有時候我們想讓型別參數限定在某個範圍之內,就需要用到extends關鍵字(extends後面可以跟一個介面,這裡的extends既可以表示繼承了某個類,也可以表示實現了某個介面),例如,我們想讓參數是數字類型:

class Box<T extends Number>{  //型別參數限定為Number的子類           private T t;           public Box(T t){             this.t = t;      }      public void print(){             System.out.println(t.getClass().getName());      }           public static void main(String[] args) {              Box<Integer> box1 = new Box<Integer>(new Integer(2));             box1.print();  //列印結果:java.lang.Integer             Box<Double> box2 = new Box<Double>(new Double(1.2));             box2.print();  //列印結果:java.lang.Double                         Box<String> box2 = new Box<String>(new String("abc")); //報錯,因為String類型不是Number的子類             box2.print();      } }
如果加入多個限定,可以用“&”串連起來,但是由於java是單繼承,多個限定中 最多隻能有一個類,而且必須放在第一個位置。例如:

class Box<T extends Number & Cloneable & Comparable >{ //該類型必須為Number的子類並且實現了Cloneable介面和Comparable介面。……}

泛型類的繼承

java是物件導向的進階語言,在一個接受A類參數的地方傳入一個A的子類是允許的,例如:

Object someObject = new Object();Integer someInteger = new Integer(10);someObject =someInteger;   // 因為Integer是Object的子類

這種特性同樣適用型別參數,例如:

Box<Number> box = new Box<Number>();box.add(new Integer(10));   // Integer是Number的子類box.add(new Double(10.1));  // Double同樣是Number的子類

但是,有一種情況很容易引起混淆,例如:

 

//該方法接受的參數類型為Box<Number>public void boxTest(Box<Number> n) {……}//下面兩種調用都會報錯boxTest(Box<Integer>);boxTest(Box<Double>);

雖然Integer和Double都是Number的子類,但是Box<Integer>與<Double>並不是Box<Number>的子類,不存在繼承關係。Box<Integer>與Box<Double>的共同父類是Object。

 

以JDK中的集合類為例,ArrayList<E>實現了 List<E>介面,List<E>介面繼承了 Collection<E>介面,所以,ArrayList<String>是List<String>的子類,而非List<Integer>的子類。三者的繼承關係如下:



類型推斷(Type Inference)

先來看一個例子:

public class Demo{       static <T> T pick(T a1, T a2) {             return a2;      } }

靜態方法pick()在三個地方使用了泛型,分別限定了兩個輸入參數的類型與傳回型別。調用該方法的代碼如下:

Integer ret =Demo.<Integer> pick(new Integer(1), new Integer(2));//前文已經提到,上面的代碼可以簡寫為:Integer ret =Demo. pick(new Integer(1), new Integer(2));

因為java編譯器會根據方法內的參數類型推斷出該方法返回的類型應該為Integer,這種機制稱為 類型推斷

 

那麼問題來了,加入兩個輸入參數為不同的類型,應該返回什麼類型呢?

例如:

pick("d", new ArrayList<String>());

第一個參數為String類型,第二個參數為ArrayList類型,java編譯器就會根據這兩個參數類型來推斷,盡量使傳回型別為最明確的一種。本例中,String與ArrayList都實現了同樣的介面——Serializable,當然,他們也是Object的子類,Serializable類型顯然比Object類型更加明確,因為它的範圍更小更細分,所以最終的傳回型別應該為Serializable:

Serializable s =pick("d", new ArrayList<String>());

在泛型類執行個體化的時候同樣可以利用這種機制簡化代碼,需要注意的是,角括弧“<>”在此時是不能省略的。例如:

 

Map<String,List<String>> myMap = new HashMap<>();//編譯器能推斷出後面的類型,所以可以簡化為:Map<String,List<String>> myMap = new HashMap<>();//但是,不能簡化為:Map<String,List<String>> myMap =new HashMap<>();//因為HashMap()是HashMap原始類型(Raw Type)的建構函式,而非HashMap<String,List<String>>的建構函式,如果不加“<>”編譯器不會進行類型檢查

萬用字元

上文中我們提到過一個例子:

public void boxTest(Box<Number> n){             ……}

該方法只能接受Box <Number>這一種類型的參數,當我們輸入一個Box <Double>或者Box <Integer>時會報錯,儘管Integer與Double是Number的子類。可是如果我們希望該方法可以接受Number以及它的任何子類,該怎麼辦呢?

這時候就要用到萬用字元了,改寫如下:

public void boxTest(Box<? extends Number> n){             ……}


“? extends Number”就代表可以接受Number以及它的子類作為參數。這種聲明方式被稱為上限萬用字元(upper boundedwildcard)。

 

相反地,如果我們希望該方法可以接受Integer,Number以及Object類型的參數怎麼辦呢?應該使用下限萬用字元(lower bounded wildcard):

public void boxTest(Box<? super Integer> n){……}


“? super Integer”代表可以接受Integer以及它的父類作為參數。

 

如果型別參數中既沒有extends 關鍵字,也沒有super關鍵字,只有一個?,代表無限定萬用字元(Unbounded Wildcards)。

通常在兩種情況下會使用無限定萬用字元:

(1)如果正在編寫一個方法,可以使用Object類中提供的功能來實現

(2)代碼實現的功能與型別參數無關,比如List.clear()與List.size()方法,還有經常使用的Class<?>方法,其實現的功能都與型別參數無關。

 

來看一個例子:

public static void printList(List<Object> list) {    for (Object elem : list)        System.out.println(elem + "");    System.out.println();}

該方法只能接受List<Object>型的參數,不接受其他任何類型的參數。但是,該方法實現的功能與List之中參數類型沒有關係,所以我們希望它可以接受包含任何類型的List參數。代碼改動如下:

public static void printList(List<?> list) {    for (Object elem : list)        System.out.println(elem + " ");    System.out.println();}

需要特別注意的是,List<?>與List<Object>並不相同,無論A是什麼類型,List<A>是List<?>的子類,但是,List<A>不是List<Object>的子類。

例如:

List<Number> lb = new ArrayList<>();

List<Integer> la = lb;   // 會報編譯錯誤,儘管Integer是Number的子類,但是List<Integer>不是List<Number>的子類

List<Integer>與List<Number>的關係如下:


所以,下面的代碼是正確的:

List<? extends Integer> intList = new ArrayList<>();List<? extends Number>  numList = intList;  // 不會報錯, List<? extends Integer> 是 List<? extends Number>的子類

下面這張圖介紹了上限萬用字元、下限萬用字元、無限定萬用字元之間的關係:

 

編譯器可以通過類型推斷機制來決定萬用字元的類型,這種情況被稱為萬用字元捕獲。大多時候我們不必擔心萬用字元捕獲,除非編譯器報出了包含“capture of”的錯誤。例如:

public class WildcardError {     void foo(List<?> ii) {             i.set(0, i.get(0));  //會報編譯錯誤}}

上例中,調用List.set(int,E)方法的時候,編譯器無法推斷i.get(0)是什麼類型,就會報錯。

我們可以藉助一個私人的可以捕獲萬用字元的helper方法來解決這種錯誤:

public class WildcardFixed {     void foo(List<?> i) {        fooHelper(i);    }      // 該方法可以確保編譯器通過萬用字元捕獲來推斷出參數類型    private <T> void fooHelper(List<T> l) {        l.set(0, l.get(0));    } }

按照約定俗成的習慣,helper方法的命名方法為“原始方法”+“helper”,上例中,原始方法為“foo”,所以命名為“fooHelper”

 

關於什麼時候該使用上限萬用字元,什麼時候該使用下限萬用字元,應該遵循一下幾項指導規則。

首先將變數分為in-變數out-變數:in-變數持有為當前代碼服務的資料,out-變數持有其他地方需要使用的資料。例如copy(src, dest)方法實現了從src源頭將資料複製到dest目的地的功能,那麼src就是in-變數,而dest就是out-變數。當然,在一些情況下,一個變數可能既是in-變數也是out-變數。

(1)in-變數使用上限萬用字元;

(2)out-變數使用下限萬用字元;

(3)當in-變數可以被Object類中的方法訪問時,使用無限定萬用字元;

(4)一個變數既是in-變數也是out-變數時,不使用萬用字元

注意,上面的規則不適用於方法的傳回型別。


類型擦除(Type Erasure)

java編譯器在處理泛型的時候,會做下面幾件事:

(1)將沒有限定的型別參數用Object替換,保證class檔案中只含有正常的類、介面與方法;

(2)在必要的時候進行類型轉換,保證型別安全;

(3)在泛型的繼承上使用橋接方法(bridge methods)保持多態性。

這類操作被稱為類型擦除

例如:

public class Node<T> {     private T data;    private Node<T> next;     public Node(T data, Node<T> next) }        this.data = data;        this.next = next;    }     public T getData() { return data; }    // ...}

該類中的T沒有被extends或者super限定,會被編譯器替換成Object:

public class Node {     private Object data;    private Node next;     public Node(Object data, Node next) {        this.data = data;        this.next = next;    }     public Object getData() { return data; }    // ...}

如果T加了限定,編譯器會將它替換成合適的類型:

public class Node<T extends Comparable<T>> {     private T data;    private Node<T> next;     public Node(T data, Node<T> next) {        this.data = data;        this.next = next;    }     public T getData() { return data; }    // ...}

改造成:

public class Node {     private Comparable data;    private Node next;     public Node(Comparable data, Node next) {        this.data = data;        this.next = next;    }     public Comparable getData() { return data;}    //...}
 

方法中的類型擦除與之類似。

 

有時候類型擦除會產生一些我們預想不到的情況,下面通過一個例子來分析它是如何產生的。

public class Node<T> {     public T data;     public Node(T data) { this.data = data; }     public void setData(T data) {       System.out.println("Node.setData");        this.data = data;    }} public class MyNode extends Node<Integer>{    public MyNode(Integer data) { super(data);}     public void setData(Integer data) {       System.out.println("MyNode.setData");        super.setData(data);    }}


上面的代碼定義了兩個類,MyNode類繼承了Node類,然後運行下面的代碼:

MyNode mn = new MyNode(5);Node n =mn;           n.setData("Hello");    Integer x =mn.data;    // 拋出ClassCastException異常
 

上面的代碼在類型擦除之後會轉換成下面的形式:

MyNode mn = new MyNode(5);Node n =(MyNode)mn;        n.setData("Hello");Integer x =(String)mn.data;   // 拋出ClassCastException異常
 

我們來看看代碼是怎麼執行的:

(1)n.setData("Hello")調用的其實是MyNode類的setData(Object)方法(從Node類繼承的);

(2)n引用的對象中的data欄位被賦值一個String變數;

(3)mn引用的相同對象中的data預期為Integer類型(mn為Node<Integer>類型);

(4)第四行代碼試圖將一個String賦值給Integer類型的變數,所以引發了ClassCastException異常。

當編譯一個繼承了帶有參數化泛型的類或借口時,編譯器會根據需要建立被稱為bridge method的橋接方法,這是類型擦除中的一部分。

上例中MyNode繼承了Node<Integer>類,類型擦除之後,代碼變為:

class MyNode extends Node {     //編譯器添加的橋接方法    public void setData(Object data){        setData((Integer) data);    }       // MyNode的該方法並沒有覆寫父類的setData(Objectdata)方法,因為參數類型不一樣    public void setData(Integer data) {       System.out.println("MyNode.setData");        super.setData(data);    }     // ...}

注意事項

為了高效地使用泛型,應該注意下面幾個方面:

(1)不能用基本類型執行個體化型別參數

例如

class Pair<K,V> {     private K key;    private V value;     public Pair(K key, V value) {        this.key = key;        this.value = value;    }     // ...}

當建立一個Pair類時,不能用基本類型來替代K,V兩個型別參數。

Pair<int,char> p = new Pair<>(8, 'a'); // 編譯錯誤Pair<Integer,Character> p = new Pair<>(8, 'a'); //正確寫法
 

(2)不可執行個體化型別參數

例如:

public static <E> void append(List<E> list) {    E elem = new E();  // 編譯錯誤    list.add(elem);}

但是,我們可以通過反射執行個體化帶有型別參數的對象:

public static <E> void append(List<E> list, Class<E> cls) throws Exception{    E elem = cls.new Instance();   // 正確    list.add(elem);} List<String> ls = new ArrayList<>();append(ls,String.class);  //傳入型別參數的Class對象

(3)不能在靜態欄位上使用泛型

通過一個反例來說明:

public class MobileDevice <T> {    private static T os;  //假如我們定義了一個帶泛型的靜態欄位     // ...} MobileDevice<Smartphone> phone = new MobileDevice<>();MobileDevice<Pager> pager = new MobileDevice<>();MobileDevice<TabletPC> pc = new MobileDevice<>();

因為靜態變數是類變數,被所有執行個體共用,此時,靜態變數os的真實類型是什麼呢?顯然不能同時是Smartphone、Pager、TabletPC。

這就是為什麼不能在靜態欄位上使用泛型的原因。


(4)不能對帶有參數化型別的類使用 castinstanceof方法

public static<E> void rtti(List<E> list) {    if (list instanceof ArrayList<Integer>){  // 編譯錯誤        // ...    }}

傳給蓋該方法的參數化型別集合為:

S = { ArrayList<Integer>,ArrayList<String> LinkedList<Character>, ... }

運行環境並不會跟蹤型別參數,所以分辨不出ArrayList<Integer>與ArrayList<String>,我們能做的至多是使用無限定萬用字元來驗證list是否為ArrayList:

public static void rtti(List<?> list) {    if (list instanceof ArrayList<?>){  // 正確        // ...    }}

同樣,不能將參數轉換成一個帶參數化型別的對象,除非它的參數化型別為無限定萬用字元(<?>):

List<Integer> li = new ArrayList<>();List<Number>  ln = (List<Number>) li;  // 編譯錯誤

當然,如果編譯器知道參數化型別肯定有效,是允許這種轉換的:

List<String> l1 = ...;ArrayList<String> l2 = (ArrayList<String>)l1;  // 允許轉變,型別參數沒變化

 

(5)不能建立帶有參數化型別的數組

 

例如:

List<Integer>[] arrayOfLists = new List<Integer>[2]; // 編譯錯誤

下面通過兩段代碼來解釋為什麼不行。先來看一個正常的操作:

Object [] strings= new String[2];string s[0] ="hi";   // 插入正常string s[1] =100;    //報錯,因為100不是String類型
 

同樣的操作,如果使用的是泛型數組,就會出問題:

Object[] stringLists = new List<String>[]; // 該句代碼實際上會報錯,但是我們先假定它可以執行string Lists[0] =new ArrayList<String>();   // 插入正常string Lists[1] =new ArrayList<Integer>();  // 該句代碼應該報ArrayStoreException的異常,但是運行環境探測不到
 

(6)不能建立、捕獲泛型異常

泛型類不能直接或間接繼承Throwable類

class MathException<T> extends Exception { /* ... */ }    //編譯錯誤 class QueueFullException<T> extends Throwable { /* ... */} // 編譯錯誤

方法不能捕獲泛型異常:

public static<T extends Exception, J> void execute(List<J> jobs) {    try {        for (J job : jobs)            // ...    } catch (T e) {   // 編譯錯誤        // ...    }}

但是,我們可以在throw子句中使用型別參數:

class Parser<T extends Exception> {    public void parse(File file) throws T{     // 正確        // ...    }}


(7)不能重載經過類型擦除後形參轉化為相同原始類型的方法

先來看一段代碼:

List<String> l1 = new ArrayList<String>();List<Integer> l2 = new ArrayList<Integer>();System.out.println(l1.getClass()== l2.getClass());
列印結果可能與我們猜測的不一樣,列印出的是true,而非false,因為一個泛型類的所有執行個體在運行時具有相同的運行時類(class),而不管他們的實際型別參數。

事實上,泛型之所以叫泛型,就是因為它對所有其可能的型別參數,有同樣的行為;同樣的類可以被當作許多不同的類型。

認識到了這一點,再來看下面的例子:

public class Example {    public void print(Set<String> strSet){ }  //編譯錯誤    public void print(Set<Integer>intSet) { }  //編譯錯誤}

因為Set<String>與Set<Integer>本質上屬於同一個運行時類,在經過類型擦出以後,上面的兩個方法會共用一個方法簽名,相當於一個方法,所以重載出錯。


著作權聲明:本文為博主原創文章,未經博主允許不得轉載。

java泛型詳解

聯繫我們

該頁面正文內容均來源於網絡整理,並不代表阿里雲官方的觀點,該頁面所提到的產品和服務也與阿里云無關,如果該頁面內容對您造成了困擾,歡迎寫郵件給我們,收到郵件我們將在5個工作日內處理。

如果您發現本社區中有涉嫌抄襲的內容,歡迎發送郵件至: info-contact@alibabacloud.com 進行舉報並提供相關證據,工作人員會在 5 個工作天內聯絡您,一經查實,本站將立刻刪除涉嫌侵權內容。

A Free Trial That Lets You Build Big!

Start building with 50+ products and up to 12 months usage for Elastic Compute Service

  • Sales Support

    1 on 1 presale consultation

  • After-Sales Support

    24/7 Technical Support 6 Free Tickets per Quarter Faster Response

  • Alibaba Cloud offers highly flexible support services tailored to meet your exact needs.