泛型新认知

文章目录

概要

想要成为一个写好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是什么类型,然后反序列化成功的呢?

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

相关推荐
星空19 分钟前
maven镜像
java·maven
chuan.bai1 小时前
Java RAG 实战附录:qwen3 与 bge-m3 模型切换指南
java·人工智能·算法
SL_staff1 小时前
销售知识管理的断点分析与技术解法:从3天找话术到秒级检索
java·开源·github
荣码1 小时前
向量数据库选型:4种方案全测了,选错重来成本最高
java·python
rannn_1111 小时前
【力扣hot100】二叉树专题+总结
java·算法·leetcode·二叉树
摇滚侠2 小时前
《Docker技术入门与实战 第4版》阅读笔记 6 使用 Dockerfile 创建镜像 2
java·笔记·docker
余额瞒着我当琳2 小时前
C++--vector第二讲:手写 C++ STL:vector 源码剖析与迭代器失效分析
android·java·c++
资源大佬星 课it2 小时前
黑马 Java+AI新版V16零基础就业班视频课程百度网盘下载
java·开发语言·人工智能
二十雨辰2 小时前
[Java]-微服务面试题
java·开发语言·微服务
我命由我123452 小时前
Compose Codelab 学习 - Compose 中的基本布局
android·java·java-ee·kotlin·android studio·android-studio·android runtime