1. 程式人生 > >Java基礎(六)深入解讀泛型(1)

Java基礎(六)深入解讀泛型(1)

一名合格的Java程式設計師,當然要經常翻翻JDK的原始碼。經常看JDK的API或者原始碼,我們才能更加了解JDK,才能更加熟悉底層。

一、引出泛型

然而,在看原始碼的過程中,我們經常會看到類似於如下這樣的程式碼:

private void putAllForCreate(Map<? extends K, ? extends V> m) {
        for (Map.Entry<? extends K, ? extends V> e : m.entrySet())
            putForCreate(e.getKey(), e.getValue());
    }

上面程式碼出自HashMap中的一段。程式碼中的泛型,雖然不影響我們看懂這段程式碼,但是屬於半個處女座的我,還是有著強烈的強迫症,就有那麼一股衝動,慫恿著我把它搞清楚。

我們可以觀察一下上面的程式碼,“? extends K”,很明顯,這應該是某種繼承關係的體現,但是什麼時候會用到這種泛型裡的繼承關係呢?除了這種泛型關係,你應該還見過一些其他的表現形式,那麼好,如果你想搞清楚這些,就好好看這篇文章吧,我相信:搞清楚這些,一定會對你做研發時,封裝程式碼,有很大的幫助。

二、泛型的定義

泛型,大家在學習,工作中,一般都會多多少少的接觸過一些。所以,關於定義,我們長話短說,幫大家複習一下即可。

泛型是在JDK1.5增加的,主要用來標記Java集合中元素的資料型別。怎麼講呢?

在JDK1.5之前,一旦把一個物件丟進Java集合中,集合就會忘記物件的型別,把所有的物件當成Object型別處理。當程式衝集合中取出物件時,就需要進行強制型別轉換,這種做法,不僅程式碼看起來很臃腫,而且容易引發型別轉換異常ClassCastException。

而通常情況下使用集合,一個集合,只存放同一型別的東西。即一個定義了的String型別的List,不能盛放Integer型別的資料。

而泛型,可以標記或規定集合中元素的型別,並且在編譯時,檢查其型別。

三、泛型的應用

1、定義泛型介面、類

所謂泛型:就是允許在定義類、介面時,指定型別形參,這個型別形參將在宣告變數、建立物件時確定(即傳入實際的型別引數,也可以成為型別實參)。

搞個例子看看,List、ListIterator、Map介面,都是在定義時指定型別,而型別的確定是在建立變數的時候,如List<String> list= new ArrayList<String>(),建立list變數時,確定型別實參為String型別:

//定義介面時指定了一個型別形參,該形參名為E
public interface ListA<E> extends Collection<E> {
	//在該接口裡,E可作為型別使用
	boolean add(E e); //引數型別
	ListIterator<E> listIterator();
}
//定義介面時,指定你了一個型別形參,該形參名為E
public interface ListIterator<E> extends Iterator<E>{ 
	//在該接口裡E完全可以作為型別使用
	E next();
	E previous();
	void set(E e);
}
//定義該介面時,指定了兩個型別形參,其形參名為K、V
public interface Map<K,V>{
	//在該接口裡K,V完全可以作為型別使用
	Set<K> keySet();
	V put(K key, V value);
	void putAll(Map<? extends K, ? extends V> m);
}


這就是泛型的實質:允許在定義介面、類時,指定型別形參,型別形參在整個介面、類體內可當成型別使用,幾乎所有可使用其他普通型別的地方,都可以使用這個型別形參。

上面方法宣告返回值型別:ListIterator<E>、Set<K>,這表明Set<K>形式 是一種特殊的資料型別,是一種與Set不同的資料型別——我們可以認為Set<K>是Set型別的子類

例如,我們使用List型別時,為E形參傳入String型別實參,則產生了一個新的型別:List<String>型別,我們可以把List<String>想象成E被全部替換成String的特殊List子介面。

我們可以把List<String>想象成E被全部替換成String的特殊List子介面:

//List<String>等同於如下介面
public interface ListString extends List {
	//原來的E形參全部變成String型別實參
	boolean add(String e); //引數型別
	ListIterator<String> listIterator();
}

通過上面這種方式,解決了一個問題:雖然程式只定義了一個List<E>介面,但實際使用時,可以產生無數多個List介面,只要為E傳入不同的型別實參,系統就會多出一個新的List子介面。如List<String>,List<Integer>,List<Long>,List<Boolean>等等。

當然,List<String>絕不會被替換成ListString,系統沒有進行原始碼複製,二進位制程式碼中沒有,磁碟沒有,記憶體中也沒有。

PS:包含泛型宣告的型別可以在定義變數、建立物件時,傳入一個型別實參,從而可以動態生成無數多個邏輯上的子類,但這種子類在物理上並不存在

搞個例項:

//定義Apple類時使用了泛型宣告
public class Apple<E> {
	//使用E型別形參定義屬性
	private E info;
	public Apple() {}
	//下面方法中使用E型別形參來定義方法
	public Apple(E info) {
		this.info = info;
	}
	public E getInfo() {
		return info;
	}
	public void setInfo(E info) {
		this.info = info;
	}
	public static void main(String[] args) {
		//因為傳給T形參的是String實際型別,所以構造器的引數只能是String
		Apple<String> a1 = new Apple<String>("蘋果");
		System.out.println(a1.getInfo());
		
		//因為傳給T形參的是Double實際型別,所以構造器的引數只能是Double或double
		Apple<Double> a2 = new Apple<Double>(5.67);
		System.out.println(a2.getInfo());
	}
}


上面程式定義了一個帶泛型宣告的Apple<T>類,而在Main函式中,實際使用Apple<T>類時會為T形參傳入實際型別,這樣就可以生成如Apple<String>、Apple<Double>……形式的多個邏輯子類(物理上並不存在),這時建立對應的邏輯形參

當建立帶泛型宣告的自定義類,為該類定義構造器時,構造器還是原來的類名,不要增加泛型宣告。例如為Apple<T>類定義構造器,其構造器名依然是Apple,而不是Apple<T>,但呼叫該構造器時,卻可以使用Apple<T>的形式,當然應該為T形參傳入實際的型別引數。

2、從泛型類派生子類

當建立了帶泛型宣告的介面、父類之後,可以為該介面建立實現類,或從父類派生子類,但是:當使用這些介面、父類時不能再包含型別形參。

例如下面程式碼是錯誤的:

//定義類A繼承Apple類,  Apple類後面不能跟型別形參
public class A extends Apple<E> {}

注意:方法中的形參,只有當定義方法時,才可以使用資料形參,當呼叫方法時,必須為這些資料形參傳入實際的資料;與此類似的是:類、介面中的型別形參,只有在定義類、介面時,才可以使用型別形參,當使用類、介面時,應為型別形參傳入實際的型別

所以,總結起來就是:方法、類、介面 中的型別形參,只有在定義時,才可以使用型別形參;在使用時,應為型別形參傳入實際的型別

所以,上面的程式碼可以改為如下(當然刪除後面的String也是可以的):

//使用Apple類時,為T形參傳入String型別
public class A extends Apple<String> {}

public class A extends Apple {}


如果從Apple<String>類派生子類,則在Apple類中所有使用E型別形參的地方都將被替換成String型別,即它的子類將會繼承到String  getInfo()  和 void setInfo(String info)兩個方法,如果子類需要重寫父類的方法,必須注意這一點。

搞個例子,解釋一下:

public class subApple extends Apple<String> {
	
	//正確重寫了父類的方法,返回值與父類Apple<String>的返回值完全相同
	public String getInfo(){
		return "子類:"+super.getInfo();
	}
	
//	下面方法是錯誤的,重寫父類方法時,返回值型別不一致
//	public Object getInfo(){
//		return "子類";
//	}
}


如果使用Apple類時沒有傳入實際的引數型別,系統會將Apple<E>類裡的E形參當成Object型別處理

上面例子是帶泛型宣告的父類派生子類,建立帶泛型宣告的介面的實現類於此幾乎完全一樣。

3、不存在泛型類

ArrayList<String>類,是一種特殊的ArrayList類,這個ArrayList<String>物件只能新增String物件作為集合元素。

但實際上,系統並沒有為ArrayList<String>生成新的class檔案,而且也不會把ArrayList<String>當成新類來處理。

下面看個例子:

import java.util.ArrayList;
import java.util.List;

public class CommonTest {
	public static void main(String[] args) {
		List<String> lst1 = new ArrayList<String>();
		List<Integer> lst2 = new ArrayList<Integer>();
		
		Boolean flag = false;
		flag = lst1.getClass() ==lst2.getClass();
		
		//true
		System.out.println(flag);
	}
}

上面程式的輸出結果是true。因為不管泛型型別的實際型別引數是什麼,它們在執行時總有同樣的類(class)

實際上,泛型對其所有可能的型別引數,都具有同樣的行為,從而可以把相同的類當成許多不同的類來處理。

與此完全一致的是:類的靜態變數和方法也在所有例項間共享,所以在靜態方法、靜態初始化   或者   變數的宣告 和初始化  中,不允許使用型別形參。

下面程式演示了這種錯誤:

public class Test<E> {
	
	E age;
	//下面程式碼錯誤,不能再靜態屬性宣告中使用型別形參
//	static E info;
	
	public void foo(E msg){}
	//下面程式碼錯誤,不能在靜態方法宣告中使用型別形參
//	public static void bar(E msg){}
}


由於系統不併不會正真生成泛型類,所以instanceof運算後,不能使用泛型類,如下程式碼是錯誤的:

import java.util.ArrayList;
import java.util.Collection;
import java.util.List;

public class Test2 {
	public static void main(String[] args) {
		Collection<String> cs = new ArrayList<String>();
		//下面程式碼編譯時引發錯誤:instanceof運算子後不能使用泛型類
//		if (cs instanceof List<String>) {}
	}
}

本文初步講解了泛型基礎內容,下一篇繼續深入探討。