泛型新认知

文章目录

概要

想要成为一个写好java代码的Coder,泛型一定是不可少的。大家都听说过java的泛型是伪泛型,是编译期进行校验使用的,而不是在运行期生效。

怎么理解?

就是泛型里的类信息在实际运行的时候,java对象本身是获取不到的,需要从java对象的类信息才能获取到。
注:本文是通过protostuff框架,实现了protobuf的序列化/反序列化功能
protostuff框架地址

一个小demo

序列化

我们今天通过一个序列化/反序列化的例子来验证说明一下。我们的demo是基于kafka的序列化,反序列化接口来进行实现。只保留需要我们实现的接口方法

java 复制代码
public interface Serializer<T> extends Closeable {

    /**
     * Convert {@code data} into a byte array.
     *
     * @param topic topic associated with data
     * @param data typed data
     * @return serialized bytes
     */
    byte[] serialize(String topic, T data);

}

此时我们有个PO对象类:Company

java 复制代码
@Data
@NoArgsConstructor
@AllArgsConstructor
@Builder
public class Company {
    private String name;
    private String address;
}

然后我们很快写出来了Company的序列化类

java 复制代码
public class ProtoCompanySerializer implements Serializer<Company> {
    
    @Override
    public byte[] serialize(String topic, Company data) {
        if (data == null) {
            return null;
        }
        Schema<Company> schema = RuntimeSchema.getSchema(Company.class);
        LinkedBuffer buffer = LinkedBuffer.allocate(LinkedBuffer.DEFAULT_BUFFER_SIZE);
        return ProtostuffIOUtil.toByteArray(data, schema, buffer);
    }
}

看着很简单,是吧?不过如果我们有多个PO类呢?譬如还有一个User类,再写一个

java 复制代码
public class ProtoUserSerializer implements Serializer<User> {

    @Override
    public byte[] serialize(String topic, User data) {
        if (data == null) {
            return null;
        }
        Schema<User> schema = RuntimeSchema.getSchema(User.class);
        LinkedBuffer buffer = LinkedBuffer.allocate(LinkedBuffer.DEFAULT_BUFFER_SIZE);
        return ProtostuffIOUtil.toByteArray(data, schema, buffer);
    }
}

这么写下去,确实是很清晰,但是我们很快就能发现这两个类的实现,除了入参类型不同、Schema泛型不同,其他都是一样的。那写个通用的不就行了?

java 复制代码
public class ProtostuffSerializer implements Serializer {

    @Override
    public byte[] serialize(String topic, Object data) {
        if (data == null) {
            return null;
        }
        Schema schema = RuntimeSchema.getSchema(data.getClass());
        LinkedBuffer buffer = LinkedBuffer.allocate(LinkedBuffer.DEFAULT_BUFFER_SIZE);
        return ProtostuffIOUtil.toByteArray(data, schema, buffer);
    }
}

Object类型的对象作为入参,看着很别扭,是吧?但是实际运行没有任何问题,当然我们可以通过泛型,让代码看起来稍微优雅一些

java 复制代码
public class ProtoCommonSerializer<T> implements Serializer<T> {

    @Override
    public byte[] serialize(String topic, T data) {
        if (data == null) {
            return null;
        }
        Schema<T> schema = (Schema<T>) RuntimeSchema.getSchema(data.getClass());
        LinkedBuffer buffer = LinkedBuffer.allocate(LinkedBuffer.DEFAULT_BUFFER_SIZE);
        return ProtostuffIOUtil.toByteArray(data, schema, buffer);
    }
}

但是这样的优雅其实并没有什么大用,因为为了支持一个序列化工具对象序列化多种类型的对象,实际我们在实例化ProtoCommonSerializer时,根本没法指定泛型

反序列化

java 复制代码
public interface Deserializer<T> extends Closeable {

    /**
     * Deserialize a record value from a byte array into a value or object.
     * @param topic topic associated with the data
     * @param data serialized bytes; may be null; implementations are recommended to handle null by returning a value or null rather than throwing an exception.
     * @return deserialized typed data; may be null
     */
    T deserialize(String topic, byte[] data);

    

有了上面的先验认知,我们当然想直接写通用的反序列化工具类,然后我们开始动笔,然后很快就发现一个问题,入参没有对象,我们没法判断入参的data这个字节数组要转化为哪个Class的对象
那换成上面后来写的泛型那种呢?

java 复制代码
public class ProtoCommonDeserializer<T> implements Deserializer<T> {

    @SneakyThrows
    @Override
    public T deserialize(String topic, byte[] data) {
        if (data == null) {
            return null;
        }
        Class<T> tClazz = (Class<T>) GenericTypeResolver.resolveTypeArgument(this.getClass(), ProtoCommonDeserializer.class);
        Schema<T> schema = RuntimeSchema.getSchema(tClazz);
        T t = tClazz.newInstance();
        ProtostuffIOUtil.mergeFrom(data, t, schema);
        return t;
    }
}

然后美滋滋写个单测

java 复制代码
@Test
public void testDeserialize1() {
    String topic = "abc";
    Company company = new Company("wangsansan", "not same place");
    ProtostuffSerializer protostuffSerializer = new ProtostuffSerializer();
    byte[] data = protostuffSerializer.serialize(topic, company);
    ProtoCommonDeserializer<Company> protoCommonDeserializer = new ProtoCommonDeserializer<Company>();
    Company company2 = protoCommonDeserializer.deserialize(topic, data);
    assert "wangsansan".equals(company2.getName());
}

一跑,哗擦,空指针。看了下异常栈,Schema<T> schema = RuntimeSchema.getSchema(tClazz);这一行空指针,也就是Class<T> tClazz = (Class<T>) GenericTypeResolver.resolveTypeArgument(this.getClass(), ProtoCommonDeserializer.class);没拿到Class信息,问了下豆包,豆包说出了那句我们都知道的八股文:

Java的泛型在运行时是擦除的,只有编译时有限制作用

而Spring的工具类GenericTypeResolver拿到的是运行时类信息,但是因为protoCommonDeserializer对象的类信息是ProtoCommonDeserializer,所以根本拿不到泛型参数,豆包给的解法是修改为
ProtoCommonDeserializer<Company> protoCommonDeserializer = new ProtoCommonDeserializer<Company>(){};注意,跟之前比,多了个花括号 ,也就是protoCommonDeserializer变成了一个匿名类的对象,而不再是ProtoCommonDeserializer的对象了,这个匿名类是ProtoCommonDeserializer<Company>的子类,且被load进jvm里了,所以该匿名类能查到的类的泛型参数

但是!!!

这样的话,protoCommonDeserializer就只能反序列化某一种类型的对象了

怎么办呢

如何实现一个反序列化对象可以通用,反序列化任何类型的对象呢?

参考ProtostuffSerializer写个ProtostuffDeserializer

java 复制代码
public class ProtostuffDeserializer implements Deserializer {

    @Override
    public Object deserialize(String topic, byte[] data) {
        if (data == null) {
            return null;
        }
        Schema schema = RuntimeSchema.getSchema(Object.class);
        Company ans = new Company();
        ProtostuffIOUtil.mergeFrom(data, ans, schema);
        return ans;
    }
}

跑个单测试试

java 复制代码
@Test
public void testDeserialize1() {
    String topic = "abc";
    Company company = new Company("wangsansan", "not same place");
    ProtostuffSerializer protostuffSerializer = new ProtostuffSerializer();
    byte[] data = protostuffSerializer.serialize(topic, company);
    ProtostuffDeserializer protostuffDeserializer = new ProtostuffDeserializer();
    Company company1 = (Company) protostuffDeserializer.deserialize(topic, data);
    assert "wangsansan".equals(company1.getName());
}

这次又是空指针,不过是protostuff框架报的了,其实是因为protostuff最外层的类信息是没有序列化到字节数组里,需要程序员指定?

何如?软件学的精髓是啥?封装,抽象呗,于是,我们再加一层

java 复制代码
@NoArgsConstructor
@AllArgsConstructor
@Data
public class DomainWrapper<T> {

    private T domain;

    public static <T> DomainWrapper<T> of(T obj) {
        return new DomainWrapper<>(obj);
    }

}

写出了这样的通用反序列化类

java 复制代码
public class ProtoWrapperDeserializer implements Deserializer<DomainWrapper> {


    @Override
    public DomainWrapper deserialize(String topic, byte[] data) {
        if (data == null) {
            return null;
        }
        Schema<DomainWrapper> schema = RuntimeSchema.getSchema(DomainWrapper.class);
        DomainWrapper ans = new DomainWrapper();
        ProtostuffIOUtil.mergeFrom(data, ans, schema);
        return ans;
    }
}

写个单测试试

java 复制代码
@Test
public void testWrapperDeserialize() {
    String topic = "abc";
    Company company = new Company("wangsansan", "not same place");
    User user = new User("wangsansan", 25);
    ProtostuffSerializer protostuffSerializer = new ProtostuffSerializer();
    byte[] data1 = protostuffSerializer.serialize(topic, DomainWrapper.of(company));
    byte[] data2 = protostuffSerializer.serialize(topic, DomainWrapper.of(user));
    ProtoWrapperDeserializer protoWrapperDeserializer = new ProtoWrapperDeserializer();
    DomainWrapper<Company> obj1 = protoWrapperDeserializer.deserialize(topic, data1);
    DomainWrapper<User> obj2 = protoWrapperDeserializer.deserialize(topic, data2);
    assert obj1.getDomain().getName().equals("wangsansan");
    assert obj2.getDomain().getAge() == 25;
}

执行是没问题的,因为protostuffSerializer对象的类信息是有泛型的->DomainWrapper,ProtoWrapperDeserializer类本身是不支持设置泛型的。

问题

虽然都是BeanWrapper,但是反序列化时,ProtostuffIOUtil.mergeFrom(data, ans, schema);是怎么知道里面的domain是什么类型,然后反序列化成功的呢?

这个问题,我们下次探讨一下。

相关推荐
奋进的芋圆2 小时前
Java 延时任务实现方案详解(适用于 Spring Boot 3)
java·spring boot·redis·rabbitmq
sxlishaobin2 小时前
设计模式之桥接模式
java·设计模式·桥接模式
model20052 小时前
alibaba linux3 系统盘网站迁移数据盘
java·服务器·前端
荒诞硬汉2 小时前
JavaBean相关补充
java·开发语言
提笔忘字的帝国2 小时前
【教程】macOS 如何完全卸载 Java 开发环境
java·开发语言·macos
2501_941882483 小时前
从灰度发布到流量切分的互联网工程语法控制与多语言实现实践思路随笔分享
java·开发语言
華勳全栈3 小时前
两天开发完成智能体平台
java·spring·go
alonewolf_993 小时前
Spring MVC重点功能底层源码深度解析
java·spring·mvc
沛沛老爹3 小时前
Java泛型擦除:原理、实践与应对策略
java·开发语言·人工智能·企业开发·发展趋势·技术原理
专注_每天进步一点点3 小时前
【java开发】写接口文档的札记
java·开发语言