十四,泛型(Generics)

1.泛型介紹

Java泛型編程是JDK1.5版本後引入的.泛型讓編程人員可以使用類型抽象,一般用於集合裏面.最大的特色是泛型中的屬性能夠由外部決定. java

類的泛型聲明格式: 編程

class 類名稱<泛型類型, 泛型類型... ...>{} 數組

示例: 安全


List myIntList=new LinkedList(); 
myIntList.add(new Integer(0));
Integer x=(Integer)myIntList.iterator().next();    // next()返回的是Object,因此必須強轉
注意第 3,存儲在 List裏面的對象類型是 Integer,可是在返回列表中元素時 ,仍是必須強制轉換類型 ,這是爲何呢?緣由在於 ,編譯器只能保證迭代器的 next()方法返回的是 Object類型的對象 ,爲保證 Integer變量的類型安全 ,因此必須強制轉換 .


這種轉換不只顯得混亂,更可能致使類型轉換異常ClassCastException,爲保證操做安全,減小轉換髮生錯誤, 而泛型使取出變得很是容易,不須要再使用向下轉型.這就是泛型設計的初衷. 數據結構

示例: app

List<Integer> myIntList=newLinkedList<Integer>();
myIntList.add(new Integer(0));
Integer x=myIntList.iterator().next();
在第 1行代碼中 指定List 中存儲的對象類型爲Integer,這樣在獲取列表中的對象時 ,沒必要強制轉換類型了 .


2泛型示例

下面是一個引用自java.util包中的接口ListIterator的定義,其中用到了泛型技術. 函數

示例: 學習

public interface List<E> {  //類型由外部決定
        void add(E x);      //add的類型與設置類型保持一致
        Iterator<E> iterator();  
    }  
    public interface Iterator<E> {   //類型由外部決定
        E next();                    //返回的類型與傳入類型保持一致
        boolean hasNext();  
    }
這跟原生類型沒有什麼區別 ,只是在接口後面加入了一個尖括號 ,尖括號裏面是一個類型參數 .

List<Integer>表示List中的類型參數E被替換成Integer類型.和以下代碼等價: ui

public interface IntegerList {

    void add(Integer x)

    Iterator<Integer> iterator();

}



3.類型擦除

類型擦除指的是經過類型參數合併,將泛型類型實例關聯到同一份字節碼上.在使用時若是沒有指定泛型類型,則表示擦除泛型類型.擦除後按Object接收.通常不要擦除泛型,由於沒有什麼實際意義,同時以保證操做的安全性. this

編譯器只爲泛型類型生成一份字節碼,並將其實例關聯到這份字節碼上,所以泛型類型中的靜態變量是全部實例共享的.故一個static方法,沒法訪問泛型類的類型參數,由於類尚未實例化,因此,static方法須要使用泛型能力,必須使其成爲泛型方法.

類型擦除的關鍵在於從泛型類型中清除類型參數的相關信息,而且在必要的時候添加類型檢查和類型轉換的方法.在使用泛型時,任何具體的類型都被擦除,惟一知道的是你在使用一個對象.好比:List<String>List<Integer>在運行事實上是相同的類型.他們都被擦除成他們的原生類型,List.

由於編譯的時候會有類型擦除,因此不能經過同一個泛型類的實例來區分方法,由於類型擦除後,兩個方法都是List類型的參數,所以並不能根據泛型類的類型來區分方法.

示例:

public class Info<T> {
	private T msg;
	public Info(T msg) {
		this.msg = msg;
	}
	public T getMsg() {
		return msg;
	}
	public void setMsg(T msg) {
		this.msg = msg;
	}
}
public class GenDemo {
	public static void main(String[] args) {
             Info info0 = new Info(1) ;	// 沒有指定泛型類型,但寫代碼時會有警告
             Info<Object> info1 = new Info<Object>(1) ;	// 沒有指定泛型類型
	     Info<Object> info2 = new Info<Object>("1") ;	// 沒有指定泛型類型
                             //注意的是 info0, info1, info2的類型都是Info,並非Integer和String型
             System.out.println(info0.getClass() == info1.getClass());    //True
	}
}


那麼這就有個問題了 ,既然在編譯的時候會在方法和類中擦除實際類型的信息 ,那麼在返回對象時又是如何知道其具體類型的呢?如 List<String>編譯後會擦除掉 String信息 ,那麼在運行時經過迭代器返回 List中的對象時 ,又是如何知道 List中存儲的是 String類型對象呢?

擦除在方法體中移除了類型信息,因此在運行時的問題就是邊界即對象進入和離開方法的地點,這正是編譯器在編譯期執行類型檢查並插入轉型代碼的地點.泛型中的全部動做都發生在邊界處:對傳遞進來的值進行額外的編譯期檢查,並插入對傳遞出去的值的轉型.


4.泛型和子類型

泛型不支持咱們以前接觸過的向上轉型,爲了完全理解泛型,這裏看個例子:(AppleFruit的子類)

示例:

List<Apple> apples = new ArrayList<Apple>(); // 向上轉型
List<Fruit> fruits = apples;

1行代碼顯然是對的 ,可是第 2行在編譯的時候會出錯 .這會讓人比較納悶的是一個蘋果是水果 ,爲何一箱蘋果就不是一箱水果了呢?能夠這樣考慮 ,假定第 2行代碼沒有問題 ,那麼咱們可使用語句 fruits.add(new Strawberry())fruits中加入草莓了 ,可是這樣的話 ,一個 List中裝入了各類不一樣類型的子類水果 ,這顯然是不能夠的 ,由於咱們在取出 List中的水果對象時 ,就分不清楚到底該轉型爲蘋果仍是草莓了 .

一般來講,若是FooBar的子類型,G是一種帶泛型的類型,G<Foo>不是G<Bar>的子類型.這也是泛型學習裏面最讓人容易混淆的一點.


5.通配符

4.1通配符?

先看一個打印集合中全部元素的代碼.

示例:

//不使用泛型
void printCollection(Collection c) {
    Iterator i=c.iterator();
    for (k=0;k < c.size();k++) {
    System.out.println(i.next());
    }
}

示例:

//使用泛型
void printCollection(Collection<Object> c) {
    for (Object e:c) {
    System.out.println(e);
    }
}
很容易發現 ,使用泛型的版本只能接受元素類型爲 Object類型的集合 ,ArrayList<Object>();若是是 ArrayList<String>,則會編譯時出錯 .


Collection<Object>並非全部集合的超類.而老版本能夠打印任何類型的集合,那麼如何改造新版本以便它能接受全部類型的集合呢?這個問題能夠經過使用通配符來解決.修改後的代碼以下所示:

示例:

//使用通配符?表示能夠接收任何元素類型的集合做爲參數
void printCollection(Collection<?> c) {
    for (Object e:c) {
    System.out.println(e);
    }
}
這裏使用了通配符?指定可使用任何類型的集合做爲參數 .讀取的元素使用了 Object類型來表示 ,這是安全的 ,由於全部的類都是 Object的子類 .

這裏就又出現了另一個問題,若是試圖往使用通配符?的集合中加入(add)對象,就會在編譯時出現錯誤.

須要注意的是,這裏無論加入什麼類型的對象都會出錯.這是由於通配符?表示該集合存儲的元素類型未知,能夠是任何類型.往集合中加入元素須要是一個未知元素類型的子類型,正由於該集合存儲的元素類型未知,因此咱們無法向該集合中添加任何元素.惟一的例外是null,由於null是全部類型的子類型,因此儘管元素類型不知道,可是null必定是它的子類型.

示例:

Collection<?> c=new ArrayList<String>();
c.add(new Object()); //compile time error,無論加入什麼對象都出錯,除了null外。
c.add(null); //OK
另外一方面 ,咱們能夠從 List<?> lists中獲取對象 ,雖然不知道 List中存儲的是什麼類型 ,可是能夠確定的是存儲的類型必定是 Object的子類型 ,因此能夠用 Object類型來獲取值 .for(Object obj: lists),這是合法的 .

4.2邊界通配符

1extends通配符

即泛型的上限,如今假定有一個畫圖的應用,能夠畫各類形狀的圖形,如矩形和圓形等.

示例:

public abstract class Shape {  
         public abstract void draw(Canvas c);  
    }  
     
    public class Circle extends Shape {  
         private int x,y,radius;  
         public void draw(Canvas c) { ... }  
    }  
      
    public class Rectangle extends Shape {
         private int x,y,width,height;  
         public void draw(Canvasc) { ... }  
    }
爲了畫出集合中全部的形狀 ,咱們能夠定義一個函數 ,該函數接受帶有泛型的集合類對象做爲參數 .可是不幸的是 ,咱們只能接收元素類型爲 ShapeList對象 ,而不能接收類型爲 List<Cycle>的對象 ,這在前面已經說過 .爲了解決這個問題 ,因此有了 邊界通配符的概念 .這裏能夠採用 public void drawAll(List<? extends Shape> shapes)來知足條件 ,這樣就能夠接收元素類型爲 Shape子類型的列表做爲參數了 .

示例:

//原始版本
public void drawAll(List<Shape> shapes) {
   for (Shape s:shapes) {
   s.draw(this);
   }
}

示例:

//使用邊界通配符的版本
public void drawAll(List<?exends Shape> shapes) {

    for (Shape s:shapes) {
    s.draw(this);
    }
}
這裏就又有個問題要注意了 ,若是咱們但願在 List<exends Shape> shapes中加入一個矩形對象 ,以下所示:

shapes.add(0, new Rectangle()); //compile-time error
那麼這時會出現一個編譯時錯誤 ,緣由在於:咱們只知道 shapes中的元素時 Shape類型的子類型 ,具體是什麼子類型咱們並不清楚 ,因此咱們不能往 shapes中加入任何類型的對象 . 不過咱們在取出其中對象時, 可使用Shape類型來取值 ,由於雖然咱們不知道列表中的元素類型具體是什麼類型 ,可是咱們確定的是它必定是 Shape類的子類型 .


2)?super通配符

即泛型的下限.

示例:

List<Shape> shapes = new ArrayList<Shape>();
List<? super Cicle> cicleSupers = shapes;
cicleSupers.add(new Cicle()); //OK, subclass of Cicle also OK
cicleSupers.add(new Shape()); //ERROR
這表示 cicle Supers列表存儲的元素爲 Cicle的超類 ,所以咱們能夠往其中加入 Cicle對象或者 Cicle的子類對象 ,可是不能加入 Shape對象 .這裏的緣由在於列表 cicle Supers存儲的元素類型爲 Cicle的超類 ,可是具體是 Cicle的什麼超類並不清楚 .可是咱們能夠肯定的是隻要是 Cicle或者 Circle的子類 ,則必定是與該元素類別兼容 .


3)邊界通配符總結

  • 想從一個數據類型裏獲取數據,使用 ? extends 通配符

  • 若想把對象寫入一個數據結構裏,使用 ? super 通配符

  • 若既想存,又想取,那就別用通配符.


6.泛型方法

考慮實現一個方法,該方法拷貝一個數組中的全部對象到集合中.下面是初始的版本:

示例:

static void fromArrayToCollection(Object[]a, Collection<?> c) {
    for (Object o:a) {
    c.add(o); //compile time error
    }
}
能夠看到顯然會出現編譯錯誤 ,緣由在以前有講過 ,由於集合 c中的類型未知 ,因此不能往其中加入任何的對象(固然 ,null除外) .解決該問題的一種比較好的辦法是使用泛型方法 ,以下所示:

示例:

static <T> void fromArrayToCollection(T[] a, Collection<T>c){
    for(T o : a) {
    c.add(o);// correct
    }
}
注意泛型方法的格式 ,類型參數 <T>須要放在函數返回值以前 .而後在參數和返回值中就可使用泛型參數了 .具體一些調用方法的實例以下:

示例:

Object[] oa = new Object[100];  
    Collection<Object>co = new ArrayList<Object>();  
    fromArrayToCollection(oa, co);// T inferred to be Object  
    String[] sa = new String[100];  
    Collection<String>cs = new ArrayList<String>();  
    fromArrayToCollection(sa, cs);// T inferred to be String  
    fromArrayToCollection(sa, co);// T inferred to be Object  
    Integer[] ia = new Integer[100];  
    Float[] fa = new Float[100];  
    Number[] na = new Number[100];  
    Collection<Number>cn = new ArrayList<Number>();  
    fromArrayToCollection(ia, cn);// T inferred to be Number  
    fromArrayToCollection(fa, cn);// T inferred to be Number  
    fromArrayToCollection(na, cn);// T inferred to be Number  
    fromArrayToCollection(na, co);// T inferred to be Object  
    fromArrayToCollection(na, cs);// compile-time error
注意到咱們調用方法時並不須要傳遞類型參數 ,系統會自動判斷類型參數並調用合適的方法 .固然在某些狀況下須要指定傳遞類型參數 ,好比當存在與泛型方法相同的方法的時候(方法參數類型不一致) ,以下面的一個例子:

示例:

public  <T> void go(T t) {  
        System.out.println("generic function");  
    }  
    public void go(String str) {  
        System.out.println("normal function");  
    }  
    public static void main(String[] args) {  
            FuncGenric fg = new FuncGenric();  
            fg.go("haha");//打印normal function  
            fg.<String>go("haha");//打印generic function  
            fg.go(new Object());//打印generic function  
            fg.<Object>go(new Object());//打印generic function  
    }

如例子中所示 ,當不指定類型參數時 ,調用的是普通的方法 ,若是指定了類型參數 ,則調用泛型方法 .能夠這樣理解 ,由於泛型方法編譯後類型擦除 ,若是不指定類型參數 ,則泛型方法此時至關因而 public void go(Object t).而普通的方法接收參數爲 String類型 ,所以以 String類型的實參調用函數 ,確定會調用形參爲 String的普通方法了 .若是是以 Object類型的實參調用函數 ,則會調用泛型方法 .


7.補充說明

1)方法重載

JAVA裏面方法重載是不能經過返回值類型來區分的,好比代碼一中一個類中定義兩個以下的方法是不允許的.可是當參數爲泛型類型時,倒是能夠的.以下面代碼二中所示,雖然形參通過類型擦除後都爲List類型,可是返回類型不一樣,這是能夠的.

示例1:

/*代碼一:編譯時錯誤*/   
public class Erasure{  
            public void test(int i){  
                System.out.println("Sting");  
            }  
            public int test(int i){  
                System.out.println("Integer");  
            }  
  }

示例2:

/*代碼二:正確 */  
     public class Erasure{  
                public void test(List<String> ls){  
                    System.out.println("Sting");  
                }  
                public int test(List<Integer> li){  
                    System.out.println("Integer");  
                }  
      }


2 )泛型類型是被全部調用共享的

全部泛型類的實例都共享同一個運行時類,類型參數信息會在編譯時被擦除.所以考慮以下代碼,雖然ArrayList<String>ArrayList<Integer>類型參數不一樣,可是他們都共享ArrayList,因此結果會是true.

示例:

List<String>l1 = new ArrayList<String>();
List<Integer>l2 = new ArrayList<Integer>();
System.out.println(l1.getClass() == l2.getClass()); //True

3instanceof

不能對確切的泛型類型使用instanceOf操做.以下面的操做是非法的,編譯時會出錯.

示例:

Collection cs = new ArrayList<String>();
if (cs instanceof Collection<String>){…}// compile error.
//若是改爲instanceof Collection<?>則不會出錯.


4) 泛型數組問題

不能建立一個確切泛型類型的數組.以下面代碼會出錯.

List<String>[] lsa = new ArrayList<String>[10]; //compile error.
由於若是能夠這樣, 那麼考慮以下代碼, 會致使運行時錯誤.

示例:

List<String>[] lsa = new ArrayList<String>[10]; // 實際上並不容許這樣建立數組  
    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
所以 只能建立帶通配符的泛型數組 , 下面例子所示 ,這回能夠經過編譯 ,可是在倒數第二行代碼中必須顯式的轉型才行 ,即使如此 ,最後仍是會拋出類型轉換異常 ,由於存儲在 lsa中的是 List<Integer>類型的對象 ,而不是 List<String>類型 .最後一行代碼是正確的 ,類型匹配 ,不會拋出異常 .

示例:

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  
String s = (String) lsa[1].get(0);// run time error, but cast is explicit  
Integer it = (Integer)lsa[1].get(0); // OK


參考資料:

http://qiemengdao.iteye.com/blog/1525624



20150419


JAVA學習筆記系列

--------------------------------------------

                    聯繫方式

--------------------------------------------

        Weibo: ARESXIONG

        E-Mail: aresxdy@gmail.com

------------------------------------------------
相關文章
相關標籤/搜索