泛型新认知

文章目录

概要

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

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

相关推荐
仰望星空_Star8 分钟前
Java证书操作
java·开发语言
河北小博博11 分钟前
分布式系统稳定性基石:熔断与限流的深度解析(附Python实战)
java·开发语言·python
岳轩子11 分钟前
JVM Java 类加载机制与 ClassLoader 核心知识全总结 第二节
java·开发语言·jvm
J_liaty21 分钟前
Spring Boot + MinIO 文件上传工具类
java·spring boot·后端·minio
2601_9496130225 分钟前
flutter_for_openharmony家庭药箱管理app实战+药品详情实现
java·前端·flutter
木井巳28 分钟前
【递归算法】求根节点到叶节点数字之和
java·算法·leetcode·深度优先
没有bug.的程序员31 分钟前
Spring Boot 事务管理:@Transactional 失效场景、底层内幕与分布式补偿实战终极指南
java·spring boot·分布式·后端·transactional·失效场景·底层内幕
华农第一蒟蒻39 分钟前
一次服务器CPU飙升的排查与解决
java·运维·服务器·spring boot·arthas
m0_748229991 小时前
帝国CMS后台搭建全攻略
java·c语言·开发语言·学习
码农娟1 小时前
Hutool XML工具-XmlUtil的使用
xml·java