之前寫過一篇博客介紹過Java泛型的類型擦除機制,實際上Java的泛型是使用所有類的公共父類Object去實現:
//錯誤,泛型的類型被擦除了,T在運行時實際上是Object,不能直接new出來
public <T> void foo(T arg) {
return arg != null ? arg : new T();
}
那是不是運行的時候就真的無法獲取到泛型使用的實際類型呢?
我們可以從一些第三方庫找到答案,例如Gson,它就支持解析泛型類型:
public static class Foo<T> {
T data;
}
Type type = new TypeToken<Foo>() {}.getType();
Foo foo = new Gson().fromJson(json, type);
它是怎麼做到的呢?
爲什麼拿不到泛型的具體類型
我們一步步來探索這個問題。先寫一個簡單的例子:
import java.lang.reflect.Field;
public class GenericsDemo {
public static void main(String[] args) throws NoSuchFieldException {
Foo<String> foo = new Foo<String>();
Class fooClass = foo.getClass();
Field field = fooClass.getField("data");
System.out.println(fooClass);
System.out.println(field.getType());
System.out.println(field.getGenericType());
}
public static class Foo<T> {
public T data;
}
}
執行之後看到打印裏面泛型的實際類型String已經不見了,變成了Object:
class GenericsDemo$Foo
class java.lang.Object
T
所以我們也就沒有辦法知道new Foo的時候指定的String類型。
繼承泛型類指定泛型類型
這裏還涉及到一個類加載的知識點,getClass方法拿到的實際是類加載器加載的類信息,而類信息在編譯的時候就已經確定了。我們用"javac GenericsDemo.java"編譯這個java代碼,可以看到它生成了兩個.class文件:
GenericsDemo$Foo.class GenericsDemo.class GenericsDemo.java
GenericsDemoFoo"命令查看內容,可以發現它裏面沒有運行時的類型信息,也就是說在編譯Foo類的時候,編譯器沒有辦法確定它的具體泛型類型:
Compiled from "GenericsDemo.java"
public class GenericsDemo$Foo<T> {
public T data;
public GenericsDemo$Foo();
}
但是如果我們繼承Foo並且指定泛型類型:
public static class StringFoo extends Foo<String> {
}
再使用"javap GenericsDemo$StringFoo"命令去查看它編譯生成的GenericsDemo$StringFoo.class文件:
Compiled from "GenericsDemo.java"
public class GenericsDemo$StringFoo extends GenericsDemo$Foo<java.lang.String> {
public GenericsDemo$StringFoo();
}
會發現class文件裏攜帶了泛型的類型信息,也就是說StringFoo在編譯的時候就能確定其泛型類型。既然class文件攜帶了信息,那麼java虛擬機在加載的時候就能把這些信息加載進去。
但是要怎麼獲取到它呢?如果使用getClass().getSuperclass()去獲取父類信息是拿不到泛型的信息的,但是java提供了一個getGenericSuperclass的方法可以獲取:
import java.lang.reflect.Field;
public class GenericsDemo {
public static void main(String[] args) throws NoSuchFieldException {
Foo<String> foo = new StringFoo();
System.out.println(foo.getClass().getSuperclass());
System.out.println(foo.getClass().getGenericSuperclass());
}
public static class Foo<T> {
public T data;
}
public static class StringFoo extends Foo<String> {
}
}
class GenericsDemoFoo<java.lang.String>
如果父類使用了泛型那麼getGenericSuperclass拿到的Type實際是ParameterizedType類型,可以通過其getActualTypeArguments方法獲取泛型參數:
Foo<String> foo = new StringFoo();
ParameterizedType superGenericSuperclass = (ParameterizedType) foo.getClass().getGenericSuperclass();
for (Type arg : superGenericSuperclass.getActualTypeArguments()) {
System.out.println(arg);
}
打印如下:
class java.lang.String
所以我們可以再配合class的getTypeParameters方法確定在運行的時候每個泛型參數對應的具體是什麼:
Foo<String> foo = new StringFoo();
ParameterizedType superGenericSuperclass = (ParameterizedType) foo.getClass().getGenericSuperclass();
Class superClass = foo.getClass().getSuperclass();
for (int i = 0; i < superGenericSuperclass.getActualTypeArguments().length; i++) {
System.out.println(superClass.getTypeParameters()[i] + " -> " + superGenericSuperclass.getActualTypeArguments()[i]);
}
打印如下:
T -> class java.lang.String
封裝工具類
所以如果需要在運行的時候獲取泛型的具體類型,可以寫一個子類去繼承它,然後使用getGenericSuperclass獲取父類的泛型信息。但是這樣的方式比較死板,有沒有什麼好的方式可以不用繼承去實現呢?
我們看看下面的代碼:
System.out.println(new Foo<String>().getClass().getGenericSuperclass());
System.out.println(new Foo<String>(){}.getClass().getGenericSuperclass());
它的打印如下:
class java.lang.Object
GenericsDemo$Foo<java.lang.String>
可以看到我們只是在new的後面加上了{},就能通過getClass().getGenericSuperclass()去獲取泛型信息了。
這樣是因爲加上{}之後實際上已經是一個匿名內部類了。通過“javac GenericsDemo.java”命令編譯,可以看到編出出來多了個“GenericsDemo$1.class”文件。
匿名內部類的實現原理實際上就是給它創建一個名爲GenericsDemo$XXX的子類,所以從他的class信息裏面可以看到父類的泛型信息:
Compiled from "GenericsDemo.java"
final class GenericsDemo$1 extends GenericsDemo$Foo<java.lang.String> {
GenericsDemo$1();
}
基於上面的知識點:
- 匿名內部類會生成一個子類
- 生成的子類攜帶了父類的泛型信息
我們可以寫一個抽象的Token類,強制必須創建匿名內部類。然後通過查找父類的泛型信息的方式獲取具體的泛型類型:
public class GenericsDemo {
@Test
public void main() {
new Token<Foo<String>>() {}.parseGenericInfo();
}
public static class Foo<T> {
T data;
}
public static abstract class Token<T> {
public void parseGenericInfo() {
// GenericsDemo$Token<GenericsDemo$Foo<java.lang.String>>
ParameterizedType genericSuperclass = (ParameterizedType) this.getClass().getGenericSuperclass();
// GenericsDemo$Foo<java.lang.String>
ParameterizedType targetGenericClass = (ParameterizedType) genericSuperclass.getActualTypeArguments()[0];
// class GenericsDemo$Foo
Class targetClass = (Class) targetGenericClass.getRawType();
for (int i = 0; i < targetGenericClass.getActualTypeArguments().length; i++) {
System.out.println(targetClass.getTypeParameters()[i] + " -> " + targetGenericClass.getActualTypeArguments()[i]);
}
}
}
}
可以看到這個代碼已經和Gson裏面解析泛型的代碼類似了:
Type type = new TypeToken<Foo>() {}.getType();
Foo foo = new Gson().fromJson(json, type);