资讯专栏INFORMATION COLUMN

JAVA运行时的泛型擦除与反序列化的应用

weizx / 629人阅读

摘要:回到的第二方法的用法,通过上面的分析,我们可以知道,方法其实也是用来获取泛型的实际类型的,这样就可以将响应反序列化为带泛型的类型了。在很多反序列化的开源组件中,都用了这个原理例如的方法,所以我们会经常见到实例化的时候会多个花括号。

前段日子在使用google-http-client.jar 这个组件做http请求时,发现一件有趣的事情,具体代码如下:

       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)response.parseAs(Bean.class);
         ......

这是一段很简单的http请求的代码,引起我注意的是最后一段代码,并且有个疑问:
为什么HttpResponse.parseAs方法可以通过入参Bean.class就能够将结果装配到Bean类,并返回Bean类型?
事实上,HttpResponse.parseAs有两个同名的重载方法:

public  T parseAs(Class 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类型,需要使用者自行强转。那么这两个方法到底有什么区别呢,既然存在肯定是为了解决什么问题吧。我们来看看这两个方法用在哪儿:

1、Bean ret = response.parseAs(Bean.class);
2、Map ret = (Map)response.parseAs(new TypeToken>() {}.getType());

相信已经有的朋友已经看出来了, 像Map,List这些带有泛型的类型是无法直接通过.class的静态变量获取的,就算我们可以通过Map.class获取到,但得到的却是Map,和Map还是不一样的。泛型存在于编译期,在运行时Map和Map的类实例(Class对象)是同一个,这是为了防止在运行期过多创建类实例,防止类型膨胀,减少运行时开销,这样的实现不可避免的就需要在运行时将泛型擦除,所以第二个parseAs方法就是为了动态的在运行时获取带泛型的实际类型,从而反序列化到该类型。泛型在运行时被擦除和在运行时获取泛型的实际类型看似矛盾的两个问题,前者表述没有问题,后者在一定条件下也是对的,为什么这么说,我们来看怎么获取运行时对象a的泛型指代的实际类型,请看如下代码:

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> a = new Bean>();
        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> a = new Bean>() {
        };
        ParameterizedType type = (ParameterizedType) a.getClass().getGenericSuperclass();
        if (type.getActualTypeArguments() != null) {
            System.out.println(Arrays.asList(type.getActualTypeArguments()));
        }
    }

}

class Father {

}

class Bean extends Father {

}
输出:
[T]
[java.util.Map]

有人会问我,为什么Bean要继承一个Father? 因为不这么做会导致(ParameterizedType)a.getClass().getGenericSuperclass()语句报cast exception,getGenericSuperclass方法jdk 1.5 之后加入的,返回直接父类,继承的父类。(泛型也是同期引入的,同期引入的还有接口java.lang.reflect.Type,以及一些和java.lang.Class 同级别的实现类如ParameterizedType等),那第二Test为什么可以得到运行时真实类型?不知道大家也没有注意到这个细微的差别:

 Bean> a = new Bean>();
 Bean> a = new Bean>(){};

下面那句话多了一对花括号,相信大家都知道这是什么意思,这样就创建了一个匿名类,

第一种方法显示a的类型是Bean

第一种方法显示a的类型是GenericTest$1

匿名类继承类型Bean>,而这个匿名类是在运行时定义的,所以保留了泛型的实际类型(实际就是相当于Bean extends Father,此时继承的是确定类型)
所以getGenericSuperclass方法返回一个ParameterizedType的结果,然后通过ParameterizedType的getActualTypeArguments方法便可以获取实际的类型,实际上用这种方法的话Bean就无需在编译器继承某个父类了,直接在运行时声明一个匿名类即可:

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> a = new Bean>() {
        };
        ParameterizedType type = (ParameterizedType) a.getClass().getGenericSuperclass();
        if (type.getActualTypeArguments() != null) {
            System.out.println(Arrays.asList(type.getActualTypeArguments()));
        }
    }

}

class Bean {

}

上述代码亦可以输出实际类型。

回到HttpResponse的第二parseAs方法的用法:Map ret = (Map)response.parseAs(new TypeToken>() {}.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> a = new Bean>() {
        };
        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>() {}.getType());
    }

}

class Bean {

}
实际输出:

  实验结果和我们猜想的那样,我们再看看TypeToken的无参构造方法,


无参构造方法的访问权限是protected,有人会问了,那我怎么实例化?呵呵,其实作者的意图就是为了确保你不能直接实例化TypeToken,但是我们可以用匿名实现类直接继承TypeToken并实例化(就是多了对花括号{})。
无参构造方法调用了父类的capture(捕获)方法,从截图中可以看到,该方法调用了getGenericSuperclass,返回并且判断父类的类型是不是ParameterizedType,不是的话便抛出异常,是就返回第一个。这也验证了我们的想法,其实parseAs方法就是用了上面的原理。
在很多反序列化的开源组件中,都用了这个原理例如com.fasterxml.jackson.databind.ObjectMapper.ObjectMapper 的readValue方法,所以我们会经常见到实例化的时候会多个花括号。

文章版权归作者所有,未经允许请勿转载,若此文章存在违规行为,您可以联系管理员删除。

转载请注明本文地址:https://www.ucloud.cn/yun/68802.html

相关文章

  • Java系列之泛型

    摘要:总结泛型的类型必须是引用类型,不能是基本类型,泛型的个数可以有多个,可以使用对创建对象时的泛型类型以及方法参数类型进行限制,如使用关键字和对泛型的具体类型进行向下限制或向上限制,最后一点,可以声明泛型数组,但是不能创建泛型数组的实例。 自从 JDK 1.5 提供了泛型概念,泛型使得开发者可以定义较为安全的类型,不至于强制类型转化时出现类型转化异常,在没有反省之前,可以通过 Object...

    MadPecker 评论0 收藏0
  • Java™ 教程(类型擦除

    类型擦除 泛型被引入到Java语言中,以便在编译时提供更严格的类型检查并支持通用编程,为了实现泛型,Java编译器将类型擦除应用于: 如果类型参数是无界的,则用它们的边界或Object替换泛型类型中的所有类型参数,因此,生成的字节码仅包含普通的类、接口和方法。 如有必要,插入类型转换以保持类型安全。 生成桥接方法以保留扩展泛型类型中的多态性。 类型擦除确保不为参数化类型创建新类,因此,泛型不会...

    zsy888 评论0 收藏0
  • Java知识点总结(Java泛型

    摘要:知识点总结泛型知识点总结泛型泛型泛型就是参数化类型适用于多种数据类型执行相同的代码泛型中的类型在使用时指定泛型归根到底就是模版优点使用泛型时,在实际使用之前类型就已经确定了,不需要强制类型转换。 Java知识点总结(Java泛型) @(Java知识点总结)[Java, Java泛型] [toc] 泛型 泛型就是参数化类型 适用于多种数据类型执行相同的代码 泛型中的类型在使用时指定 泛...

    linkin 评论0 收藏0
  • Java知识点总结(反射-反射操作泛型

    摘要:知识点总结反射反射操作泛型知识点总结反射采用泛型擦除的机制来引入泛型。中的泛型仅仅是给编译器使用的,确保数据的安全性和免去强制类型转换的麻烦。 Java知识点总结(反射-反射操作泛型) @(Java知识点总结)[Java, 反射] Java采用泛型擦除的机制来引入泛型。Java中的泛型仅仅是给编译器javac使用的, 确保数据的安全性和免去强制类型转换的麻烦 。但是,__一旦编译完成,...

    AprilJ 评论0 收藏0

发表评论

0条评论

weizx

|高级讲师

TA的文章

阅读更多
最新活动
阅读需要支付1元查看
<