00、故事的起源
“二哥,要不我上大學的時候也學習程式設計吧?”有一天,三妹突發奇想地問我。
“你確定要做一名程式媛嗎?”
“我覺得女生做程式設計師,有著天大的優勢,尤其是我這種長相甜美的。”三妹開始認真了起來。
“好像是啊,遇到女生提問,我好像一直蠻熱情的。”
“二哥,你不是愛好寫作嘛,還是一個 Java 程式設計師,不妨寫個專欄,名字就叫《教妹學 Java》。我高考完就開始跟著你學習程式設計,還能省下一筆培訓費。”三妹看起來已經替我籌劃好了呀。
“真的很服氣你們零零後,蠻有想法的。剛好我最近在寫 Java 系列的專欄,不妨試一試!”
PS:親愛的讀者朋友們,我們今天就從晦澀難懂的“泛型”開始吧!(子標題是三妹提出來的,內容由二哥我來回答)
01、二哥,為什麼要設計泛型啊?
三妹啊,聽哥慢慢給你講啊。
Java 在 5.0 時增加了泛型機制,據說專家們為此花費了 5 年左右的時間(聽起來很不容易)。有了泛型之後,尤其是對集合類的使用,就變得更規範了。
看下面這段簡單的程式碼。
ArrayList<String> list = new ArrayList<String>();
list.add("沉默王二");
String str = list.get(0);
複製程式碼
但在沒有泛型之前該怎麼辦呢?
首先,我們需要使用 Object 陣列來設計 Arraylist
類。
class Arraylist {
private Object[] objs;
private int i = 0;
public void add(Object obj) {
objs[i++] = obj;
}
public Object get(int i) {
return objs[i];
}
}
複製程式碼
然後,我們向 Arraylist
中存取資料。
Arraylist list = new Arraylist();
list.add("沉默王二");
list.add(new Date());
String str = (String)list.get(0);
複製程式碼
你有沒有發現兩個問題:
- Arraylist 可以存放任何型別的資料(既可以存字串,也可以混入日期),因為所有類都繼承自 Object 類。
- 從 Arraylist 取出資料的時候需要強制型別轉換,因為編譯器並不能確定你取的是字串還是日期。
對比一下,你就能明顯地感受到泛型的優秀之處:使用型別引數解決了元素的不確定性——引數型別為 String 的集合中是不允許存放其他型別元素的,取出資料的時候也不需要強制型別轉換了。
02、二哥,怎麼設計泛型啊?
三妹啊,你一個小白只要會用泛型就行了,還想設計泛型啊?!不過,既然你想了解,那麼哥義不容辭。
首先,我們來按照泛型的標準重新設計一下 Arraylist
類。
class Arraylist<E> {
private Object[] elementData;
private int size = 0;
public Arraylist(int initialCapacity) {
this.elementData = new Object[initialCapacity];
}
public boolean add(E e) {
elementData[size++] = e;
return true;
}
E elementData(int index) {
return (E) elementData[index];
}
}
複製程式碼
一個泛型類就是具有一個或多個型別變數的類。Arraylist 類引入的型別變數為 E(Element,元素的首字母),使用尖括號 <>
括起來,放在類名的後面。
然後,我們可以用具體的型別(比如字串)替換型別變數來例項化泛型類。
Arraylist<String> list = new Arraylist<String>();
list.add("沉默王三");
String str = list.get(0);
複製程式碼
Date 型別也可以的。
Arraylist<Date> list = new Arraylist<Date>();
list.add(new Date());
Date date = list.get(0);
複製程式碼
其次,我們還可以在一個非泛型的類(或者泛型類)中定義泛型方法。
class Arraylist<E> {
public <T> T[] toArray(T[] a) {
return (T[]) Arrays.copyOf(elementData, size, a.getClass());
}
}
複製程式碼
不過,說實話,泛型方法的定義看起來略顯晦澀。來一副圖吧(注意:方法返回型別和方法引數型別至少需要一個)。
現在,我們來呼叫一下泛型方法。
Arraylist<String> list = new Arraylist<>(4);
list.add("沉");
list.add("默");
list.add("王");
list.add("二");
String [] strs = new String [4];
strs = list.toArray(strs);
for (String str : strs) {
System.out.println(str);
}
複製程式碼
最後,我們再來說說泛型變數的限定符 extends
。在解釋這個限定符之前,我們假設有三個類,它們之間的定義是這樣的。
class Wanglaoer {
public String toString() {
return "王老二";
}
}
class Wanger extends Wanglaoer{
public String toString() {
return "王二";
}
}
class Wangxiaoer extends Wanger{
public String toString() {
return "王小二";
}
}
複製程式碼
我們使用限定符 extends
來重新設計一下 Arraylist
類。
class Arraylist<E extends Wanger> {
}
複製程式碼
當我們向 Arraylist
中新增 Wanglaoer
元素的時候,編譯器會提示錯誤:Arraylist
只允許新增 Wanger
及其子類 Wangxiaoer
物件,不允許新增其父類 Wanglaoer
。
Arraylist<Wanger> list = new Arraylist<>(3);
list.add(new Wanger());
list.add(new Wanglaoer());
// The method add(Wanger) in the type Arraylist<Wanger> is not applicable for the arguments
// (Wanglaoer)
list.add(new Wangxiaoer());
複製程式碼
也就是說,限定符 extends
可以縮小泛型的型別範圍。
03、二哥,聽說虛擬機器沒有泛型?
三妹,你功課做得可以啊,連虛擬機器都知道了啊。哥可以肯定地回答你,虛擬機器是沒有泛型的。
囉嗦一句哈。我們編寫的 Java 程式碼(也就是原始碼,字尾為 .java 的檔案)是不能夠被作業系統直接識別的,需要先編譯,生成 .class 檔案(也就是位元組碼檔案)。然後 Java 虛擬機器(JVM)會充當一個翻譯官的角色,把位元組碼翻譯給作業系統能聽得懂的語言,告訴它該幹嘛。
怎麼確定虛擬機器沒有泛型呢?我們需要把泛型類的位元組碼進行反編譯——強烈推薦超神反編譯工具 Jad !
現在,在命令列中敲以下程式碼吧(反編譯 Arraylist
的位元組碼檔案 Arraylist.class
)。
jad Arraylist.class
複製程式碼
命令執行完後,會生成一個 Arraylist.jad 的檔案,用文字編輯工具開啟後的結果如下。
// Decompiled by Jad v1.5.8g. Copyright 2001 Pavel Kouznetsov.
// Jad home page: http://www.kpdus.com/jad.html
// Decompiler options: packimports(3)
// Source File Name: Arraylist.java
package com.cmower.java_demo.fanxing;
import java.util.Arrays;
class Arraylist
{
public Arraylist(int initialCapacity)
{
size = 0;
elementData = new Object[initialCapacity];
}
public boolean add(Object e)
{
elementData[size++] = e;
return true;
}
Object elementData(int index)
{
return elementData[index];
}
private Object elementData[];
private int size;
}
複製程式碼
型別變數 <E>
消失了,取而代之的是 Object !
既然如此,那如果泛型類使用了限定符 extends
,結果會怎麼樣呢?我們先來看看 Arraylist2
的原始碼。
class Arraylist2<E extends Wanger> {
private Object[] elementData;
private int size = 0;
public Arraylist2(int initialCapacity) {
this.elementData = new Object[initialCapacity];
}
public boolean add(E e) {
elementData[size++] = e;
return true;
}
E elementData(int index) {
return (E) elementData[index];
}
}
複製程式碼
位元組碼檔案 Arraylist2.class
使用 Jad 反編譯後的結果如下。
// Decompiled by Jad v1.5.8g. Copyright 2001 Pavel Kouznetsov.
// Jad home page: http://www.kpdus.com/jad.html
// Decompiler options: packimports(3)
// Source File Name: Arraylist2.java
package com.cmower.java_demo.fanxing;
// Referenced classes of package com.cmower.java_demo.fanxing:
// Wanger
class Arraylist2
{
public Arraylist2(int initialCapacity)
{
size = 0;
elementData = new Object[initialCapacity];
}
public boolean add(Wanger e)
{
elementData[size++] = e;
return true;
}
Wanger elementData(int index)
{
return (Wanger)elementData[index];
}
private Object elementData[];
private int size;
}
複製程式碼
型別變數 <E extends Wanger>
不見了,E 被替換成了 Wanger
。
通過以上兩個例子說明,Java 虛擬機器會將泛型的型別變數擦除,並替換為限定型別(沒有限定的話,就用 Object
)。
04、二哥,型別擦除會有什麼問題嗎?
三妹啊,你還別說,型別擦除真的會有一些“問題”。
我們來看一下這段程式碼。
public class Cmower {
public static void method(Arraylist<String> list) {
System.out.println("Arraylist<String> list");
}
public static void method(Arraylist<Date> list) {
System.out.println("Arraylist<Date> list");
}
}
複製程式碼
在淺層的意識上,我們會想當然地認為 Arraylist<String> list
和 Arraylist<Date> list
是兩種不同的型別,因為 String 和 Date 是不同的類。
但由於型別擦除的原因,以上程式碼是不會通過編譯的——編譯器會提示一個錯誤(這正是型別擦除引發的那些“問題”):
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
。
有句俗話叫做:“百聞不如一見”,但即使見到了也未必為真——泛型的擦除問題就可以很好地佐證這個觀點。
05、二哥,聽說泛型還有萬用字元?
三妹啊,哥突然覺得你很適合作一枚可愛的程式媛啊!你這預習的功課做得可真到家啊,連萬用字元都知道!
萬用字元使用英文的問號(?)來表示。在我們建立一個泛型物件時,可以使用關鍵字 extends
限定子類,也可以使用關鍵字 super
限定父類。
為了更好地解釋萬用字元,我們需要對 Arraylist
進行一些改進。
class Arraylist<E> {
private Object[] elementData;
private int size = 0;
public Arraylist(int initialCapacity) {
this.elementData = new Object[initialCapacity];
}
public boolean add(E e) {
elementData[size++] = e;
return true;
}
public E get(int index) {
return (E) elementData[index];
}
public int indexOf(Object o) {
if (o == null) {
for (int i = 0; i < size; i++)
if (elementData[i]==null)
return i;
} else {
for (int i = 0; i < size; i++)
if (o.equals(elementData[i]))
return i;
}
return -1;
}
public boolean contains(Object o) {
return indexOf(o) >= 0;
}
public String toString() {
StringBuilder sb = new StringBuilder();
for (Object o : elementData) {
if (o != null) {
E e = (E)o;
sb.append(e.toString());
sb.append(',').append(' ');
}
}
return sb.toString();
}
public int size() {
return size;
}
public E set(int index, E element) {
E oldValue = (E) elementData[index];
elementData[index] = element;
return oldValue;
}
}
複製程式碼
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 的子類。但如果我們確實需要這種 “向上轉型” 的關係,該怎麼辦呢?這時候就需要萬用字元來發揮作用了。
利用 <? extends Wanger>
形式的萬用字元,可以實現泛型的向上轉型,來看例子。
Arraylist<? extends Wanger> list2 = new Arraylist<>(4);
list2.add(null);
// list2.add(new Wanger());
// list2.add(new Wangxiaoer());
Wanger w2 = list2.get(0);
// Wangxiaoer w3 = list2.get(1);
複製程式碼
list2 的型別是 Arraylist<? extends Wanger>
,翻譯一下就是,list2 是一個 Arraylist
,其型別是 Wanger
及其子類。
注意,“關鍵”來了!list2 並不允許通過 add(E e)
方法向其新增 Wanger
或者 Wangxiaoer
的物件,唯一例外的是 null
。為什麼不能存呢?原因還有待探究(苦澀)。
那就奇了怪了,既然不讓存放元素,那要 Arraylist<? extends Wanger>
這樣的 list2 有什麼用呢?
雖然不能通過 add(E e)
方法往 list2 中新增元素,但可以給它賦值。
Arraylist<Wanger> list = new Arraylist<>(4);
Wanger wanger = new Wanger();
list.add(wanger);
Wangxiaoer wangxiaoer = new Wangxiaoer();
list.add(wangxiaoer);
Arraylist<? extends Wanger> list2 = list;
Wanger w2 = list2.get(1);
System.out.println(w2);
System.out.println(list2.indexOf(wanger));
System.out.println(list2.contains(new Wangxiaoer()));
複製程式碼
Arraylist<? extends Wanger> list2 = list;
語句把 list 的值賦予了 list2,此時 list2 == list
。由於 list2 不允許往其新增其他元素,所以此時它是安全的——我們可以從容地對 list2 進行 get()
、indexOf()
和 contains()
。想一想,如果可以向 list2 新增元素的話,這 3 個方法反而變得不太安全,它們的值可能就會變。
利用 <? super Wanger>
形式的萬用字元,可以向 Arraylist 中存入父類是 Wanger
的元素,來看例子。
Arraylist<? super Wanger> list3 = new Arraylist<>(4);
list3.add(new Wanger());
list3.add(new Wangxiaoer());
// Wanger w3 = list3.get(0);
複製程式碼
需要注意的是,無法從 Arraylist<? super Wanger>
這樣型別的 list3 中取出資料。為什麼不能取呢?原因還有待探究(再次苦澀)。
雖然原因有待探究,但結論是明確的:<? extends T>
可以取資料,<? super T>
可以存資料。那麼利用這一點,我們就可以實現陣列的拷貝——<? extends T>
作為源(保證源不會發生變化),<? super T>
作為目標(可以儲存值)。
public class Collections {
public static <T> void copy(Arraylist<? super T> dest, Arraylist<? extends T> src) {
for (int i = 0; i < src.size(); i++)
dest.set(i, src.get(i));
}
}
複製程式碼
06、故事的未完待續
“二哥,你今天苦澀了啊!嘿嘿。竟然還有你需要探究的。”三妹開始調皮了起來。
“......”
“不要不好意思嘛,等三妹啥時候探究出來了原因,三妹給你講,好不好?”三妹越說越來勁了。
“......”
“二哥,你還在想泛型萬用字元的原因啊!那三妹先去預習下個知識點了啊,你思考完了,再給我講!”三妹看著我陷入了沉思,扔下這句話走了。
“......”