起源
“二哥,要不我上大學的時候也學習編程吧?”有一天,三妹突發奇想地問我。
“你確定要做一名程序媛嗎?”
“我覺得女生做程序員,有著天大的優勢,尤其是我這種長相甜美的。”三妹開始認真了起來。
“好像是啊,遇到女生提問,我好像一直蠻熱情的。”
“二哥,你不是愛好寫作嘛,還是一個 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{ /<code>
privateObject[]elementData;
privateintsize=0;
publicArraylist(intinitialCapacity){
this.elementData=newObject[initialCapacity];
}
publicbooleanadd(Ee){
elementData[size++]=e;
returntrue;
}
EelementData(intindex){
return(E)elementData[index];
}
}
一個泛型類就是具有一個或多個類型變量的類。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{ /<code>
publicT[]toArray(T[]a){
return(T[])Arrays.copyOf(elementData,size,a.getClass());
}
}
不過,說實話,泛型方法的定義看起來略顯晦澀。來一副圖吧(注意:方法返回類型和方法參數類型至少需要一個)。
現在,我們來調用一下泛型方法。
<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>
類型變量
既然如此,那如果泛型類使用了限定符 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>
類型變量
通過以上兩個例子說明,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{ /<code>
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>
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){ /<code>
for(inti=0;i<src.size>dest.set(i,src.get(i));
}
}
/<src.size>
故事未完待續
閱讀更多 千鋒python學院 的文章