深刻理解Java泛型(三)-泛型擦除及其相關內容

點擊上方「藍字」關注咱們吧!


咱們下面看一個例子:
java

Class<?> class1=new ArrayList<String>().getClass();
Class<?> class2=new ArrayList<Integer>().getClass();
System.out.println(class1); //class java.util.ArrayList
System.out.println(class2); //class java.util.ArrayList
System.out.println(class1.equals(class2)); //true

咱們看輸出發現,class1和class2竟然是同一個類型ArrayList,在運行時咱們傳入的類型變量String和Integer都被丟掉了。Java語言泛型在設計的時候爲了兼容原來的舊代碼,Java的泛型機制使用了「擦除」機制。咱們來看一個更完全的例子:web

class Table {}
class Room {}
class House<Q> {}
class Particle<POSITION, MOMENTUM> {}
//調用代碼及輸出
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()));
/**
[E]
[K, V]
[Q]
[POSITION, MOMENTUM]
*/

上面的代碼裏,咱們想在運行時獲取類的類型參數,可是咱們看到返回的都是「形參」。在運行期咱們是獲取不到任何已經聲明的類型信息的。數組

注意:編譯器雖然會在編譯過程當中移除參數的類型信息,可是會保證類或方法內部參數類型的一致性。微信

泛型參數將會被擦除到它的第一個邊界(邊界能夠有多個,重用 extends 關鍵字,經過它能給與參數類型添加一個邊界)。編譯器事實上會把類型參數替換爲它的第一個邊界的類型。若是沒有指明邊界,那麼類型參數將被擦除到Object。下面的例子中,能夠把泛型參數T看成HasF類型來使用。編輯器

public interface HasF {
void f();
}

public class Manipulator<T extends HasF> {
T obj;
public T getObj() {
return obj;
}
public void setObj(T obj) {
this.obj = obj;
}
}

extend關鍵字後後面的類型信息決定了泛型參數能保留的信息。Java類型擦除只會擦除到HasF類型。ide

Java泛型擦除的原理

咱們經過例子來看一下,先看一個非泛型的版本:函數

// 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
}

下面咱們給出一個泛型的版本,從字節碼的角度來看看:flex

//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行有一處類型轉換的邏輯。它是編譯器自動幫咱們加進去的。 因此在泛型類對象讀取和寫入的位置爲咱們作了處理,爲代碼添加約束。this

Java泛型擦除的缺陷及補救措施

泛型類型不能顯式地運用在運行時類型的操做當中,例如:轉型、instanceof 和 new。由於在運行時,全部參數的類型信息都丟失了。相似下面的代碼都是沒法經過編譯的:spa

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];
}
}

那咱們有什麼辦法來補救呢?下面介紹幾種方法來一一解決上面出現的問題。

類型判斷問題

咱們能夠經過下面的代碼來解決泛型的類型信息因爲擦除沒法進行類型判斷的問題:

/**
* 泛型類型判斷封裝類
* @param <T>
*/

class GenericType<T>{
Class<?> classType;

public GenericType(Class<?> type) {
classType=type;
}

public boolean isInstance(Object object) {
return classType.isInstance(object);
}
}

在main方法咱們能夠這樣調用:

GenericType<A> genericType=new GenericType<>(A.class);
System.out.println("------------");
System.out.println(genericType.isInstance(new A()));
System.out.println(genericType.isInstance(new B()));

咱們經過記錄類型參數的Class對象,而後經過這個Class對象進行類型判斷。

建立類型實例

泛型代碼中不能new T()的緣由有兩個,一是由於擦除,不能肯定類型;而是沒法肯定T是否包含無參構造函數。

爲了不這兩個問題,咱們使用顯式的工廠模式:

/**
* 使用工廠方法來建立實例
*
* @param <T>
*/

interface Factory<T>{
T create();
}

class Creater<T>{
T instance;
public <F extends Factory<T>> T newInstance(F f) {
instance=f.create();
return instance;
}
}

class IntegerFactory implements Factory<Integer>{
@Override
public Integer create() {
Integer integer=new Integer(9);
return integer;
}
}

咱們經過工廠模式+泛型方法來建立實例對象,上面代碼中咱們建立了一個IntegerFactory工廠,用來建立Integer實例,之後代碼有變更的話,咱們能夠添加新的工廠類型便可。

調用代碼以下:

Creater<Integer> creater=new Creater<>();
System.out.println(creater.newInstance(new IntegerFactory()));

建立泛型數組

通常不建議建立泛型數組。儘可能使用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) {

}
}

這裏咱們使用的仍是傳參數類型,利用類型的newInstance方法建立實例的方式。


本文分享自微信公衆號 - 喘口仙氣(gh_db8538619cdd)。
若有侵權,請聯繫 support@oschina.cn 刪除。
本文參與「OSC源創計劃」,歡迎正在閱讀的你也加入,一塊兒分享。

相關文章
相關標籤/搜索