怎樣獲取Java泛型的具體類型 爲什麼拿不到泛型的具體類型 繼承泛型類指定泛型類型 封裝工具類

之前寫過一篇博客介紹過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.class就是內部類Foo的信息了,使用"javap GenericsDemo\Foo"命令查看內容,可以發現它裏面沒有運行時的類型信息,也就是說在編譯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 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();
}

基於上面的知識點:

  1. 匿名內部類會生成一個子類
  2. 生成的子類攜帶了父類的泛型信息

我們可以寫一個抽象的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);
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章