Java防止范型擦除的方法

防止范型擦除的方法

前言

java的范型我最喜歡的東西, 他可以把代碼變得更精簡, 但是也可能會是代碼中的一個小陷阱.

如果讓我去比喻一下范型在java是一個什么角色.

如果java項目工程是一個小區(qū)(爪哇小區(qū)), 那各種class 可能就是小區(qū)的住戶了. 范型就是這個小區(qū)的門衛(wèi)大爺. 他可以攔住那些看起來就是不法分子的入侵者. 但是還是有辦法偷偷進(jìn)入小區(qū), 或者像外賣小哥進(jìn)入小區(qū).

作為小區(qū)的“物業(yè)”的我, 應(yīng)該對范型的行為有所掌握, 至少出了問題, 我有方向可尋.

范型舉例

// 這可能是我最常用的范型應(yīng)用場景了
List<String> list = new HashList<>(15);

范型擦除場景

故名思義, 范型被干掉了.
范型擦出發(fā)生在什么階段呢??

背景設(shè)置

這里有兩個class

class A {
    @Override
    public String toString() {
        return "A";
    }
}

class B {
    @Override
    public String toString() {
        return "B";
    }
}
class C {
    @Override
    public String toString() {
        return "C";
    }
}
class D {
    @Override
    public String toString() {
        return "D";
    }
}

以及2個范型使用的class

public class Test<T> {
    T obj;
    Test(T obj) {
        this.obj = obj;
    }
    Test() {}
    public void setObj(Object obj) {
        // 注意該方法的區(qū)別
        this.obj = (T)obj;
    }
    @Override
    public String toString() {
        return obj.toString();
    }
}
public class Test2<T> {
    T obj;
    Test(T obj) {
        this.obj = obj;
    }
    Test2() {}
    
    public void setObj(T obj) {
        // 注意該方法的區(qū)別
        this.obj = obj;
    }
    @Override
    public String toString() {
        return obj.toString();
    }
}

以及一個測試輸出方法

public static void show(String mark, Object o) {
    System.out.println("[ " + mark + " ] " + o);
}

場景1-被忽略的范型

注意方法

B b = new B();
A a = new A();

Test<A> test1 = new Test<>();
test1.setObj(b);
show("1", test1);
Test<A> test2 = new Test<>(a);
test2.setObj(b);
show("2", test2);

結(jié)果

[ 1 ] B
[ 2 ] B

編譯通過了也沒有出現(xiàn)了報錯, Test<A> 范型我也寫了, 但是B還是被set進(jìn)去.
與預(yù)想中 可能會報錯的地方(T)obj;
可以看出 這個過程中并未發(fā)生 B->A 的強(qiáng)制轉(zhuǎn)換.
(原因: 發(fā)現(xiàn)在編譯后的文件被擦除, 該處(T)為 (Object), 驗證在后面)

場景2-常規(guī)操作

B b = new B();
A a = new A();

Test2<A> test3 = new Test2<>();
/// 編譯不通過
// test3.setObj(b);

Test2<A> test4 = new Test2<>(a);
/// 編譯不通過
// test4.setObj(b);

以上兩種情況均出現(xiàn)了編譯不通過的情況. 這就是我前言所說看起來就是不法分子的入侵者
那么有辦法, 越過這道防線嘛??
答案是可以的(場景3).

場景3-反射操作

B b = new B();
A a = new A();
Test2<A> test6 = new Test2<>(a);

/// 這一步會發(fā)現(xiàn) 這樣是找不到方法的
/// Exception in thread "main" java.lang.NoSuchMethodException: cn.net.bale.demo.reflect.Test2.setObj(cn.net.bale.demo.reflect.ModifiedClass$A)
// Test2.class.getMethod("setObj", A.class);

Method method = Test2.class.getMethod("setObj", Object.class);
method.setAccessible(true);
method.invoke(test6, b);
show("6", test6);

輸出結(jié)果

[ 6 ] B

這種方式就非常暴力的用反射把調(diào)用 setObj 方法把 b放了進(jìn)去.
但是我們可以看到, 反射的是getMethod并不是通過 getMethod("setObj", A.class)
(進(jìn)一步佐證, 范型在編譯過后被擦除, 驗證在后面)

這種方式太暴力, 一般的場景用不到, 那么什么方式能“委婉”一些呢?
見 場景4

場景4-多次范型擦除

引入工具類

public class ObjectUtil {
    private ObjectUtil() {}
    /**
     * 類型轉(zhuǎn)換
     * WARNING: 確保強(qiáng)制轉(zhuǎn)換正確無誤方可使用
     *
     * @param object
     * @param <T>
     * @return (T) object
     */
    @SuppressWarnings("unchecked")
    public static <T> T cast(Object object) {
        return (T) object;
    }
}

使用場景

B b = new B();
A a = new A();
Test2<A> test5 = new Test2<>(a);
test5.setObj(ObjectUtil.cast(b));
show("5", test5);

輸出結(jié)果

[ 5 ] B

我們可以看到, 沒有編譯出錯, 也沒有使用反射. 但是就這樣, 一路通行的進(jìn)入了“爪哇小區(qū)”??
這個方法在編譯的過程中發(fā)生了 2次范型擦除.

  1. Test2方法 中的T都被擦除變?yōu)?Object
  2. cast()方法中發(fā)生了一次范型擦除 變?yōu)?return Object.

b 首先進(jìn)入 cast方法成為Object 偽裝自己. 然后兩次范型擦除 cast()與setObj()打通, 讓b成功被set.

這個過程中, 沒有發(fā)生我們想象中的, B -> A 發(fā)生強(qiáng)制轉(zhuǎn)換報出運(yùn)行時異常.
(如果沒有發(fā)生范型擦除, 那么我們設(shè)想的“強(qiáng)轉(zhuǎn)報錯”的情況就會發(fā)生, 但是事實與設(shè)想截然相反)

場景5-創(chuàng)建實例

該場景下, 來驗證一下, 范型信息會不會存儲在object中.

引入輸出方法

/**
     * 輸出 obj.class
     *
     * @param mark 標(biāo)記區(qū)分
     * @param test
     */
    public static void showObjClass(String mark, Test test) {
        try {
            System.out.println("[ " + mark + " ]Test.class: " + test.getClass());
            System.out.println("[ " + mark + " ]toString: " + test);
            System.out.println("[ " + mark + " ]Test.obj.class: " + test.obj.getClass());
        } catch (Exception e) {
            System.out.println("[ " + mark + " ]Test.obj.class: " + e.getMessage());
        } finally {
            System.out.println();
        }
    }

場景應(yīng)用

Test test1 = new Test<>("bale");
Test test2 = new Test<>(1);
Test test3 = new Test();
// 強(qiáng)制轉(zhuǎn)換一下
Test test4 = new Test((String) null);
Test.showObjClass("1", test1);
Test.showObjClass("2", test2);
Test.showObjClass("3", test3);
System.out.println("Test1.class == Test2.class: " + test1.getClass().equals(test2.getClass()));

輸出結(jié)果

[ 1 ]Test.class: class cn.net.bale.demo.reflect.Test
[ 1 ]toString: bale
[ 1 ]Test.obj.class: class java.lang.String

[ 2 ]Test.class: class cn.net.bale.demo.reflect.Test
[ 2 ]toString: 1
[ 2 ]Test.obj.class: class java.lang.Integer

[ 3 ]Test.class: class cn.net.bale.demo.reflect.Test
[ 3 ]Test.obj.class: null

Test1.class == Test2.class: true

從最后的結(jié)果看到, Test1.class == Test2.class: true.
Test.class 無論范型是否相同, 但是class都是相同的.

范型擦除驗證

通過以上場景, 可以看出, 范型在編譯之后的class似乎就不起作用了.
通過反編譯來看一下class是否和我們的場景保持一致, 還是在什么地方發(fā)生了奇妙的反應(yīng), 讓范型被擦除.

引入命令

// javap 反編譯輸出class
javap -v ClassName.class

class 結(jié)構(gòu)參考如下

// todo jvm文檔在補(bǔ)ing

驗證1-范型類

Test.class 反編譯之后篩選出有用的信息

#9 = Utf8               TT;
#19 = Utf8               (TT;)V
// ... 基本參數(shù)與常量池省略 ...

{
  T obj;
    descriptor: Ljava/lang/Object;
    flags: (0x0000)
    Signature: #9                           // TT;

  cn.net.bale.demo.reflect.Test2(T);
    descriptor: (Ljava/lang/Object;)V
    flags: (0x0000)
    Code:
      stack=2, locals=2, args_size=2
         0: aload_0
         1: invokespecial #1                  // Method java/lang/Object."<init>":()V
         4: aload_0
         5: aload_1
         6: putfield      #2                  // Field obj:Ljava/lang/Object;
         9: return
      // 省略臨時變量區(qū)
            0      10     1   obj   TT;
    Signature: #19                          // (TT;)V

  // 省略 無參數(shù)構(gòu)造器

  public void setObj(T);
    descriptor: (Ljava/lang/Object;)V
    flags: (0x0001) ACC_PUBLIC
    Code:
      stack=2, locals=2, args_size=2
         0: aload_0
         1: aload_1
         2: putfield      #2                  // Field obj:Ljava/lang/Object;
         5: return
      LineNumberTable:
        line 19: 0
        line 20: 5
      LocalVariableTable:
        Start  Length  Slot  Name   Signature
            0       6     0  this   Lcn/net/bale/demo/reflect/Test2;
            0       6     1   obj   Ljava/lang/Object;
      LocalVariableTypeTable:
        Start  Length  Slot  Name   Signature
            0       6     0  this   Lcn/net/bale/demo/reflect/Test2<TT;>;
            0       6     1   obj   TT;
    Signature: #19                          // (TT;)V

  // 省略 toString方法
}
Signature: #24                          // <T:Ljava/lang/Object;>Ljava/lang/Object;
SourceFile: "Test2.java"

我們通過反編譯結(jié)果可以看到 所有范型出現(xiàn)的地方 都是 Ljava/lang/Object;

T obj;
    descriptor: Ljava/lang/Object; // Object屬性
    flags: (0x0000)
    Signature: #9 // TT;
    
cn.net.bale.demo.reflect.Test2(T);  
    descriptor: (Ljava/lang/Object;)V // Object參數(shù)無返回值的構(gòu)造器方法
    Signature: #19 // (TT;)V
    
public void setObj(T);  
    descriptor: (Ljava/lang/Object;)V // Object參數(shù)無返回值的setObject方法
    Signature: #19 // (TT;)V

可以看出, 編譯后的結(jié)果, 所有范型的位置都被Object擦除

驗證2-調(diào)用范型

引入class用于反編譯觀察Test<T>的調(diào)用情況

public class NewClass {
    public static void main(String[] args) {
        Test test1 = new Test<>("bale");
        Test test2 = new Test<>(1);
        Test test3 = new Test();
    }
}

反編并省略無關(guān)緊要的信息

// 省略基礎(chǔ)信息以及常量池
{
  // 省略構(gòu)造器
  public static void main(java.lang.String[]);
    descriptor: ([Ljava/lang/String;)V
    flags: (0x0009) ACC_PUBLIC, ACC_STATIC
    Code:
      stack=3, locals=4, args_size=1
         0: new           #2                  // class cn/net/bale/demo/reflect/Test
         3: dup
         4: ldc           #3                  // String bale
         6: invokespecial #4                  // Method cn/net/bale/demo/reflect/Test."<init>":(Ljava/lang/Object;)V
         9: astore_1
        10: new           #2                  // class cn/net/bale/demo/reflect/Test
        13: dup
        14: iconst_1
        15: invokestatic  #5                  // Method java/lang/Integer.valueOf:(I)Ljava/lang/Integer;
        18: invokespecial #4                  // Method cn/net/bale/demo/reflect/Test."<init>":(Ljava/lang/Object;)V
        21: astore_2
        22: new           #2                  // class cn/net/bale/demo/reflect/Test
        25: dup
        26: invokespecial #6                  // Method cn/net/bale/demo/reflect/Test."<init>":()V
        29: astore_3
        30: return
      // ...省略...
}
SourceFile: "NewClass.java"

重點(diǎn)關(guān)注內(nèi)容如下


// 調(diào)用1, 直接調(diào)用 Test構(gòu)造器, 參數(shù)為Object
6: invokespecial #4                  // Method cn/net/bale/demo/reflect/Test."<init>":(Ljava/lang/Object;)V

// 調(diào)用 valueOf 將數(shù)字包裝成包裝類
15: invokestatic  #5                  // Method java/lang/Integer.valueOf:(I)Ljava/lang/Integer;
// 調(diào)用2, 直接調(diào)用 Test構(gòu)造器, 參數(shù)為Object
18: invokespecial #4                  // Method cn/net/bale/demo/reflect/Test."<init>":(Ljava/lang/Object;)V

可以看到調(diào)用方法使用范型的部分也都被擦除為Object
(感興趣的同學(xué)可以觀察 場景4cast()方法的范型擦除情況, 不過也都大同小異)

范型防止擦除的應(yīng)用

通過以上的場景觀察, 以及反編譯驗證可以了解到范型在編譯之后會被擦除, 那如和防止這種情況的發(fā)生呢?
換一種說法, 范型我們?nèi)绾稳ナ褂盟? 就目前來看, 范型僅僅做到類型限制的作用, 如何讓范型變得很靈活呢??
(要不我憑什么喜歡范型)
如果只做到這一步, 豈不是直接用Object也沒什么問題, 只要開發(fā)的時候注釋寫好一些不就可以了??
不是這樣的的, 見 應(yīng)用1

應(yīng)用1-extends指定擦除類型

使用 extend 關(guān)鍵字來指定擦除類

在不使用extends的時候, 默認(rèn)使用 Object 作為擦除類(畢竟 超類是萬能的)

引入class

class C {
}
public class Test3<T extends C> {
    T obj;
}

反編譯結(jié)果

// 省略基礎(chǔ)信息和常量池
{
  T obj;
    descriptor: Lcn/net/bale/demo/reflect/C;
    flags: (0x0000)
    Signature: #7                           // TT;

  // 省略構(gòu)造器
}
Signature: #17                          // <T:Lcn/net/bale/demo/reflect/C;>Ljava/lang/Object;
SourceFile: "Test3.java"

結(jié)果很明顯, extends 關(guān)鍵字之后的范型擦除的位置不在是 Ljava/lang/Object; 而變?yōu)橹付ǖ?C

應(yīng)用2-extend保存范型

!!! 該處extends 不同于 應(yīng)用1
此處為class的 extend繼承

此處應(yīng)用場景最為廣泛且實用

應(yīng)用如下

public class ExtendTest extends Test<C>{
}

此處通過 extends Test<C> 將范型的信息以 標(biāo)簽的形式存儲在 ExtendTest中

那么如何使用它?

在普通的情況下 Who extends Test<C> 那么在Who 中一定就知道C是誰. 如何靈活使用這個關(guān)系呢?

引入幫助接口

package cn.net.bale.demo.reflect;

import cn.net.bale.demo.util.ObjectUtil;

import java.lang.reflect.ParameterizedType;

/**
 * TClass, 用于輔助范型獲取 class
 *
 * @author bale 2019-06-11
 */
public interface Clazz<T> {
    /**
     * 獲取 ParameterizedType 輔助獲取 T class
     *
     * @return ParameterizedType
     */
    default ParameterizedType getParameterizedType() {
        return ObjectUtil.cast(getClass().getGenericSuperclass());
    }
    /**
     * 獲取 T class
     *
     * @return Class<T>
     */
    default Class<T> getTClass() {
        // [0]為<T> 復(fù)雜情況, 按需求改寫
       return ObjectUtil.cast(getParameterizedType().getActualTypeArguments()[0]);
    }
}

假設(shè)一個場景, 對mongoTemplate 二次封裝, 每一個mongoCollection都對應(yīng)一個 class entity
這樣我們可以通過 class entity的屬性名,生成通用的查詢方法

代碼如下
引入輔助注解

/**
 * Mongo json集(表)
 *
 * @author wentao.liu01@hand-china.com 2019-05-22
 */
@Target({ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
public @interface MongoCollection {
    String name() default "";
}
public abstract class BaseMongoRepositoryImpl<T> implements Clazz<T> {
    // 這里可以根據(jù)情況改寫 spring 中可以 @Autowired
    private final MongoTemplate mongoTemplate;
    /**
     * 獲取 mongo 集合 name
     *
     * @return String
     */
    protected String getCollectionName() {
        Class<T> tClass = getTClass();
        MongoCollection mongoCollection = tClass.getAnnotation(MongoCollection.class);
        if (Objects.nonNull(mongoCollection)) {
            return mongoCollection.name();
        }
        return null;
    }
     /**
     * mongo find 
     * (封裝一個最簡單的find方法, 更多的黑科技可以在這里使用)
     * @param query
     * @return
     */
    public List<T> find(final Query query) {
        List<T> list = this.mongoTemplate.find(query, getTClass(), getCollectionName());
        setMulFieldFunction(list);
        return list;
    }
}
對應(yīng)接口 
public interface BaseMongoRepositoryI<T> {
    List<T> find(final Query query);
}

使用情況:

entity:

@MongoCollection(name = "集合名稱")
public class ClassName {
    // ...
}

impl:

public class Impl extends BaseMongoRepositoryImpl<ClassName> implements BaseMongoRepository {
}

使用:

Impl impl = 想辦法獲取到 Impl
Query query= new Query();
impl.find(Query)

這里已經(jīng)實現(xiàn)最簡單的版本, 讓mongoTempalte 自帶collectionName, 而不用每次查詢都手動指定collectionName

還有更多的功能, 比如多語言, 根據(jù)屬性自動生成查詢方法等通用的功能, 來方便接下來的開發(fā).

總結(jié)

范型是一個好東西, 了解范型的使用與擦除, 那么在編寫通用的代碼的時候是非常有意義的.
并且可以極大的縮短代碼的行數(shù), 讓代碼更有效.

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

友情鏈接更多精彩內(nèi)容