淺析Java泛型

什麼是泛型?

泛型是JDK 1.5的一項新特性,它的本質是參數化類型(Parameterized Type)的應用,也就是說所操做的數據類型被指定爲一個參數,在用到的時候在指定具體的類型。這種參數類型能夠用在類、接口和方法的建立中,分別稱爲泛型類泛型接口泛型方法html

基本術語介紹java

以ArrayList<E>和ArrayList<Integer>爲例
整個ArrayList<E>稱爲泛型類型
ArrayList<E>中的E稱爲類型變量或者類型形參
整個ArrayList<Integer>稱爲參數化的類型
ArrayList<Integer>中的Integer稱爲類型參數的實例或者類型實參
ArrayList<Integer>中的<Integer>念爲typeof Integer
ArrayList稱爲原始類型

爲何使用泛型?

泛型使類型(類和接口)在定義類、接口和方法時成爲參數,好處在於:數組

  • 強化類型安全,因爲泛型在編譯期進行類型檢查,從而保證類型安全,減小運行期的類型轉換異常。
  • 提升代碼複用,泛型能減小重複邏輯,編寫更簡潔的代碼。
  • 類型依賴關係更加明確,接口定義更加優好,加強了代碼和文檔的易讀性。

一個簡單的例子安全

public class Test1 {
    public static void main(String[] args) {
        List list = new ArrayList();
        list.add("kiwen1");
        list.add("kiwen2");
        list.add(123);

        for (int i = 0; i < list.size(); i++) {
            String name = (String) list.get(i); 
            System.out.println("name:" + name);
        }
        
    }
}
//輸出結果
name:kiwen1
name:kiwen2
Exception in thread "main" java.lang.ClassCastException: java.lang.Integer cannot be cast to java.lang.String
    at DateTest.Test1.main(Test.java:17)

從上面例子能夠看出,定義了一個List類型的集合,向其中加入了兩個字符串類型的值和 一個Integer類型的值。此時list默認的類型爲Object類型。但這裏有兩個問題,在循環中,一是當獲取一個值時必須進行強制類型轉換,二是沒有錯誤檢查。因爲定義了name爲String類型,運行時將Integer轉成String會產生錯誤。即編譯階段正常,而運行時會出現「java.lang.ClassCastException」異常。所以,致使此類錯誤編碼過程當中不易被發現。app

public class Test2 {
    public static void main(String[] args) {
        List<String> list = new ArrayList<String>();
        list.add("kiwen1");
        list.add("kiwen2");
        //list.add(123);   //提示編譯錯誤

        for (int i = 0; i < list.size(); i++) {
            String name = list.get(i);
            System.out.println("name:" + name);
        }
    }
}
//輸出結果
name:kiwen1
name:kiwen2

該段代碼採用泛型寫法後,向list加入一個Integer類型的對象時會出現編譯錯誤,經過List<String>,直接限定了list集合中只能含有String類型的元素,從而在輸出時處無須進行強制類型轉換。由於此時,集合可以記住元素的類型信息,編譯器已經可以確認它是String類型了。dom

經過上面的例子能夠證實,在編譯以後程序會採起去泛型化的措施。也就是說Java中的泛型,只在編譯階段有效。在編譯過程當中,正確檢驗泛型結果後,會將泛型的相關信息擦出,而且在對象進入和離開方法的邊界處添加類型檢查和類型轉換的方法。也就是說,泛型信息不會進入到運行時階段。ide

對此總結成一句話:泛型類型在邏輯上看以當作是多個不一樣的類型,實際上都是相同的基本類型。函數

泛型類

在類的申明時指定參數,即構成了泛型類。泛型類的類型參數部分能夠有一個或多個類型參數,它們之間用逗號分隔。這些類稱爲參數化類或參數化類型,由於它們接受一個或多個參數。ui

定義一個簡單的泛型類

//在實例化泛型類時,必須指定T的具體類型
public class Test<T>{
    //在類中聲明的泛型整個類裏面均可以用,除了靜態部分,由於泛型是實例化時聲明的。
    //靜態區域的代碼在編譯時就已經肯定,只與類相關
    class A <E>{
        T t;
    }
    //類裏面的方法或類中再次聲明同名泛型是容許的,而且該泛型會覆蓋掉父類的同名泛型T
    class B <T>{
        T t;
    }
    //靜態內部類也可使用泛型,實例化時賦予泛型實際類型
    static class C <T> {
        T t;
    }
    public static void main(String[] args) {
        //報錯,不能使用T泛型,由於泛型T屬於實例不屬於類
//        T t = null;
    }

    //key這個成員變量的類型爲T,T的類型由外部指定
    private T key;

    public Test(T key) { //泛型構造方法形參key的類型也爲T,T的類型由外部指定
        this.key = key;
    }

    public T getKey(){ //泛型方法getKey的返回值類型爲T,T的類型由外部指定
        return key;
    }
}

在使用泛型的時候若是傳入泛型實參,則會根據傳入的泛型實參作相應的限制,此時泛型纔會起到本應起到的限制做用,可是,泛型的類型參數只能是類類型,不能是簡單類型。若是不傳入泛型類型實參的話,在泛型類中使用泛型的方法或成員變量定義的類型能夠爲任何的類型。換句話說,泛型類能夠當作普通類的工廠。this

//不傳入泛型類型實參
List list = new List();
list.add(123);
list.add("hello");
//傳入的泛型實參
List list<String> = new List<String>();
list.add("hello");

如何繼承一個泛型類

若是不傳入具體的類型,則子類也須要指定類型參數,

class Son<T> extends Test<T>{}

若是傳入具體參數,則子類不須要指定類型參數

class Son extends Test<String>{}

泛型接口

泛型接口與泛型類的定義基本一致

//定義一個泛型接口
public interface Generator<T> {
    public T next();
}

如何實現一個泛型接口

一個簡單的例子

/**
 * 傳入泛型實參時:
 * 定義一個生產器實現這個接口,雖然咱們只建立了一個泛型接口Generator<T>
 * 可是咱們能夠爲T傳入無數個實參,造成無數種類型的Generator接口。
 * 在實現類實現泛型接口時,如已將泛型類型傳入實參類型,則全部使用泛型的地方都要替換成傳入的實參類型
 * 即:Generator<T>,public T next();中的的T都要替換成傳入的String類型。
 */

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)];
    }
}

泛型通配符

咱們知道,Box<Number>Box<Integer>實際上都是Box類型,如今須要繼續探討一個問題,那麼在邏輯上,相似於Box<Number>Box<Integer>是否能夠當作具備父子關係的泛型類型呢?

爲了弄清楚這個問題,咱們使用Box這個泛型類繼續看下面的例子:

package DateTest;
public class GenericTest {
    class Box<T> {
        private T data;
        public Box() {
        }
        public Box(T data) {
            this.data = data;
        }
        public T getData() {
            return data;
        }
        public void setData(T data) {
        this.data = data;
        }
    }
    public static void main(String[] args) {
        Box<Number> name = new Box<Number>(99);
        Box<Integer> age = new Box<Integer>(712);
        getData(name);
        //The method getData(Box<Number>) in the type GenericTest is 
        //not applicable for the arguments (Box<Integer>)
        getData(age);   // 1
    }
    public static void getData(Box<Number> data){
        System.out.println("data :" + data.getData());
    }
}

經過提示信息咱們能夠看到Box<Number>不能被看做爲Box<Integer>的子類。由此能夠看出:同一種泛型能夠對應多個版本(由於參數類型是不肯定的),不一樣版本的泛型類實例是不兼容的。

回到上面的例子,如何解決上面的問題?總不能爲了定義一個新的方法來處理Generic類型的類,這顯然與java中的多態理念相違背。所以咱們須要一個在邏輯上能夠表示同時是Box<Integer>Box<Number>的父類的引用類型。由此類型通配符應運而生。

咱們能夠將上面的方法改一下:

public static void getData(Box<?> data) {
    System.out.println("data :" + data.getData());
}

類型通配符通常是使用?代替具體的類型實參,注意, 此處的?和Number、String、Integer同樣都是一種實際的類型,能夠把?當作全部類型的父類。是一種真實的類型。

能夠解決當具體類型不肯定的時候,這個通配符就是 ? ;當操做類型時,不須要使用類型的具體功能時,只使用Object類中的功能。那麼能夠用 ? 通配符來表未知類型。

泛型無限定通配符

無限定通配符使用<?>的格式,表明未知類型的泛型。 當可使用Object類中提供的功能或當代碼獨立於類型參數來實現方法時,這樣的參數可使用任何對象。

public void showKeyValue1(List<?> list) {
    for (Object item : list) {  
        System.out.print(item + " ");   
    } 
}

泛型上限通配符

通配符上界使用<? extends T>的格式,意思是須要一個T類型或者T類型的子類,通常T類型都是一個具體的類型,例以下面的代碼。

//只能傳入number的子類或者number
public void showKeyValue2(List<? extends Number> list) {  
    for (Number number : list) {  
        System.out.print(number.intValue()+" ");   
    }  
}
//假如傳入String類型,list.add("hello");會提示
//The method add(Number) in the type List<Number> is not applicable for the arguments (String)

不管傳入的是何種類型的集合,咱們均可以使用其父類的方法統一處理。

泛型下限通配符

通配符下界使用<? super T>的格式,意思是須要一個T類型或者T類型的父類,通常T類型都是一個具體的類型,例以下面的代碼。

//只能傳入Integer的父類或者Integer
public void showKeyValue3(List<? super Integer> obj){
    System.out.println(obj);
}
//假如傳入String類型,list.add("hello");會提示
//The method add(Number) in the type List<Number> is not applicable for the arguments (String)

泛型方法

在java中,泛型類的定義很是簡單,可是泛型方法就比較複雜了。
尤爲是咱們見到的大多數泛型類中的成員方法也都使用了泛型,有的甚至泛型類中也包含着泛型方法,這樣在初學者中很是容易將泛型方法理解錯了。
泛型類,是在實例化類的時候指明泛型的具體類型;泛型方法,是在調用方法的時候指明泛型的具體類型 。

定義泛型方法以下

調用泛型方法以下

定義泛型方法時,必須在返回值前邊加一個<T>,來聲明這是一個泛型方法,持有一個泛型T,而後才能夠用泛型T做爲方法的返回值。
Class<T>的做用就是指明泛型的具體類型,而Class<T>類型的變量c,能夠用來建立泛型類的對象。
爲何要用變量c來建立對象呢?既然是泛型方法,就表明着咱們不知道具體的類型是什麼,也不知道構造方法如何,所以沒有辦法去new一個對象,但能夠利用變量c的newInstance方法去建立對象,也就是利用反射建立對象。
泛型方法要求的參數是Class<T>類型,而Class.forName()方法的返回值也是Class<T>,所以能夠用Class.forName()做爲參數。其中,forName()方法中的參數是何種類型,返回的Class<T>就是何種類型。在本例中,forName()方法中傳入的是User類的完整路徑,所以返回的是Class<User>類型的對象,所以調用泛型方法時,變量c的類型就是Class<User>,所以泛型方法中的泛型T就被指明爲User,所以變量obj的類型爲User。

/**
 * 泛型方法的基本介紹
 * 說明:
 *     1)public 與 返回值中間<T>很是重要,能夠理解爲聲明此方法爲泛型方法。
 *     2)只有聲明瞭<T>的方法纔是泛型方法,泛型類中的使用了泛型的成員方法並非泛型方法。
 *     3)<T>代表該方法將使用泛型類型T,此時才能夠在方法中使用泛型類型T。
 *     4)與泛型類的定義同樣,此處T能夠隨便寫爲任意標識,常見的如T、E、K、V等形式的參數經常使用於表示泛型。
 */
public class Generic<T> {
    public T name;
    public  Generic(){}
    public Generic(T param){
        name=param;
    }
    public T m(){
        return name;
    }
    public <E> void m1(E e){ }
    public <T> T m2(T e){ }
}

上面代碼中,m()方法不是泛型方法,m1()和m2()都是泛型方法。

泛型方法與可變參數

public class Test{
    @Test
    public void test () {
        printMsg("hello1",1,"hello2",2.0,false);
        print("hello1","hello2", "hello3");
    }
    //普通可變參數只能適配一種類型
    public void print(String ... args) {
        for(String t : args){
            System.out.println(t);
        }
    }
    //泛型的可變參數能夠匹配全部類型的參數。。有點無敵
    public <T> void printMsg( T... args){
        for(T t : args){
            System.out.println(t);
        }
    }
}

靜態方法與泛型

若是在類中定義使用泛型的靜態方法,須要添加額外的泛型聲明(將這個方法定義成泛型方法)。即便靜態方法要使用泛型類中已經聲明過的泛型也不能夠。

public class Test<T> {
    private static T num;//此時編譯器會提示錯誤信息
    public static void test(T t){//此時編譯器會提示錯誤信息:
        ...
        //Cannot make a static reference to the non-static type T
    }
}

由於靜態方法和靜態變量屬於類全部,而泛型類中的泛型參數的實例化是在建立泛型類型對象時指定的,因此若是不建立對象,根本沒法肯定參數類型。可是靜態泛型方法是可使用的,咱們前面說過,泛型方法裏面的那個類型和泛型類那個類型徹底是兩回事。

public class StaticGenerator<T> {
    public static <T> void show(T t){
    }
}

泛型的限制

Java泛型不能使用原始類型

使用泛型,原始類型不能做爲類型參數傳遞。例如

Test<Integer> test = new Test<Integer>();

若是將int原始類型傳遞給Test類,那麼編譯器會報錯。爲了不這種狀況,須要傳遞Integer對象而不是int原始類型。

Java泛型不能使用實例

類型參數不能用於在方法中實例化其對象。例如

public static <T> void show(Test<T> test) {
   //compiler error
   //Cannot instantiate the type T
   //T item = new T();  
   //test.add(item);
}

若是須要實現這樣的功能,可使用反射。

public static <T> void show(Test<T> test, Class<T> clazz) 
   throws InstantiationException, IllegalAccessException{
   T item = clazz.newInstance();   // OK
   test.add(item);
   System.out.println("Item showed.");
}

Java泛型不能使用靜態域

使用泛型時,類型參數不容許爲靜態(static)。因爲靜態變量在對象之間共享,所以編譯器沒法肯定要使用的類型。若是容許靜態類型參數。

Java泛型不能轉換類型

除非由無界通配符進行參數化,不然不容許轉換爲參數化類型。

Test<Integer> integerTest = new Test<Integer>();
Test<Number> numberTest = new Test<Number>();
//Compiler Error: Cannot cast from Test<Number> to Test<Integer>,下面用法是錯誤的
integerTest = (Test<Integer>)numberTest;
//可使用無界通配符進行轉換功能
private static void add(Test<?> test){
   Test<Integer> integerTest = (Test<Integer>)test;
}

Java泛型instanceof運算符

由於編譯器使用類型擦除,運行時不會跟蹤類型參數,因此在Test <Integer>Test <String>之間的運行時差別沒法使用instanceOf運算符進行驗證。

Java泛型不能使用異常

泛型類不容許直接或間接擴展Throwable類。

//The generic class Test<T> may not subclass java.lang.Throwable
class Test<T> extends Exception {}
//The generic class Test1<T> may not subclass java.lang.Throwable
class Test1<T> extends Throwable {}

在一個方法中,不容許捕獲一個類型參數的實例,但throws子句中容許使用類型參數。

public static <T extends Exception, J> 
   void execute(List<J> jobs) {
      try {
         for (J job : jobs){}

         // compile-time error
         //Cannot use the type parameter T in a catch block
      } catch (T e) { 
         // ...
   }
}

//
class Test<T extends Exception>  {
   private int t;

   public void add(int t) throws T {
      this.t = t;
   }
   public int get() {
      return t;
   }   
}

Java泛型不能使用數組

錯誤代碼

//Cannot create a generic array of Test<Integer>
Test<Integer>[] arrayOfLists = new Test<Integer>[2];

由於編譯器使用類型擦除,類型參數被替換爲Object,用戶能夠向數組添加任何類型的對象。但在運行時,代碼將沒法拋出ArrayStoreException

下面使用Sun的一篇文檔的一個例子來講明這個問題:

List<String>[] lsa = new List<String>[10]; // Not really allowed.    
Object o = lsa;    
Object[] oa = (Object[]) o;    
List<Integer> li = new ArrayList<Integer>();    
li.add(new Integer(3));    
oa[1] = li; // Unsound, but passes run time store check    
String s = lsa[1].get(0); // Run-time error: ClassCastException.

這種狀況下,因爲JVM泛型的擦除機制,在運行時JVM是不知道泛型信息的,因此能夠給oa[1]賦上一個ArrayList而不會出現異常,可是在取出數據的時候卻要作一次類型轉換,因此就會出現ClassCastException,若是能夠進行泛型數組的聲明,上面說的這種狀況在編譯期將不會出現任何的警告和錯誤,只有在運行時纔會出錯。

而對泛型數組的聲明進行限制,對於這樣的狀況,能夠在編譯期提示代碼有類型安全問題,比沒有任何提示要強不少。
下面採用通配符的方式是被容許的:數組的類型不能夠是類型變量,除非是採用通配符的方式,由於對於通配符的方式,最後取出數據是要作顯式的類型轉換的。

List<?>[] lsa = new List<?>[10]; // OK, array of unbounded wildcard type.    
Object o = lsa;    
Object[] oa = (Object[]) o;    
List<Integer> li = new ArrayList<Integer>();    
li.add(new Integer(3));    
oa[1] = li; // Correct.    
Integer i = (Integer) lsa[1].get(0); // OK

Java泛型不能重載

一個類不容許有兩個重載方法,能夠在類型擦除後使用相同的簽名。

類型擦除

Java編譯器應用類型擦除。 類型擦除是指編譯器使用實際的類或橋接方法替換泛型參數的過程。 在類型擦除中,編譯器確保不會建立額外的類,而且沒有運行時開銷。

Java編譯器編譯泛型的步驟:

  1. 檢查泛型的類型 ,得到目標類型
  2. 擦除類型變量,並替換爲限定類型(T爲無限定的類型變量,用Object替換)
  3. 調用相關函數,並將結果強制轉換爲目標類型。
ArrayList<String> arrayString=new ArrayList<String>();     
 ArrayList<Integer> arrayInteger=new ArrayList<Integer>();     
 System.out.println(arrayString.getClass()==arrayInteger.getClass());

上面代碼輸入結果爲 true,可見經過運行時獲取的類信息是徹底一致的,泛型類型被擦除了!

如何擦除:
當擦除泛型類型後,留下的就只有原始類型了,例如上面的代碼,原始類型就是ArrayList。擦除類型變量,並替換爲限定類型(T爲無限定的類型變量,用Object替換),以下所示

擦除以前:

//泛型類型  
class Pair<T> {    
    private T value;    
    public T getValue() {    
        return value;    
    }    
    public void setValue(T  value) {    
        this.value = value;    
    }    
}

擦除以後:

//原始類型  
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泛型內部原理,請參考文章泛型的內部原理:類型擦除以及類型擦除帶來的問題Java泛型深刻了解

總結

在使用泛型類時,因爲 Java 泛型的類型參數之實際類型在編譯時會被消除,因此沒法在運行時得知其類型參數的類型。雖然傳入了不一樣的泛型實參,但並無真正生成不一樣的類型,傳入不一樣泛型實參的泛型類在內存上實際只有一個,但在邏輯上,咱們能夠理解爲多個不一樣的泛型類型。

參考文章

http://www.javashuo.com/article/p-obpwunof-co.html

http://www.javashuo.com/article/p-tdlvtkjj-bd.html

https://www.cnblogs.com/iyangyuan/archive/2013/04/09/3011274.html

相關文章
相關標籤/搜索