亲宝软件园·资讯

展开

Java获取List<String>中String

raysonxin 人气:0

前言

在写这篇文章之前,我几乎没有思路去定义这个问题。只是知道,List<String>是泛型,是接口List<T>的实现,实例化以后只能存储String类型的对象,仅此而已!

提到泛型,每个Java开发人员都比较熟悉。常见的List、Map<K,V>等;另外,我们在进行工具类、公共包的开发时,也经常使用泛型实现规范化、模板化的目标。

问题场景

最近,在为新系统封装公共包时遇到了一个与泛型有关的问题。在这里,结合实际场景讨论一下。描述一下场景:封装MQ中间件,统一MQ的消息订阅与处理过程,统一MQ相关日志与监控。

解决思路还是比较简单的,整体由三个部分组成(如下图所示):

通过这个图,我们可以知道MessageQueueListener#consumeMessage负责接收消息,转换为指定类型后,交给IMessageSub#processMessage进行处理。IMessageSub<T>是一个泛型接口,consumeMessage需要把消息转换为IMessageSub实例的所需实际类型(如下ConcreteMessageSub1示例的DemoMsg)。

public interface IMessageSub<T> {
 String getTopic();
    String getTag();
    String getConsumerGroup();
    void processMessage(MqEvent<DemoMsg> mqEvent);
}

@Component
public class ConcreteMessageSub1 implements IMessageSub<DemoMsg> {
 public String getTopic(){
     return "TOPIC_TEST"
    }
    
 public String getTag(){
     return "TAG_TEST";
    }
 public String getConsumerGroup(){
     return "CID_TEST"
    }
 public void processMessage(MqEvent<DemoMsg> mqEvent){
     // do something...
    }
}

public class MessageQueueListener implements MessageListenerConcurrently {

    private IMessageSub<?> messageSub;
    
    public ConsumeConcurrentlyStatus consumeMessage(List<MessageExt> msgs, ConsumeConcurrentlyContext context) {
     
    }
}

每个MessageQueueListener对象持有一个IMessageSub的实例messageSub,可能是DemoMsg、DemoMsg2等等,如何才能确定对象MessageQueueListener#messageSub的实际类型呢?

问题讨论

我们知道,对于一个非泛型对象,只需要调用其getClass()方法就可以了。但是对于泛型对象,做同样的操作,结果却不是我们想要的。如下简单示例:

public static void main(String[] args) {
    String str = "";
    System.out.println(str.getClass().getName());
    
    List<String> list=new ArrayList<>();
    System.out.println(list.getClass().getName());
}

输出结果:

java.lang.String
java.util.ArrayList

泛型对象list的输出结果是“java.util.ArrayList”,貌似跟String没有关系了,怎么回事儿呢?这就涉及到Java泛型的实现原理了。

Java的泛型是一种伪泛型,是通过类型擦除(Type Erasure)实现的参数化类型(Parameterized Type),也就是说把所操作的数据类型作为参数的一种语法。具体的历史背景就是:

Java在实现泛型机制时,为了避免新增泛型类型,直接把需要支持泛型的原始类型泛型化,比如:ArrayList变为ArrayList。

这就需要,Java能够实现具备向前、向后兼容性的泛型,也就是说以前使用原始类型的代码可以继续被泛型使用,现在的泛型也可以作为参数传递给原始类型的代码。

为了实现以上功能,Java 设计者将泛型完全作为了语法糖加入了新的语法中,也就是说泛型对于JVM来说是透明的,有泛型的和没有泛型的代码,通过编译器编译后所生成的二进制代码是完全相同的。编译器在编译过程中去除泛型的过程,被称作类型擦除。

泛型的参数化类型本质可以应用在类、接口、方法,于是就产生了泛型类、泛型接口、泛型方法,可以说极大提升了Java代码的灵活性。

考察大家一个小知识,我们天天使用或者见到泛型,如List<E>,你知道它的各个组成部分叫什么名字吗?

类型擦除确实保证了良好的兼容性,但是在很多场景下我们确实需要知道泛型对象的原始信息。比如“问题场景”中获取泛型接口实现类对象的实际类型参数。

虽然在编译期间编译器擦除了泛型,但是在字节码中仍然保留了与泛型有关的信息,这就使得我们可以通过反射来获取泛型擦除前的原始信息。为了表达泛型类型声明,Java提供了接口Type及其子类型。

通过这些API我们可以对泛型的原始信息了如指掌:

解决方案

了解泛型的原理后,结合反射包提供的API,我们就很容易解决第一部分提出的问题了。

结合上面的示例,MessageQueueListener#messageSub是一个泛型接口对象,实际为IMessageSub<T>泛型接口的实现类(如ConcreteMessageSub1),我们的目标就是获取ConcreteMessageSub1实现泛型接口时指定的实际类型参数DemoMsg。所以,需要按照以下步骤进行处理:

用代码实现如下所示:

/**
 * 获取消息执行器范性类型
 *
 * @return 类型
 */
private Type getExecutorGenericType(IEventProcessor eventProcessor) {
    try {
        Optional<Type> typeOptional = Arrays.stream(eventProcessor.getClass().getGenericInterfaces())
            .filter(type -> ParameterizedType.class.isAssignableFrom(type.getClass()))
            .map(type -> (ParameterizedType)type)
            .filter(parameterizedType -> IEventProcessor.class.getTypeName().equals(parameterizedType.getRawType().getTypeName()))
            .map(ParameterizedType::getActualTypeArguments)
            .filter(actualTypes -> actualTypes.length > 0)
            .map(actualTypes -> actualTypes[0])
            .findFirst();
        return typeOptional.orElse(null);
    } catch (Throwable cause) {

    }
    return null;
}

本文总结

依托于泛型提供的API,我们可以开发出灵活的工具及框架,也可以使我们的代码更加简洁高效。可以说,Java的泛型是一种“语法糖”。以复用性更强的方式来提高开发效率,帮助开发人员在编译阶段识别系统存在的安全隐患,以更强的约束力来保证代码的健壮性。

本来只想简单的介绍获取参数化类型的方式,可是当把问题展开的时候,才发现自己对泛型的体系认识不够,每天上下班路上一边学习,一边记录笔记。关于泛型,还有许多要去学习和了解的知识,大家一起进步。

加载全部内容

相关教程
猜你喜欢
用户评论