Java 泛型編程所說(shuō)的類型擦除到底是什么?
大部分語(yǔ)言都支持泛型,泛型是一種語(yǔ)言機(jī)制,各種語(yǔ)言的實(shí)現(xiàn)機(jī)制都不太一樣,例如C++使用模板方式來(lái)實(shí)現(xiàn)泛型,而 Java 中用類型擦除機(jī)制來(lái)實(shí)現(xiàn)泛型。
什么是泛型
在 Java 中,不會(huì)泛型,寸步難行。泛型可能是一個(gè) Java 初學(xué)者需要攻克的第一個(gè)難點(diǎn)。隨便跟著一門教程或 任何一本《Java入門到精通》,前面關(guān)于變量、關(guān)鍵字、語(yǔ)法(if、while、for等等)這些基本上是一看就懂,而當(dāng)內(nèi)容來(lái)到泛型的時(shí)候,大部分人可能就突然感覺(jué)沒(méi)那么輕松了。
如果沒(méi)有編程經(jīng)驗(yàn)的話,可能需要練習(xí)一段時(shí)間才能完全掌握泛型編程概念和技巧,這么說(shuō)吧,有些人寫了好幾年代碼,碰到泛型的時(shí)候可能還是不太熟練。
說(shuō)到Java泛型,最明顯的標(biāo)志就是 <> 。
泛型是什么呢?通俗的說(shuō)就是一個(gè)類型是沒(méi)有固定類型的,即可以是Integer 也可以是 Long,還可能是你自定義的類。
泛型使類型(類和接口)能夠在定義類、接口和方法時(shí)成為參數(shù)。與方法聲明中使用的更熟悉的形式參數(shù)非常相似,類型參數(shù)為您提供了一種通過(guò)不同輸入重復(fù)使用相同代碼的方法。區(qū)別在于形式參數(shù)的輸入是值,而類型參數(shù)的輸入是類型。
例如在類定義中使用泛型,最常見的 ArrayList
public class ArrayList<E> extends AbstractList<E>
implements List<E>, RandomAccess, Cloneable, java.io.Serializable
{
//... code
}
例如在方法參數(shù)中使用泛型,來(lái)一個(gè)復(fù)雜的例子
public static <T extends Number & Comparable<T>, U extends List<T>, R extends T> R complexMethod(U list, T element) {
}
在這個(gè)例子中,有兩個(gè)傳入?yún)?shù) U list, T element,而這兩個(gè)參數(shù)需要在方法的返回類型前用<>做出說(shuō)明,也就是 <T extends Number & Comparable<T>, U extends List<T>, R extends T>這一部分。
返回值也是一個(gè)泛型 R。
為什么是 T、U、R
經(jīng)??吹椒盒皖愋陀?T、U、R,還有K、V 這樣的符號(hào)表示。我們肯定知道不用T也完全沒(méi)問(wèn)題,用 X 也可以。
之所以這么統(tǒng)一是因?yàn)檫@是官方比較推薦的寫法,推薦的規(guī)則如下:
- E - 表示一個(gè)元素,例如集合元素、數(shù)組元素
- K - 表示一個(gè) Key,鍵值對(duì)經(jīng)常用到,與之對(duì)應(yīng)的是 V
- V - 表示一個(gè) Value,鍵值對(duì)經(jīng)常用
- N - 表示 Number(數(shù)字類型)
- T - 這個(gè)見得最多,表示一個(gè)類型 Type,不管是基礎(chǔ)類型還是自定義的類
泛型的作用
前面也說(shuō)了,當(dāng)一個(gè)參數(shù)預(yù)期可能有多種類型的時(shí)候,就會(huì)用到泛型,那既然是類型不確定,那直接用 Object 不就行了嗎,何必費(fèi)事兒呢?一會(huì)兒講到類型擦除的時(shí)候會(huì)發(fā)現(xiàn),本身類型擦除的核心就是把泛型類型轉(zhuǎn)為 Object。但是這是編譯器干的,為了給JVM看的。而作為開發(fā)者和編譯器,使用泛型還是有很大好處的。
1、在編譯時(shí)提供更嚴(yán)格的類型檢查,如果代碼違反類型安全,編譯器可以及時(shí)發(fā)現(xiàn),而不是等到運(yùn)行的時(shí)候拋出運(yùn)行時(shí)異常。
2、使程序員能夠?qū)崿F(xiàn)通用算法。通過(guò)使用泛型,程序員可以實(shí)現(xiàn)適用于不同類型集合的泛型算法,可以自定義,并且類型安全且更易于閱讀。
例如下面這個(gè)方法,只接受Number 類型的參數(shù),用來(lái)比較兩數(shù)的大小。
public static <T extends Number> Boolean compare(T first, T second) {
double firstValue = first.doubleValue();
double secondValue = second.doubleValue();
return firstValue > secondValue;
}
3、消除不必要的類型轉(zhuǎn)換。
例如下面不用泛型的情況,每次取數(shù)據(jù)的時(shí)候都要轉(zhuǎn)換一下類型。
List list = new ArrayList();
list.add("hello");
String s = (String) list.get(0);
而用了泛型后,就不用自己轉(zhuǎn)換了。
List<String> list = new ArrayList<String>();
list.add("hello");
String s = list.get(0);
類型擦除
Java 中的泛型實(shí)現(xiàn)可以說(shuō)就是用的類型擦除原理。通俗一點(diǎn)說(shuō),類型只在編譯期存在,在運(yùn)行時(shí)就不在了,都變?yōu)榱?Object,一視同仁。
在我們寫好代碼進(jìn)行編譯時(shí),編譯器會(huì)將泛型參數(shù)的類型進(jìn)行替換,大部分情況下會(huì)將類型替換為 0bject 類型。這種行為模式用類型擦除來(lái)描述就非常形象。
類型擦除原理
在類型擦除過(guò)程中,Java 編譯器會(huì)擦除所有類型參數(shù),如果類型參數(shù)有界,則用其第一個(gè)邊界替換每個(gè)參數(shù);如果類型參數(shù)無(wú)界,則用 Object 替換。
在類型擦除過(guò)程中,編譯器會(huì)按照以下規(guī)則來(lái)處理泛型類型參數(shù):
如果類型參數(shù)有界(bounded type),即使用了extends關(guān)鍵字限定了類型的上界,例如<T extends Number>,則編譯器會(huì)用該類型的第一個(gè)邊界來(lái)替換類型參數(shù)。
例如下面這個(gè)例子,泛型 T 繼承了Number類型,又實(shí)現(xiàn)了 Displayable 接口(沒(méi)錯(cuò),泛型可以這樣定義)
interface Displayable {
void display();
}
public class Result<T extends Number & Displayable> {
private T value;
public Result(T value) {
this.value = value;
}
public T getValue() {
return value;
}
public void show() {
value.display();
}
}
在編譯器進(jìn)行類型擦除后會(huì)變成下面這樣,因?yàn)?T 的上限是 Number,所以直接將 T 替換為 Number。
public class Result {
private Number value;
public Result(Number value) {
this.value = value;
}
public Number getValue() {
return value;
}
}
如果類型參數(shù)無(wú)界(unbounded type),即沒(méi)有限定類型的上界,例如<T>,則編譯器會(huì)用Object類型來(lái)替換類型參數(shù)。
例如下面方法,沒(méi)有指定類型上限類型。
public static <T> int count(T[] anArray, T elem) {
int cnt = 0;
for (T e : anArray)
if (e.equals(elem))
++cnt;
return cnt;
}
經(jīng)過(guò)編譯器的擦除處理后,就變成下面這樣,都替換成了 Object。
public static int count(Object[] anArray, Object elem) {
int cnt = 0;
for (Object e : anArray)
if (e.equals(elem))
++cnt;
return cnt;
}
橋接方法
來(lái)看一下下面這段代碼
public class Node<T> {
public T data;
public Node(T data) { this.data = data; }
public void setData(T data) {
this.data = data;
}
}
public class SubNode extends Node<Integer> {
public SubNode(Integer data) { super(data); }
public void setData(Integer data) {
super.setData(data);
}
public static void main(String[] args) {
SubNode subNode = new SubNode(8);
Node node = subNode;
node.setData("Hello");
Integer x = subNode.data;
}
}
這段代碼大家一看就知道肯定是有問(wèn)題的,運(yùn)行的時(shí)候會(huì)出現(xiàn) ClassCastException,但是編譯是可以通過(guò)的。
而運(yùn)行時(shí)出現(xiàn)錯(cuò)誤的代碼是 node.setData("Hello");這一行,但是經(jīng)過(guò)前面對(duì)類型擦除的了解,Node 類的 setData 參數(shù)肯定被擦除成了 Object 類型了,既然是 Object,那Integer 和 String 都滿足啊,為啥還會(huì)報(bào)錯(cuò)呢。
這就要說(shuō)到橋接了。
當(dāng)編譯器對(duì)泛型擴(kuò)展的類或接口進(jìn)行編譯處理的時(shí)候,會(huì)根據(jù)實(shí)際的類型進(jìn)行方法的橋接處理。什么意思呢,還是拿上面的 Node 和 SubNode 類說(shuō)明。
類型擦除后的代碼是下面這樣的,多了一個(gè)橋接方法。
public class Node {
public Object data;
public Node(Object data) { this.data = data; }
public void setData(Object data) {
this.data = data;
}
}
public class SubNode extends Node {
public SubNode(Integer data) { super(data); }
/**
** 橋接方法
**/
public void setData(Object data) {
setData((Integer) data);
}
public void setData(Integer data) {
super.setData(data);
}
}
為什么需要這個(gè)橋接方法呢?
Node 類的 setData 方法入?yún)⑹?Object 類型。
public void setData(Object data) {
this.data = data;
}
而 SubNode 的setData 方法入?yún)⑹?Integer。
public void setData(Integer data) {
super.setData(data);
}
所以,SubNode 的 setData 方法并不會(huì)重寫父類 Node 的setData 方法,而想要重寫的話,就必須讓 SubNode 的setData 的入?yún)⒁彩?Object,這就是橋接方法的由來(lái)。
public void setData(Object data) {
setData((Integer) data);
}
這樣一來(lái)重寫父類的方法,但是要把參數(shù)強(qiáng)轉(zhuǎn)成 Integer。
前面說(shuō)的 node.setData("Hello");這一行會(huì)報(bào)錯(cuò),那大家就知道為什么了吧,是因?yàn)榘?nbsp;Hello強(qiáng)轉(zhuǎn)為 Integer 的時(shí)候出現(xiàn)的錯(cuò)誤。
總結(jié)
正是類型擦除的機(jī)制幫助 Java 實(shí)現(xiàn)了泛型編程,讓我們作為開發(fā)者能夠更好的了解和控制我們正在使用類型的是什么,而不是 Object 滿天飛。