教妹學(xué) Java 第 46 講:泛型
文章的開(kāi)頭,簡(jiǎn)短說(shuō)兩句。
這次鄭州的水災(zāi),讓我一直揪著心,不能聚精會(huì)神地寫(xiě)文章。作為一名普通老百姓,除了安慰一下朋友和同學(xué),就只能略盡一點(diǎn)綿薄之力了。
災(zāi)難無(wú)情,生活還要繼續(xù),技術(shù)文還是要更。
“二哥,為什么要設(shè)計(jì)泛型啊?”三妹開(kāi)門(mén)見(jiàn)山地問(wèn)。
“三妹啊,聽(tīng)哥慢慢給你講啊。”我說(shuō)。
Java 在 1.5 時(shí)增加了泛型機(jī)制,據(jù)說(shuō)專(zhuān)家們?yōu)榇嘶ㄙM(fèi)了 5 年左右的時(shí)間(聽(tīng)起來(lái)很不容易)。有了泛型之后,尤其是對(duì)集合類(lèi)的使用,就變得更規(guī)范了。
看下面這段簡(jiǎn)單的代碼。
ArrayList<String> list = new ArrayList<String>();
list.add("沉默王二");
String str = list.get(0);
“三妹,你能想象到在沒(méi)有泛型之前該怎么辦嗎?”
“嗯,想不到,還是二哥你說(shuō)吧。”
嗯,我們可以使用 Object 數(shù)組來(lái)設(shè)計(jì) Arraylist 類(lèi)。
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 中存取數(shù)據(jù)。
Arraylist list = new Arraylist();
list.add("沉默王二");
list.add(new Date());
String str = (String)list.get(0);
“三妹,你有沒(méi)有發(fā)現(xiàn)這兩個(gè)問(wèn)題?”
Arraylist 可以存放任何類(lèi)型的數(shù)據(jù)(既可以存字符串,也可以混入日期),因?yàn)樗蓄?lèi)都繼承自 Object 類(lèi)。 從 Arraylist 取出數(shù)據(jù)的時(shí)候需要強(qiáng)制類(lèi)型轉(zhuǎn)換,因?yàn)榫幾g器并不能確定你取的是字符串還是日期。
“嗯嗯,是的呢。”三妹說(shuō)。
對(duì)比一下,你就能明顯地感受到泛型的優(yōu)秀之處:使用類(lèi)型參數(shù)解決了元素的不確定性——參數(shù)類(lèi)型為 String 的集合中是不允許存放其他類(lèi)型元素的,取出數(shù)據(jù)的時(shí)候也不需要強(qiáng)制類(lèi)型轉(zhuǎn)換了。
“二哥,那怎么才能設(shè)計(jì)一個(gè)泛型呢?”
“三妹啊,你一個(gè)小白只要會(huì)用泛型就行了,還想設(shè)計(jì)泛型啊?!不過(guò),既然你想了解,那么哥義不容辭。”
首先,我們來(lái)按照泛型的標(biāo)準(zhǔn)重新設(shè)計(jì)一下 Arraylist 類(lèi)。
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];
}
}
一個(gè)泛型類(lèi)就是具有一個(gè)或多個(gè)類(lèi)型變量的類(lèi)。Arraylist 類(lèi)引入的類(lèi)型變量為 E(Element,元素的首字母),使用尖括號(hào) <> 括起來(lái),放在類(lèi)名的后面。
然后,我們可以用具體的類(lèi)型(比如字符串)替換類(lèi)型變量來(lái)實(shí)例化泛型類(lèi)。
Arraylist<String> list = new Arraylist<String>();
list.add("沉默王三");
String str = list.get(0);
Date 類(lèi)型也可以的。
Arraylist<Date> list = new Arraylist<Date>();
list.add(new Date());
Date date = list.get(0);
其次,我們還可以在一個(gè)非泛型的類(lèi)(或者泛型類(lèi))中定義泛型方法。
class Arraylist<E> {
public <T> T[] toArray(T[] a) {
return (T[]) Arrays.copyOf(elementData, size, a.getClass());
}
}
不過(guò),說(shuō)實(shí)話(huà),泛型方法的定義看起來(lái)略顯晦澀。來(lái)一副圖吧(注意:方法返回類(lèi)型和方法參數(shù)類(lèi)型至少需要一個(gè))。

現(xiàn)在,我們來(lái)調(diào)用一下泛型方法。
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);
}
然后,我們?cè)賮?lái)說(shuō)說(shuō)泛型變量的限定符 extends。
在解釋這個(gè)限定符之前,我們假設(shè)有三個(gè)類(lèi),它們之間的定義是這樣的。
class Wanglaoer {
public String toString() {
return "王老二";
}
}
class Wanger extends Wanglaoer{
public String toString() {
return "王二";
}
}
class Wangxiaoer extends Wanger{
public String toString() {
return "王小二";
}
}
我們使用限定符 extends 來(lái)重新設(shè)計(jì)一下 Arraylist 類(lèi)。
class Arraylist<E extends Wanger> {
}
當(dāng)我們向 Arraylist 中添加 Wanglaoer 元素的時(shí)候,編譯器會(huì)提示錯(cuò)誤:Arraylist 只允許添加 Wanger 及其子類(lèi) Wangxiaoer 對(duì)象,不允許添加其父類(lèi) 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());
也就是說(shuō),限定符 extends 可以縮小泛型的類(lèi)型范圍。
“哦,明白了。”三妹若有所思的點(diǎn)點(diǎn)頭,“二哥,聽(tīng)說(shuō)虛擬機(jī)沒(méi)有泛型?”
“三妹,你功課做得可以啊。哥可以肯定地回答你,虛擬機(jī)是沒(méi)有泛型的。”
“怎么確定虛擬機(jī)有沒(méi)有泛型呢?”三妹問(wèn)。
“只要我們把泛型類(lèi)的字節(jié)碼進(jìn)行反編譯就看到了!”用反編譯工具將 class 文件反編譯后,我說(shuō),“三妹,你看。”
// 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;
}
類(lèi)型變量 <E> 消失了,取而代之的是 Object !
“既然如此,那如果泛型類(lèi)使用了限定符 extends,結(jié)果會(huì)怎么樣呢?”三妹這個(gè)問(wèn)題問(wèn)的很巧妙。
來(lái)看這段代碼。
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];
}
}
反編譯后的結(jié)果如下。
// 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;
}
“你看,類(lèi)型變量 <E extends Wanger> 不見(jiàn)了,E 被替換成了 Wanger”,我說(shuō),“通過(guò)以上兩個(gè)例子說(shuō)明,Java 虛擬機(jī)會(huì)將泛型的類(lèi)型變量擦除,并替換為限定類(lèi)型(沒(méi)有限定的話(huà),就用 Object)”
“二哥,類(lèi)型擦除會(huì)有什么問(wèn)題嗎?”三妹又問(wèn)了一個(gè)很有水平的問(wèn)題。
“三妹啊,你還別說(shuō),類(lèi)型擦除真的會(huì)有一些問(wèn)題。”我說(shuō),“來(lái)看一下這段代碼。”
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");
}
}
在淺層的意識(shí)上,我們會(huì)想當(dāng)然地認(rèn)為 Arraylist<String> list 和 Arraylist<Date> list 是兩種不同的類(lèi)型,因?yàn)?String 和 Date 是不同的類(lèi)。
但由于類(lèi)型擦除的原因,以上代碼是不會(huì)通過(guò)編譯的——編譯器會(huì)提示一個(gè)錯(cuò)誤(這正是類(lèi)型擦除引發(fā)的那些“問(wèn)題”):
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
大致的意思就是,這兩個(gè)方法的參數(shù)類(lèi)型在擦除后是相同的。
也就是說(shuō),method(Arraylist<String> list) 和 method(Arraylist<Date> list) 是同一種參數(shù)類(lèi)型的方法,不能同時(shí)存在。類(lèi)型變量 String 和 Date 在擦除后會(huì)自動(dòng)消失,method 方法的實(shí)際參數(shù)是 Arraylist list。
有句俗話(huà)叫做:“百聞不如一見(jiàn)”,但即使見(jiàn)到了也未必為真——泛型的擦除問(wèn)題就可以很好地佐證這個(gè)觀(guān)點(diǎn)。
“哦,明白了。二哥,聽(tīng)說(shuō)泛型還有通配符?”
“三妹啊,哥突然覺(jué)得你很適合作一枚可愛(ài)的程序媛啊!你這預(yù)習(xí)的功課做得可真到家啊,連通配符都知道!”
通配符使用英文的問(wèn)號(hào)(?)來(lái)表示。在我們創(chuàng)建一個(gè)泛型對(duì)象時(shí),可以使用關(guān)鍵字 extends 限定子類(lèi),也可以使用關(guān)鍵字 super 限定父類(lèi)。
我們來(lái)看下面這段代碼。
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 中的位置。注意參數(shù)為 Object 而不是泛型 E。
2)新增 contains(Object o) 方法,判斷元素是否在 Arraylist 中。注意參數(shù)為 Object 而不是泛型 E。
3)新增 toString() 方法,方便對(duì) Arraylist 進(jìn)行打印。
4)新增 set(int index, E element) 方法,方便對(duì) Arraylist 元素的更改。
因?yàn)榉盒筒脸脑颍?code style="font-size: 14px;padding: 2px 4px;border-radius: 4px;margin-right: 2px;margin-left: 2px;background-color: rgba(27, 31, 35, 0.05);font-family: "Operator Mono", Consolas, Monaco, Menlo, monospace;word-break: break-all;color: rgb(53, 179, 120);">Arraylist<Wanger> list = new Arraylist<Wangxiaoer>(); 這樣的語(yǔ)句是無(wú)法通過(guò)編譯的,盡管 Wangxiaoer 是 Wanger 的子類(lèi)。但如果我們確實(shí)需要這種 “向上轉(zhuǎn)型” 的關(guān)系,該怎么辦呢?這時(shí)候就需要通配符來(lái)發(fā)揮作用了。
利用 <? extends Wanger> 形式的通配符,可以實(shí)現(xiàn)泛型的向上轉(zhuǎn)型,來(lái)看例子。
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 的類(lèi)型是 Arraylist<? extends Wanger>,翻譯一下就是,list2 是一個(gè) Arraylist,其類(lèi)型是 Wanger 及其子類(lèi)。
注意,“關(guān)鍵”來(lái)了!list2 并不允許通過(guò) add(E e) 方法向其添加 Wanger 或者 Wangxiaoer 的對(duì)象,唯一例外的是 null。
“那就奇了怪了,既然不讓存放元素,那要 Arraylist<? extends Wanger> 這樣的 list2 有什么用呢?”三妹好奇地問(wèn)。
雖然不能通過(guò) 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; 語(yǔ)句把 list 的值賦予了 list2,此時(shí) list2 == list。由于 list2 不允許往其添加其他元素,所以此時(shí)它是安全的——我們可以從容地對(duì) list2 進(jìn)行 get()、indexOf() 和 contains()。想一想,如果可以向 list2 添加元素的話(huà),這 3 個(gè)方法反而變得不太安全,它們的值可能就會(huì)變。
利用 <? super Wanger> 形式的通配符,可以向 Arraylist 中存入父類(lèi)是 Wanger 的元素,來(lái)看例子。
Arraylist<? super Wanger> list3 = new Arraylist<>(4);
list3.add(new Wanger());
list3.add(new Wangxiaoer());
// Wanger w3 = list3.get(0);
需要注意的是,無(wú)法從 Arraylist<? super Wanger> 這樣類(lèi)型的 list3 中取出數(shù)據(jù)。
“三妹,關(guān)于泛型,這里還有一篇很不錯(cuò)的文章,你等會(huì)去看一下。”我說(shuō)。
https://www.pdai.tech/md/java/basic/java-basic-x-generic.html
“對(duì)泛型機(jī)制講的也很透徹,你結(jié)合二哥給你講的這些,再深入的學(xué)習(xí)一下。”
“好的,二哥。”
PS:點(diǎn)擊「閱讀原文」可直達(dá)《教妹學(xué)Java》專(zhuān)欄的在線(xiàn)閱讀地址,可以收藏夾伺候一波了!
