防止范型擦除的方法
前言
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次范型擦除.
- Test2方法 中的T都被擦除變?yōu)?Object
- 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ù), 讓代碼更有效.