12.24 這樣學Java 泛型,不信你還不會

起源

“二哥,要不我上大學的時候也學習編程吧?”有一天,三妹突發奇想地問我。

“你確定要做一名程序媛嗎?”

“我覺得女生做程序員,有著天大的優勢,尤其是我這種長相甜美的。”三妹開始認真了起來。

“好像是啊,遇到女生提問,我好像一直蠻熱情的。”

“二哥,你不是愛好寫作嘛,還是一個 Java 程序員,不妨寫個專欄,名字就叫《教妹學 Java》。

“真的很服氣你們零零後,蠻有想法的。剛好我最近在寫 Java 系列的專欄,不妨試一試!”

PS:親愛的讀者朋友們,我們今天就從晦澀難懂的“泛型”開始吧!(子標題是三妹提出來的,內容由二哥我來回答)


二哥,為什麼要設計泛型啊?


三妹啊,聽哥慢慢給你講啊。

Java 在 5.0 時增加了泛型機制,據說專家們為此花費了 5 年左右的時間(聽起來很不容易)。有了泛型之後,尤其是對集合類的使用,就變得更規範了。

看下面這段簡單的代碼。

<code>ArrayList<string>list=newArrayList<string>();
list.add("沉默王二");
Stringstr=list.get(0);
但在沒有泛型之前該怎麼辦呢?
/<string>/<string>/<code>

首先,我們需要使用 Object 數組來設計 Arraylist 類。

<code>classArraylist{
privateObject[]objs;
privateinti=0;
publicvoidadd(Objectobj){
objs[i++]=obj;
}

publicObjectget(inti){
returnobjs[i];
}
}
然後,我們向 Arraylist 中存取數據。
/<code>
<code>Arraylistlist=newArraylist();
list.add("沉默王二");
list.add(newDate());
Stringstr=(String)list.get(0);
你有沒有發現兩個問題:
/<code>
  • Arraylist 可以存放任何類型的數據(既可以存字符串,也可以混入日期),因為所有類都繼承自 Object 類。
  • 從 Arraylist 取出數據的時候需要強制類型轉換,因為編譯器並不能確定你取的是字符串還是日期。

對比一下,你就能明顯地感受到泛型的優秀之處:使用類型參數解決了元素的不確定性——參數類型為 String 的集合中是不允許存放其他類型元素的,取出數據的時候也不需要強制類型轉換了。

二哥,怎麼設計泛型啊?

三妹啊,你一個小白只要會用泛型就行了,還想設計泛型啊?!不過,既然你想了解,那麼哥義不容辭。

首先,我們來按照泛型的標準重新設計一下 Arraylist 類。

<code>classArraylist{
privateObject[]elementData;
privateintsize=0;

publicArraylist(intinitialCapacity){
this.elementData=newObject[initialCapacity];
}

publicbooleanadd(Ee){
elementData[size++]=e;
returntrue;
}

EelementData(intindex){

return(E)elementData[index];
}
}
/<code>

一個泛型類就是具有一個或多個類型變量的類。Arraylist 類引入的類型變量為 E(Element,元素的首字母),使用尖括號 <> 括起來,放在類名的後面。

然後,我們可以用具體的類型(比如字符串)替換類型變量來實例化泛型類。

<code>Arraylist<string>list=newArraylist<string>();
list.add("沉默王三");
Stringstr=list.get(0);/<string>/<string>/<code>

Date 類型也可以的。

<code>Arraylist<date>list=newArraylist<date>();
list.add(newDate());
Datedate=list.get(0);/<date>/<date>/<code>

其次,我們還可以在一個非泛型的類(或者泛型類)中定義泛型方法。

<code>classArraylist{
publicT[]toArray(T[]a){
return(T[])Arrays.copyOf(elementData,size,a.getClass());
}
}
/<code>

不過,說實話,泛型方法的定義看起來略顯晦澀。來一副圖吧(注意:方法返回類型和方法參數類型至少需要一個)。

這樣學Java 泛型,不信你還不會

現在,我們來調用一下泛型方法。

<code>Arraylist<string>list=newArraylist<>(4);
list.add("沉");
list.add("默");
list.add("王");
list.add("二");

String[]strs=newString[4];
strs=list.toArray(strs);

for(Stringstr:strs){
System.out.println(str);
}/<string>/<code>

最後,我們再來說說泛型變量的限定符 extends。在解釋這個限定符之前,我們假設有三個類,它們之間的定義是這樣的。

<code>classWanglaoer{
publicStringtoString(){
return"王老二";
}
}

classWangerextendsWanglaoer{
publicStringtoString(){
return"王二";
}
}

classWangxiaoerextendsWanger{
publicStringtoString(){
return"王小二";
}
}/<code>

我們使用限定符 extends 來重新設計一下 Arraylist 類。

<code>classArraylist<eextendswanger>{
}/<eextendswanger>/<code>

當我們向 Arraylist 中添加 Wanglaoer 元素的時候,編譯器會提示錯誤:Arraylist 只允許添加 Wanger 及其子類 Wangxiaoer 對象,不允許添加其父類 Wanglaoer。

<code>Arraylist<wanger>list=newArraylist<>(3);
list.add(newWanger());
list.add(newWanglaoer());
//Themethodadd(Wanger)inthetypeArraylist<wanger>isnotapplicableforthearguments
//(Wanglaoer)
list.add(newWangxiaoer());/<wanger>/<wanger>/<code>

也就是說,限定符 extends 可以縮小泛型的類型範圍。


二哥,聽說虛擬機沒有泛型?


三妹,你功課做得可以啊,連虛擬機都知道了啊。哥可以肯定地回答你,虛擬機是沒有泛型的。

囉嗦一句哈。我們編寫的 Java 代碼(也就是源碼,後綴為 .java 的文件)是不能夠被操作系統直接識別的,需要先編譯,生成 .class 文件(也就是字節碼文件)。然後 Java 虛擬機(JVM)會充當一個翻譯官的角色,把字節碼翻譯給操作系統能聽得懂的語言,告訴它該幹嘛。

怎麼確定虛擬機沒有泛型呢?我們需要把泛型類的字節碼進行反編譯——強烈推薦超神反編譯工具 Jad !

現在,在命令行中敲以下代碼吧(反編譯 Arraylist 的字節碼文件 Arraylist.class)。

<code>jadArraylist.class/<code>

命令執行完後,會生成一個 Arraylist.jad 的文件,用文本編輯工具打開後的結果如下。

<code>//DecompiledbyJadv1.5.8g.Copyright2001PavelKouznetsov.
//Jadhomepage:http://www.kpdus.com/jad.html
//Decompileroptions:packimports(3)
//SourceFileName:Arraylist.java

packagecom.cmower.java_demo.fanxing;

importjava.util.Arrays;

classArraylist
{

publicArraylist(intinitialCapacity)
{
size=0;
elementData=newObject[initialCapacity];
}

publicbooleanadd(Objecte)
{
elementData[size++]=e;
returntrue;
}

ObjectelementData(intindex)
{
returnelementData[index];
}

privateObjectelementData[];
privateintsize;
}/<code>

類型變量 消失了,取而代之的是 Object !

既然如此,那如果泛型類使用了限定符 extends,結果會怎麼樣呢?我們先來看看 Arraylist2 的源碼。

<code>classArraylist2<eextendswanger>{
privateObject[]elementData;
privateintsize=0;

publicArraylist2(intinitialCapacity){
this.elementData=newObject[initialCapacity];
}

publicbooleanadd(Ee){
elementData[size++]=e;
returntrue;
}

EelementData(intindex){
return(E)elementData[index];
}
}
字節碼文件Arraylist2.class使用Jad反編譯後的結果如下。
//DecompiledbyJadv1.5.8g.Copyright2001PavelKouznetsov.
//Jadhomepage:http://www.kpdus.com/jad.html
//Decompileroptions:packimports(3)
//SourceFileName:Arraylist2.java

packagecom.cmower.java_demo.fanxing;


//Referencedclassesofpackagecom.cmower.java_demo.fanxing:
//Wanger

classArraylist2
{

publicArraylist2(intinitialCapacity)
{
size=0;
elementData=newObject[initialCapacity];
}

publicbooleanadd(Wangere)
{
elementData[size++]=e;
returntrue;

}

WangerelementData(intindex)
{
return(Wanger)elementData[index];
}

privateObjectelementData[];
privateintsize;
}/<eextendswanger>/<code>

類型變量 不見了,E 被替換成了 Wanger。

通過以上兩個例子說明,Java 虛擬機會將泛型的類型變量擦除,並替換為限定類型(沒有限定的話,就用 Object)。


二哥,類型擦除會有什麼問題嗎?


三妹啊,你還別說,類型擦除真的會有一些“問題”。

我們來看一下這段代碼。

<code>publicclassCmower{

publicstaticvoidmethod(Arraylist<string>list){
System.out.println("Arraylist<string>list");
}

publicstaticvoidmethod(Arraylist<date>list){
System.out.println("Arraylist<date>list");

}

}/<date>/<date>/<string>/<string>/<code>

在淺層的意識上,我們會想當然地認為 Arraylist<string> list 和 Arraylist<date> list 是兩種不同的類型,因為 String 和 Date 是不同的類。/<date>/<string>

但由於類型擦除的原因,以上代碼是不會通過編譯的——編譯器會提示一個錯誤(這正是類型擦除引發的那些“問題”):

Erasure of method method(Arraylist) is the same as another method in type
Cmower

Erasure of method method(Arraylist) is the same as another method in type
Cmower

大致的意思就是,這兩個方法的參數類型在擦除後是相同的。

也就是說,method(Arraylist<string> list) 和 method(Arraylist<date> list) 是同一種參數類型的方法,不能同時存在。類型變量 String 和 Date 在擦除後會自動消失,method 方法的實際參數是 Arraylist list。/<date>/<string>

有句俗話叫做:“百聞不如一見”,但即使見到了也未必為真——泛型的擦除問題就可以很好地佐證這個觀點。

二哥,聽說泛型還有通配符?

三妹啊,哥突然覺得你很適合作一枚可愛的程序媛啊!你這預習的功課做得可真到家啊,連通配符都知道!

通配符使用英文的問號(?)來表示。在我們創建一個泛型對象時,可以使用關鍵字 extends 限定子類,也可以使用關鍵字 super 限定父類。

為了更好地解釋通配符,我們需要對 Arraylist 進行一些改進。

<code>classArraylist{
privateObject[]elementData;
privateintsize=0;

publicArraylist(intinitialCapacity){
this.elementData=newObject[initialCapacity];
}

publicbooleanadd(Ee){
elementData[size++]=e;
returntrue;
}

publicEget(intindex){
return(E)elementData[index];
}

publicintindexOf(Objecto){
if(o==null){
for(inti=0;i<size>if(elementData[i]==null)
returni;
}else{
for(inti=0;i<size>if(o.equals(elementData[i]))
returni;
}
return-1;
}

publicbooleancontains(Objecto){
returnindexOf(o)>=0;
}

publicStringtoString(){
StringBuildersb=newStringBuilder();

for(Objecto:elementData){
if(o!=null){

Ee=(E)o;
sb.append(e.toString());
sb.append(',').append('');
}
}
returnsb.toString();
}

publicintsize(){
returnsize;
}

publicEset(intindex,Eelement){
EoldValue=(E)elementData[index];
elementData[index]=element;
returnoldValue;
}
}/<size>/<size>
/<code>

1)新增 indexOf(Object o) 方法,判斷元素在 Arraylist 中的位置。注意參數為 Object 而不是泛型 E。

2)新增 contains(Object o) 方法,判斷元素是否在 Arraylist 中。注意參數為 Object 而不是泛型 E。

3)新增 toString() 方法,方便對 Arraylist 進行打印。

4)新增 set(int index, E element) 方法,方便對 Arraylist 元素的更改。

你知道,Arraylist<wanger> list = new Arraylist<wangxiaoer>(); 這樣的語句是無法通過編譯的,儘管 Wangxiaoer 是 Wanger 的子類。但如果我們確實需要這種 “向上轉型” 的關係,該怎麼辦呢?這時候就需要通配符來發揮作用了。/<wangxiaoer>/<wanger>

利用 extends Wanger> 形式的通配符,可以實現泛型的向上轉型,來看例子。

<code>Arraylist 
list2=newArraylist<>(4);
list2.add(null);
//list2.add(newWanger());
//list2.add(newWangxiaoer());

Wangerw2=list2.get(0);
//Wangxiaoerw3=list2.get(1);/<code>

list2 的類型是 Arraylist extends Wanger>,翻譯一下就是,list2 是一個 Arraylist,其類型是 Wanger 及其子類。

注意,“關鍵”來了!list2 並不允許通過 add(E e) 方法向其添加 Wanger 或者 Wangxiaoer 的對象,唯一例外的是 null。為什麼不能存呢?原因還有待探究(苦澀)。

那就奇了怪了,既然不讓存放元素,那要 Arraylist extends Wanger> 這樣的 list2 有什麼用呢?

雖然不能通過 add(E e) 方法往 list2 中添加元素,但可以給它賦值。

<code>Arraylist<wanger>list=newArraylist<>(4);

Wangerwanger=newWanger();
list.add(wanger);

Wangxiaoerwangxiaoer=newWangxiaoer();
list.add(wangxiaoer);

Arraylistlist2=list;

Wangerw2=list2.get(1);
System.out.println(w2);

System.out.println(list2.indexOf(wanger));
System.out.println(list2.contains(newWangxiaoer()));/<wanger>/<code>

Arraylist extends Wanger> list2 = list; 語句把 list 的值賦予了 list2,此時 list2 == list。由於 list2 不允許往其添加其他元素,所以此時它是安全的——我們可以從容地對 list2 進行 get()、indexOf() 和 contains()。想一想,如果可以向 list2 添加元素的話,這 3 個方法反而變得不太安全,它們的值可能就會變。

利用 super Wanger> 形式的通配符,可以向 Arraylist 中存入父類是 Wanger 的元素,來看例子。

<code>Arraylistlist3=newArraylist<>(4);
list3.add(newWanger());
list3.add(newWangxiaoer());

//Wangerw3=list3.get(0);/<code>

需要注意的是,無法從 Arraylist super Wanger> 這樣類型的 list3 中取出數據。為什麼不能取呢?原因還有待探究(再次苦澀)。

雖然原因有待探究,但結論是明確的: extends T> 可以取數據, super T> 可以存數據。那麼利用這一點,我們就可以實現數組的拷貝—— extends T> 作為源(保證源不會發生變化), super T> 作為目標(可以保存值)。

<code>publicclassCollections{
publicstaticvoidcopy(Arraylistdest,Arraylistsrc){
for(inti=0;i<src.size>dest.set(i,src.get(i));
}
}

/<src.size>
/<code>

故事未完待續


分享到:


相關文章: