擦除
在泛型代碼內部,無法獲得任何有關泛型參數類型的信息。
例子1:
//這個例子表明編譯過程中並沒有根據參數生成新的類型
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>在運行時的確是同一種類型。
例子2:
//例子, 這個例子表明類的參數類型跟傳進去的類型沒有關系,泛型參數只是`占位符`
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]
*/
我們在運行期試圖獲取一個已經聲明的類的類型參數,發現這些參數依舊是‘形參’,並沒有隨聲明改變。也就是說在運行期,我們是拿不到已經聲明的類型的任何信息。
編譯器會雖然在編譯過程中移除參數的類型信息,但是會保證類或方法內部參數類型的一致性。
例子:
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的參數類型是E,add方法的參數類型也是E,他們在類的內部是一致的,所以添加Integer類型的對象到stringList違反了內部類型一致,不能通過編譯。
重用 extends 關鍵字。通過它能給與參數類型添加一個邊界。
泛型參數將會被擦除到它的第一個邊界(邊界可以有多個)。編譯器事實上會把類型參數替換為它的第一個邊界的類型。如果沒有指明邊界,那么類型參數將被擦除到Object。下面的例子中,可以把泛型參數T當作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關鍵字后后面的類型信息決定了泛型參數能保留的信息。
Java中擦除的基本原理
剛看到這里可能有些困惑,一個泛型類型沒有保留具體聲明的類型的信息,那它是怎么工作的呢?在把《Java編程思想》書中這里的邊界與上文的邊界區分開來之后,終於想通了。Java的泛型類的確只有一份字節碼,但是在使用泛型類的時候編譯器做了特殊的處理。
這里根據作者的思路,自己動手寫了兩個類SimpleHolder和GenericHolder,然后編譯拿到兩個類的字節碼,直接貼在這里:
// 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
}
經過一番比較之后,發現兩分源碼雖然不同,但是對應的字節碼邏輯部分確是完全相同的。
在編譯過程中,類型變量的信息是能拿到的。所以,set方法在編譯器可以做類型檢查,非法類型不能通過編譯。但是對於get方法,由於擦除機制,運行時的實際引用類型為Object類型。為了‘還原’返回結果的類型,編譯器在get之后添加了類型轉換。所以,在GenericHolder.class文件main方法主體第18行有一處類型轉換的邏輯。它是編譯器自動幫我們加進去的。
所以在泛型類對象讀取和寫入的位置為我們做了處理,為代碼添加約束。
擦除的缺陷
泛型類型不能顯式地運用在運行時類型的操作當中,例如:轉型、instanceof 和 new。因為在運行時,所有參數的類型信息都丟失了。
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];
}
}
擦除的補償
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
泛型參數的類型無法用instanceof關鍵字來做判斷。所以我們使用類類型來構造一個類型判斷器,判斷一個實例是否為特定的類型。
2. 創建類型實例
Erased.java中不能new T()的原因有兩個,一是因為擦除,不能確定類型;而是無法確定T是否包含無參構造函數。
為了避免這兩個問題,我們使用顯式的工廠模式:
例子:
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());
}
}
通過特定的工廠類實現特定的類型能夠解決實例化類型參數的需求。
3. 創建泛型數組
一般不建議創建泛型數組。盡量使用ArrayList來代替泛型數組。但是在這里還是給出一種創建泛型數組的方法。
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();
}
}
這里我們使用的還是傳參數類型,利用類型的newInstance方法創建實例的方式。
邊界
這里Java重用了 extend關鍵字。邊界可以將類型參數的范圍限制到一個子集當中。
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關鍵字聲明中,有兩個要注意的地方:
- 類必須要寫在接口之前;
- 只能設置一個類做邊界,其它均為接口。
通配符
協變:
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);
// 不能自動協變
// Holder<Fruit> fruitHolder=appleHolder;
// 借助 ? 通配符和 extends 關鍵字可以實現協變
Holder<? extends Fruit> fruitHolder = appleHolder;
// 返回一個Fruit,因為添加邊界之后返回的對象是 ? extends Fruit,
// 可以把它轉型為Apple,但是在不知道具體類型的時候存在風險
d = (Apple) fruitHolder.getValue();
//Fruit以及Fruit的父類,就不需要轉型
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作為參數,並不是 ? extends Fruit
System.out.print(fruitHolder.equals(d));
}
}
在Java中父類型可以持有子類型。如果一個父類的容器可以持有子類的容器,那么我們就可以稱為發生了協變。在java中,數組是自帶協變的,但是泛型的容器沒有自帶協變。我們可以根據利用邊界和通配符?來實現近似的協變。
Holder<? extends Fruit>就是一種協變的寫法。它表示一個列表,列表持有的類型是Fruit或其子類。
這個Holder<? extends Fruit>運行時持有的類型是未知的,我們只知道它一定是Fruit的子類。正因為如此,所以我們無法向這個holder中放入任何類型的對象,Object類型的對象也不可以。但是,調用它的返回方法卻是可以的。因為邊界明確定義了它是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();
}
}
如果一個類的父類型容器可以持有該類的子類型的容器,我們稱這種關系為逆變。聲明方式List<? super Integer>, List<? super T> list。
不能給泛型參數給出一個超類型邊界;即不能聲明List<T super MyClass>。
上面的例子中,writeExact(fruits,new Apple());在《Java編程思想》中說是不能通過編譯的,但我試了一下,在Java1.6,Java1.7中是可以編譯的。不知道是不是編譯器比1.5版本升級了。
由於給出了參數類型的‘下界’,所以我們可以在列表中添加數據而不會出現類型錯誤。但是使用get方法獲取返回類型的時候要注意,由於聲明的類型區間是Object到T具有繼承關系的類。所以返回的類型為了確保沒有問題,都是以Object類型返回回來的。比如過例子中list.get(index)的返回類型就是Object。
無界通配符
無界通配符<?> 意味着可以使用任何對象,因此使用它類似於使用原生類型。但它是有作用的,原生類型可以持有任何類型,而無界通配符修飾的容器持有的是某種具體的類型。舉個例子,在List<?>類型的引用中,不能向其中添加Object, 而List類型的引用就可以添加Object類型的變量。
一些需要注意的問題
1. 任何基本類型都不能作為類型參數
2. 實現參數化接口
例子:
interface Payable<T>{}
class Employee implements Payable<Employee> {}
//Compile Error
class Hourly extends Employee implements Payable<Hourly> {}
因為擦除的原因,Payable<Employee> 與 Payable<Hourly>簡化為相同的Payable<Object>,例子中的代碼意味着重復兩次實現相同的接口。但他們的參數類型卻是不相同的。
3. 轉型和警告
使用帶有泛型類型參數的轉型或者instanceof不會有任何效果。因為他們在運行時都會被擦除到上邊界上。所以轉型的時候用的類型實際上是上邊解對應的類型。
4. 重載
//Compile Error. 編譯不能通過
public class UseList<W,T>{
void f(List<T> v){}
void f(List<W> v){}
}
由於擦除的原因,重載方法將產生相同的類型簽名。避免這種問題的方法就是換個方法名。
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);
}
父類中我們為Comparable確定了ComparablePet參數,那么其它任何類型都不能再與ComparablePet之外的對象再比較。子類中不能對同一個接口用不同的參數實現兩次。這有點類似於第四點中的重載。
但是我們可以在子類中覆寫父類中的方法。
關於泛型問題就先了解這么多,有什么不對的地方還請大家指正。也歡迎小伙伴們一起交流。
