09月21, 2019

记一次 Java 中泛型的 ClassCastException

最近公司同事自己写了一个基于redis的延时调度,但是在执行任务时,但是泛型这边做得不好,添加延时任务的时候是采用的泛型,但是从 redis 取出来的数据确实字符串,他的解释是在父类中调用子类方法,也就是常用的设计模式之一的 ‘’模板方法模式 “。废话不多说,直接上代码演示

首先是定义一个抽象的消费接口

public interface MessageConsumerInterface<T> {
    void execute(T t);
}

然后是抽象类,抽象类里头有一个 getTClass 来获取当前的泛型类型

public abstract class AbstractMessageConsumer<T> implements MessageConsumerInterface<T> {

    @SuppressWarnings("unchecked")
    public Class<T> getTClass() {
        return (Class<T>) ((ParameterizedType) getClass().getGenericSuperclass()).getActualTypeArguments()[0];
    }
}

最后是真正的消费者

public class MessageConsumer<Apple> extends AbstractMessageConsumer<Apple> {
    @Override
    public void execute(Apple apple) {
        Class<Apple> tClass = this.getTClass();
        System.out.println(tClass);
    }
}

@Data
public class Apple {
    private String name;
    private Integer age;
}

这一切看似没有什么问题,但是会报一个类型转换错误

    @Test
    public void  test(){
        AbstractMessageConsumer<Apple> consumer = new MessageConsumer<>();
        consumer.execute(new Apple());
    }

java.lang.ClassCastException: sun.reflect.generics.reflectiveObjects.TypeVariableImpl cannot be cast to java.lang.Class
    at AbstractMessageConsumer.java:22

之前我就写过类似的东西,在父类中是绝对可以获取到子类泛型的,于是翻出以前的代码对比了一下,发现了一个问题,就是在public class MessageConsumer<Apple> extends AbstractMessageConsumer<Apple>这里,我以前写的是public class MessageConsumer extends AbstractMessageConsumer<Apple>,这里最终的实现子类其实不需要再写泛型了,因为他的父类AbstractMessageConsumer<Apple>已经是一个泛型类了,这里如果子类再写一个泛型,其实是覆盖了父类的泛型,所以在父类里头去获取泛型是会失败的。

这里其实涉及到了一个基本的概念,就是泛型的擦除,其实在jvm里头,是没有泛型这个东西的,像我们上叙述的例子里头,其实在调用的时候是通过接口接口对外暴露,通常在父类里头获取子类的泛型的时候,并不知道子类具体会是什么类型,所以对于父类来说,泛型都是一个T,其实这个时候,这个TObject并没有区别,如果是实在没有办法获取到子类的泛型类型,也没什么关系,可以通过参数把子类泛型的全类名以字符串形式传进来,然后再通过Class.forName去获取也行,当然最好使做个缓存,毕竟这样需要去类加载器查找,会有一定的性能损耗。

@Resource
private MessageConsumerInterface messageConsumer;

Object object = null;
try {
    // 通过 DataClassName 获取 T的类型,然后直接使用fastjson转成对应的类型
   object = JSONObject.parseObject(delayExecuteMessage.getData(), Class.forName(delayExecuteMessage.getDataClassName()));
} catch (ClassNotFoundException e) {
   e.printStackTrace();
}
// 这里的 execute 方法其实可以传入任意类型
messageConsumer.execute(object);

如果还是没有理解,可以看这个例子

这个是原文中的源代码

    public <T> T getBean(Class<T> requiredType) throws BeansException {
        Map<Class<T>,T> map = new HashMap<>();
        return map.get(requiredType);
    }

这个是上边一段代码反编译之后的代码

  public <T> T getBean(Class<T> requiredType) throws BeansException
  {
    Map map = new java.util.HashMap();
    return map.get(requiredType);
  }

我们发现 Map 的 T 其实已经被擦除了,只是取出值的时候又被转成了 T,因为在方法带了 T 的标识,所以虚拟机会自动进行类型的转换。

本文链接:https://www.putin.ink/post/genericity-ClassCastException.html

-- EOF --

Comments