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
CmowerErasure 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、故事的未完待續
“二哥,你今天苦澀了啊!嘿嘿。竟然還有你需要探究的。”三妹開始調皮了起來。
“……”
“不要不好意思嘛,等三妹啥時候探究出來了原因,三妹給你講,好不好?”三妹越說越來勁了。
“……”
“二哥,你還在想泛型通配符的原因啊!那三妹先去預習下個知識點了啊,你思考完了,再給我講!”三妹看着我陷入了沉思,扔下這句話走了。
“……”