擦除
在泛型代碼內(nèi)部颂跨,無法獲得任何有關(guān)泛型參數(shù)類型的信息笆豁。
例子1:
//這個例子表明編譯過程中并沒有根據(jù)參數(shù)生成新的類型
public class Main2 {
public static void main(String[] args) {
Class c1 = new ArrayList<Integer>().getClass();
Class c2 = new ArrayList<String>().getClass();
System.out.print(c1 == c2);
}
}
/* output
true
*/
在 List<String>
中添加 Integer
將不會通過編譯苗傅,但是List<Sring>
與List<Integer>
在運(yùn)行時的確是同一種類型壶栋。
例子2:
//例子, 這個例子表明類的參數(shù)類型跟傳進(jìn)去的類型沒有關(guān)系埠啃,泛型參數(shù)只是`占位符`
public class Table {
}
public class Room {
}
public class House<Q> {
}
public class Particle<POSITION, MOMENTUM> {
}
public class Main {
public static void main(String[] args) {
List<Table> tableList = new ArrayList<Table>();
Map<Room, Table> maps = new HashMap<Room, Table>();
House<Room> house = new House<Room>();
Particle<Long, Double> particle = new Particle<Long, Double>();
System.out.println(Arrays.toString(tableList.getClass().getTypeParameters()));
System.out.println(Arrays.toString(maps.getClass().getTypeParameters()));
System.out.println(Arrays.toString(house.getClass().getTypeParameters()));
System.out.println(Arrays.toString(particle.getClass().getTypeParameters()));
}
}
/** output
[E]
[K, V]
[Q]
[POSITION, MOMENTUM]
*/
我們在運(yùn)行期試圖獲取一個已經(jīng)聲明的類的類型參數(shù)鹏控,發(fā)現(xiàn)這些參數(shù)依舊是‘形參’庶近,并沒有隨聲明改變富蓄。也就是說在運(yùn)行期胖缤,我們是拿不到已經(jīng)聲明的類型的任何信息尚镰。
編譯器會雖然在編譯過程中移除參數(shù)的類型信息,但是會保證類或方法內(nèi)部參數(shù)類型的一致性哪廓。
例子:
List<String> stringList=new ArrayList<String>();
//可以通過編譯
stringList.add("wakaka");
//編譯不通過
//stringList.add(new Integer(0));
//List.java
public interface List<E> extends Collection<E> {
//...
boolean add(E e);
//...
}
List
的參數(shù)類型是E
狗唉,add
方法的參數(shù)類型也是E
,他們在類的內(nèi)部是一致的涡真,所以添加Integer
類型的對象到stringList
違反了內(nèi)部類型一致分俯,不能通過編譯。
重用 extends
關(guān)鍵字哆料。通過它能給與參數(shù)類型添加一個邊界缸剪。
泛型參數(shù)將會被擦除到它的第一個邊界(邊界可以有多個)。編譯器事實上會把類型參數(shù)替換為它的第一個邊界的類型东亦。如果沒有指明邊界杏节,那么類型參數(shù)將被擦除到Object
。下面的例子中,可以把泛型參數(shù)T當(dāng)作HasF類型來使用奋渔。
例子:
/** * Created by yxf on 16-5-28. */
// HasF.java
public interface HasF {
void f();
}
//Manipulator.java
public class Manipulator<T extends HasF> {
T obj;
public T getObj() {
return obj;
}
public void setObj(T obj) {
this.obj = obj;
}
}
extend
關(guān)鍵字后后面的類型信息決定了泛型參數(shù)能保留的信息镊逝。
Java中擦除的基本原理
剛看到這里可能有些困惑,一個泛型類型沒有保留具體聲明的類型的信息卒稳,那它是怎么工作的呢蹋半?在把《Java編程思想》書中這里的邊界與上文的邊界區(qū)分開來之后,終于想通了充坑。Java的泛型類的確只有一份字節(jié)碼减江,但是在使用泛型類的時候編譯器做了特殊的處理。
這里根據(jù)作者的思路捻爷,自己動手寫了兩個類SimpleHolder
和GenericHolder
辈灼,然后編譯拿到兩個類的字節(jié)碼,直接貼在這里:
// SimpleHolder.java
public class SimpleHolder {
private Object obj;
public Object getObj() {
return obj;
}
public void setObj(Object obj) {
this.obj = obj;
}
public static void main(String[] args) {
SimpleHolder holder = new SimpleHolder();
holder.setObj("Item");
String s = (String) holder.getObj();
}
}
// SimpleHolder.class
public class SimpleHolder {
public SimpleHolder();
Code:
0: aload_0
1: invokespecial #1 // Method java/lang/Object."<init>":()V
4: return
public java.lang.Object getObj();
Code:
0: aload_0
1: getfield #2 // Field obj:Ljava/lang/Object;
4: areturn
public void setObj(java.lang.Object);
Code:
0: aload_0
1: aload_1
2: putfield #2 // Field obj:Ljava/lang/Object;
5: return
public static void main(java.lang.String[]);
Code:
0: new #3 // class SimpleHolder
3: dup
4: invokespecial #4 // Method "<init>":()V
7: astore_1
8: aload_1
9: ldc #5 // String Item
11: invokevirtual #6 // Method setObj:(Ljava/lang/Object;)V
14: aload_1
15: invokevirtual #7 // Method getObj:()Ljava/lang/Object;
18: checkcast #8 // class java/lang/String
21: astore_2
22: return
}
//GenericHolder.java
public class GenericHolder<T> {
T obj;
public T getObj() {
return obj;
}
public void setObj(T obj) {
this.obj = obj;
}
public static void main(String[] args) {
GenericHolder<String> holder = new GenericHolder<>();
holder.setObj("Item");
String s = holder.getObj();
}
}
//GenericHolder.class
public class GenericHolder<T> {
T obj;
public GenericHolder();
Code:
0: aload_0
1: invokespecial #1 // Method java/lang/Object."<init>":()V
4: return
public T getObj();
Code:
0: aload_0
1: getfield #2 // Field obj:Ljava/lang/Object;
4: areturn
public void setObj(T);
Code:
0: aload_0
1: aload_1
2: putfield #2 // Field obj:Ljava/lang/Object;
5: return
public static void main(java.lang.String[]);
Code:
0: new #3 // class GenericHolder
3: dup
4: invokespecial #4 // Method "<init>":()V
7: astore_1
8: aload_1
9: ldc #5 // String Item
11: invokevirtual #6 // Method setObj:(Ljava/lang/Object;)V
14: aload_1
15: invokevirtual #7 // Method getObj:()Ljava/lang/Object;
18: checkcast #8 // class java/lang/String
21: astore_2
22: return
}
經(jīng)過一番比較之后也榄,發(fā)現(xiàn)兩分源碼雖然不同巡莹,但是對應(yīng)的字節(jié)碼邏輯部分確是完全相同的。
在編譯過程中甜紫,類型變量的信息是能拿到的降宅。所以,set
方法在編譯器可以做類型檢查囚霸,非法類型不能通過編譯腰根。但是對于get
方法,由于擦除機(jī)制拓型,運(yùn)行時的實際引用類型為Object
類型额嘿。為了‘還原’返回結(jié)果的類型,編譯器在get
之后添加了類型轉(zhuǎn)換劣挫。所以册养,在GenericHolder.class
文件main
方法主體第18行有一處類型轉(zhuǎn)換的邏輯。它是編譯器自動幫我們加進(jìn)去的压固。
所以在泛型類對象讀取和寫入的位置為我們做了處理球拦,為代碼添加約束。
擦除的缺陷
泛型類型不能顯式地運(yùn)用在運(yùn)行時類型的操作當(dāng)中帐我,例如:轉(zhuǎn)型坎炼、instanceof
和 new
。因為在運(yùn)行時焚刚,所有參數(shù)的類型信息都丟失了。
public class Erased<T> {
private final int SIZE = 100;
public static void f(Object arg) {
//編譯不通過
if (arg instanceof T) {
}
//編譯不通過
T var = new T();
//編譯不通過
T[] array = new T[SIZE];
//編譯不通過
T[] array = (T) new Object[SIZE];
}
}
擦除的補(bǔ)償
1. 類型判斷問題
例子:
class Building {}
class House extends Building {}
public class ClassTypeCapture<T> {
Class<T> kind;
public ClassTypeCapture(Class<T> kind) {
this.kind = kind;
}
public boolean f(Object arg) {
return kind.isInstance(arg);
}
public static void main(String[] args) {
ClassTypeCapture<Building> ctt1 = new ClassTypeCapture<Building>(Building.class);
System.out.println(ctt1.f(new Building()));
System.out.println(ctt1.f(new House()));
ClassTypeCapture<House> ctt2 = new ClassTypeCapture<House>(House.class);
System.out.println(ctt2.f(new Building()));
System.out.print(ctt2.f(new House()));
}
}
//output
//true
//true
//false
//true
泛型參數(shù)的類型無法用instanceof
關(guān)鍵字來做判斷扇调。所以我們使用類類型來構(gòu)造一個類型判斷器矿咕,判斷一個實例是否為特定的類型。
2. 創(chuàng)建類型實例
Erased.java
中不能new T()
的原因有兩個,一是因為擦除碳柱,不能確定類型捡絮;而是無法確定T
是否包含無參構(gòu)造函數(shù)。
為了避免這兩個問題莲镣,我們使用顯式的工廠模式:
例子:
interface IFactory<T> {
T create();
}
class Foo2<T> {
private T x;
public <F extends IFactory<T>> Foo2(F factory) {
x = factory.create();
}
}
class IntegerFactory implements IFactory<Integer> {
@Override
public Integer create() {
return new Integer(0);
}
}
class Widget {
public static class Factory implements IFactory<Widget> {
@Override
public Widget create() {
return new Widget();
}
}
}
public class FactoryConstraint {
public static void main(String[] args) {
new Foo2<Integer>(new IntegerFactory());
new Foo2<Widget>(new Widget.Factory());
}
}
通過特定的工廠類實現(xiàn)特定的類型能夠解決實例化類型參數(shù)的需求福稳。
3. 創(chuàng)建泛型數(shù)組
一般不建議創(chuàng)建泛型數(shù)組。盡量使用ArrayList
來代替泛型數(shù)組瑞侮。但是在這里還是給出一種創(chuàng)建泛型數(shù)組的方法的圆。
public class GenericArrayWithTypeToken<T> {
private T[] array;
@SuppressWarnings("unchecked")
public GenericArrayWithTypeToken(Class<T> type, int sz) {
array = (T[]) Array.newInstance(type, sz);
}
public void put(int index, T item) {
array[index] = item;
}
public T[] rep() {
return array;
}
public static void main(String[] args) {
GenericArrayWithTypeToken<Integer> gai = new GenericArrayWithTypeToken<Integer>(Integer.class, 10);
Integer[] ia = gai.rep();
}
}
這里我們使用的還是傳參數(shù)類型,利用類型的newInstance
方法創(chuàng)建實例的方式半火。
邊界
這里Java重用了 extend
關(guān)鍵字越妈。邊界可以將類型參數(shù)的范圍限制到一個子集當(dāng)中。
interface HasColor {
Color getColor();
}
class Colored<T extends HasColor> {
T item;
public Colored(T item) {
this.item = item;
}
public T getItem() {
return item;
}
public Color color() {
return item.getColor();
}
}
class Dimension {
public int x, y, z;
}
class ColoredDemension<T extends HasColor & Dimension> {
T item;
public ColoredDemension(T item) {
this.item = item;
}
public T getItem() {
return item;
}
Color color() {
return item.getColor();
}
int getX() {
return item.x;
}
int getY() {
return item.y;
}
int getZ() {
return item.z;
}
}
interface Weight {
int weight();
}
class Solid<T extends Dimension & HasColor & Weight> {
T item;
public Solid(T item) {
this.item = item;
}
public T getItem() {
return item;
}
Color color() {
return item.getColor();
}
int getX() {
return item.x;
}
int getY() {
return item.y;
}
int getZ() {
return item.z;
}
int weight() {
return item.weight();
}
}
class Bounded extends Dimension implements HasColor, Weight {
@Override
public Color getColor() {
return null;
}
@Override
public int weight() {
return 0;
}
}
public class BasicBound {
public static void main(String[] args) {
Solid<Bounded> solid = new Solid<Bounded>(new Bounded());
solid.color();
solid.weight();
solid.getZ();
}
}
extends關(guān)鍵字聲明中钮糖,有兩個要注意的地方:
- 類必須要寫在接口之前梅掠;
- 只能設(shè)置一個類做邊界,其它均為接口店归。
通配符
協(xié)變:
public class Holder<T> {
private T value;
public Holder(T apple) {
}
public T getValue() {
return value;
}
public void setValue(T value) {
this.value = value;
}
@Override
public boolean equals(Object o) {
return value != null && value.equals(o);
}
public static void main(String[] args) {
Holder<Apple> appleHolder = new Holder<Apple>(new Apple());
Apple d = new Apple();
appleHolder.setValue(d);
// 不能自動協(xié)變
// Holder<Fruit> fruitHolder=appleHolder;
// 借助 ? 通配符和 extends 關(guān)鍵字可以實現(xiàn)協(xié)變
Holder<? extends Fruit> fruitHolder = appleHolder;
// 返回一個Fruit消痛,因為添加邊界之后返回的對象是 ? extends Fruit,
// 可以把它轉(zhuǎn)型為Apple且叁,但是在不知道具體類型的時候存在風(fēng)險
d = (Apple) fruitHolder.getValue();
//Fruit以及Fruit的父類,就不需要轉(zhuǎn)型
Fruit fruit = fruitHolder.getValue();
Object obj = fruitHolder.getValue();
try {
Orange c = (Orange) fruitHolder.getValue();
} catch (Exception e) {
System.out.print(e);
}
// 編譯不通過稠歉,因為編譯階段根本不知道子類型到底是什么類型
// fruitHolder.setValue(new Apple());
// fruitHolder.setValue(new Orange());
//這里是可以的因為equals方法接受的是Object作為參數(shù)掰担,并不是 ? extends Fruit
System.out.print(fruitHolder.equals(d));
}
}
在Java中父類型可以持有子類型。如果一個父類的容器可以持有子類的容器怒炸,那么我們就可以稱為發(fā)生了協(xié)變带饱。在java中,數(shù)組是自帶協(xié)變的阅羹,但是泛型的容器沒有自帶協(xié)變勺疼。我們可以根據(jù)利用邊界和通配符?
來實現(xiàn)近似的協(xié)變。
Holder<? extends Fruit>
就是一種協(xié)變的寫法捏鱼。它表示一個列表执庐,列表持有的類型是Fruit
或其子類。
這個Holder<? extends Fruit>
運(yùn)行時持有的類型是未知的导梆,我們只知道它一定是Fruit
的子類轨淌。正因為如此迂烁,所以我們無法向這個holder
中放入任何類型的對象,Object
類型的對象也不可以递鹉。但是盟步,調(diào)用它的返回方法卻是可以的。因為邊界明確定義了它是Fruit
類型的子類躏结。
逆變:
package wildcard;
import java.util.ArrayList;
import java.util.List;
public class GenericWriting {
static <T> void writeExact(List<T> list, T item) {
list.add(item);
}
static List<Apple> apples = new ArrayList<Apple>();
static List<Fruit> fruits = new ArrayList<Fruit>();
static void f1() {
writeExact(apples, new Apple());
//this cannot be compile,said in Thinking in Java
writeExact(fruits, new Apple());
}
static <T> void writeWithWildcard(List<? super T> list, T item) {
list.add(item);
}
static void f2() {
writeWithWildcard(apples, new Apple());
writeWithWildcard(fruits, new Apple());
}
static <T> readWithWildcard(List<? super T> list, int index) {
//Compile Error, required T but found Object
return list.get(index);
}
public static void main(String[] args) {
f1();
f2();
}
}
如果一個類的父類型容器可以持有該類的子類型的容器却盘,我們稱這種關(guān)系為逆變。聲明方式List<? super Integer>
, List<? super T> list
媳拴。
不能給泛型參數(shù)給出一個超類型邊界黄橘;即不能聲明List<T super MyClass>
。
上面的例子中禀挫,writeExact(fruits,new Apple());
在《Java編程思想》中說是不能通過編譯的旬陡,但我試了一下,在Java1.6语婴,Java1.7中是可以編譯的描孟。不知道是不是編譯器比1.5版本升級了。
由于給出了參數(shù)類型的‘下界’砰左,所以我們可以在列表中添加數(shù)據(jù)而不會出現(xiàn)類型錯誤匿醒。但是使用get方法獲取返回類型的時候要注意,由于聲明的類型區(qū)間是Object到T具有繼承關(guān)系的類缠导。所以返回的類型為了確保沒有問題廉羔,都是以O(shè)bject類型返回回來的。比如過例子中list.get(index)
的返回類型就是Object
僻造。
無界通配符
無界通配符<?>
意味著可以使用任何對象憋他,因此使用它類似于使用原生類型。但它是有作用的髓削,原生類型可以持有任何類型竹挡,而無界通配符修飾的容器持有的是某種具體的類型。舉個例子立膛,在List<?>
類型的引用中揪罕,不能向其中添加Object
, 而List
類型的引用就可以添加Object
類型的變量。
一些需要注意的問題
1. 任何基本類型都不能作為類型參數(shù)
2. 實現(xiàn)參數(shù)化接口
例子:
interface Payable<T>{}
class Employee implements Payable<Employee> {}
//Compile Error
class Hourly extends Employee implements Payable<Hourly> {}
因為擦除的原因宝泵,Payable<Employee>
與 Payable<Hourly>
簡化為相同的Payable<Object>
好啰,例子中的代碼意味著重復(fù)兩次實現(xiàn)相同的接口。但他們的參數(shù)類型卻是不相同的儿奶。
3. 轉(zhuǎn)型和警告
使用帶有泛型類型參數(shù)的轉(zhuǎn)型或者instanceof
不會有任何效果框往。因為他們在運(yùn)行時都會被擦除到上邊界上。所以轉(zhuǎn)型的時候用的類型實際上是上邊解對應(yīng)的類型闯捎。
4. 重載
//Compile Error. 編譯不能通過
public class UseList<W,T>{
void f(List<T> v){}
void f(List<W> v){}
}
由于擦除的原因椰弊,重載方法將產(chǎn)生相同的類型簽名嘁酿。避免這種問題的方法就是換個方法名。
5. 基類劫持接口
例子:
public class ComparablePet implements Comparable<ComparablePet>{
public int compareTo(ComparablePet arg) {return 0;}
}
class Cat extends ComparablePet implements Comparable<Cat>{
// Error: Comparable connot be inherited with
// different arguments: <Cat> and <ComparablePet>
public int compareTo(Cat arg);
}
父類中我們?yōu)?code>Comparable確定了ComparablePet
參數(shù)男应,那么其它任何類型都不能再與ComparablePet
之外的對象再比較。子類中不能對同一個接口用不同的參數(shù)實現(xiàn)兩次娱仔。這有點(diǎn)類似于第四點(diǎn)中的重載沐飘。
但是我們可以在子類中覆寫父類中的方法。
關(guān)于泛型問題就先了解這么多牲迫,有什么不對的地方還請大家指正耐朴。也歡迎小伙伴們一起交流。