JAVA運行時的泛型擦除與反序列化的應用

前段日子在使用google-http-client.jar 這個組件作http請求時,發現一件有趣的事情,具體代碼以下:java

try {
            HttpTransport transport = new NetHttpTransport.Builder().doNotValidateCertificate().build();
            requestFactory = transport.createRequestFactory(new HttpRequestInitializer() {
                @Override
                public void initialize(HttpRequest request) {
                    int timeout = 5 * 1000;
                    request.setReadTimeout(timeout);
                    request.setParser(new JsonObjectParser(new JacksonFactory()));
                    request.setThrowExceptionOnExecuteError(false);
                    logger.debug("set timeout = {} milliseconds", timeout);
                }
            });
        } catch (GeneralSecurityException e) {
            logger.error("init static members failed:", e);
        }
         HttpRequest request = requestFactory.buildPostRequest(new GenericUrl(url), content);
         HttpResponse response  =request.execute();
         Bean ret = (Map<String, Object>)response.parseAs(Bean.class);
         ......

這是一段很簡單的http請求的代碼,引發我注意的是最後一段代碼,而且有個疑問:
爲何HttpResponse.parseAs方法能夠經過入參Bean.class就可以將結果裝配到Bean類,並返回Bean類型?
事實上,HttpResponse.parseAs有兩個同名的重載方法:spring

public <T> T parseAs(Class<T> dataClass) throws IOException {
    if (!hasMessageBody()) {
      return null;
    }
    return request.getParser().parseAndClose(getContent(), getContentCharset(), dataClass);
  }

public Object parseAs(Type dataType) throws IOException {
    if (!hasMessageBody()) {
      return null;
    }
    return request.getParser().parseAndClose(getContent(), getContentCharset(), dataType);
  }

兩個入參不一樣,返回的類型也不一樣,第一個方法能夠在編譯期返回確切的類型,第二個只能返回Object類型,須要使用者自行強轉。那麼這兩個方法到底有什麼區別呢,既然存在確定是爲了解決什麼問題吧。咱們來看看這兩個方法用在哪兒:app

一、Bean ret = response.parseAs(Bean.class);
二、Map<String, Object> ret = (Map<String, Object>)response.parseAs(new TypeToken<Map<String, Object>>() {}.getType());

相信已經有的朋友已經看出來了, 像Map<String, Object>,List<Object>這些帶有泛型的類型是沒法直接經過.class的靜態變量獲取的,就算咱們能夠經過Map.class獲取到,但獲得的倒是Map<Object, Object>,和Map<String, Object>仍是不同的。泛型存在於編譯期,在運行時Map<Integer, Integer>和Map<String, String>的類實例(Class對象)是同一個,這是爲了防止在運行期過多建立類實例,防止類型膨脹,減小運行時開銷,這樣的實現不可避免的就須要在運行時將泛型擦除,因此第二個parseAs方法就是爲了動態的在運行時獲取帶泛型的實際類型,從而反序列化到該類型。泛型在運行時被擦除和在運行時獲取泛型的實際類型看似矛盾的兩個問題,前者表述沒有問題,後者在必定條件下也是對的,爲何這麼說,咱們來看怎麼獲取運行時對象a的泛型指代的實際類型,請看以下代碼:ide

package org.hxb.spring.generic;

import java.lang.reflect.ParameterizedType;
import java.util.Arrays;
import java.util.Map;

import org.junit.Test;

public class GenericTest {

    @Test
    public void test1() {
        Bean<Map<String, Integer>> a = new Bean<Map<String, Integer>>();
        System.out.println(a.getClass().getGenericSuperclass().getTypeName());
        ParameterizedType type = (ParameterizedType) a.getClass().getGenericSuperclass();
        if (type.getActualTypeArguments() != null) {
            System.out.println(Arrays.asList(type.getActualTypeArguments()));
        }

    }

    @Test
    public void test2() {
        Bean<Map<String, Integer>> a = new Bean<Map<String, Integer>>() {
        };
        ParameterizedType type = (ParameterizedType) a.getClass().getGenericSuperclass();
        if (type.getActualTypeArguments() != null) {
            System.out.println(Arrays.asList(type.getActualTypeArguments()));
        }
    }

}

class Father<T> {

}

class Bean<T> extends Father<T> {

}
輸出:
[T]
[java.util.Map<java.lang.String, java.lang.Integer>]

有人會問我,爲何Bean<T>要繼承一個Father<T>? 由於不這麼作會致使(ParameterizedType)a.getClass().getGenericSuperclass()語句報cast exception,getGenericSuperclass方法jdk 1.5 以後加入的,返回直接父類,繼承的父類。(泛型也是同期引入的,同期引入的還有接口java.lang.reflect.Type,以及一些和java.lang.Class 同級別的實現類如ParameterizedType等),那第二Test爲何能夠獲得運行時真實類型?不知道你們也沒有注意到這個細微的差異:ui

Bean<Map<String, Integer>> a = new Bean<Map<String, Integer>>();
 Bean<Map<String, Integer>> a = new Bean<Map<String, Integer>>(){};

下面那句話多了一對花括號,相信你們都知道這是什麼意思,這樣就建立了一個匿名類,google

clipboard.png第一種方法顯示a的類型是Bean<T>url

clipboard.png第一種方法顯示a的類型是GenericTest$1spa

匿名類繼承類型Bean<Map<String, Integer>>,而這個匿名類是在運行時定義的,因此保留了泛型的實際類型(實際就是至關於Bean extends Father<Intger>,此時繼承的是肯定類型)
因此getGenericSuperclass方法返回一個ParameterizedType的結果,而後經過ParameterizedType的getActualTypeArguments方法即可以獲取實際的類型,實際上用這種方法的話Bean就無需在編譯器繼承某個父類了,直接在運行時聲明一個匿名類便可:debug

package org.hxb.spring.generic;

import java.lang.reflect.ParameterizedType;
import java.util.Arrays;
import java.util.Map;

import org.junit.Test;

public class GenericTest {

    @Test
    public void test2() {
        Bean<Map<String, Integer>> a = new Bean<Map<String, Integer>>() {
        };
        ParameterizedType type = (ParameterizedType) a.getClass().getGenericSuperclass();
        if (type.getActualTypeArguments() != null) {
            System.out.println(Arrays.asList(type.getActualTypeArguments()));
        }
    }

}

class Bean<T> {

}

上述代碼亦能夠輸出實際類型。code


回到HttpResponse的第二parseAs方法的用法:Map<String, Object> ret = (Map<String, Object>)response.parseAs(new TypeToken<Map<String, Object>>() {}.getType()),經過上面的分析,咱們能夠知道,TypeToken.getType()方法其實也是用來獲取泛型的實際類型的,這樣就能夠將響應反序列化爲帶泛型的類型了。咱們能夠作以下實驗:

package org.hxb.spring.generic;

import java.lang.reflect.ParameterizedType;
import java.util.Map;

import org.junit.Test;

import com.google.common.reflect.TypeToken;

public class GenericTest {

    @Test
    public void test2() {
        Bean<Map<String, Integer>> a = new Bean<Map<String, Integer>>() {
        };
        ParameterizedType type = (ParameterizedType) a.getClass().getGenericSuperclass();
        if (type.getActualTypeArguments() != null) {
            System.out.println(type.getActualTypeArguments()[0]);
        }
    }
    @Test
    public void test3() {
        System.out.println(new TypeToken<Map<String, Integer>>() {}.getType());
    }

}

class Bean<T> {

}
實際輸出:

clipboard.png

實驗結果和咱們猜測的那樣,咱們再看看TypeToken的無參構造方法,

clipboard.png
clipboard.png

無參構造方法的訪問權限是protected,有人會問了,那我怎麼實例化?呵呵,其實做者的意圖就是爲了確保你不能直接實例化TypeToken,可是咱們能夠用匿名實現類直接繼承TypeToken並實例化(就是多了對花括號{})。無參構造方法調用了父類的capture(捕獲)方法,從截圖中能夠看到,該方法調用了getGenericSuperclass,返回而且判斷父類的類型是否是ParameterizedType,不是的話便拋出異常,是就返回第一個。這也驗證了咱們的想法,其實parseAs方法就是用了上面的原理。在不少反序列化的開源組件中,都用了這個原理例如com.fasterxml.jackson.databind.ObjectMapper.ObjectMapper 的readValue方法,因此咱們會常常見到實例化的時候會多個花括號。

相關文章
相關標籤/搜索