Java如何获取List中的String详解

前言

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

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

问题场景

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

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

  • IMessageSub:通过接口定义了需要订阅的Topic、Tag、消费组,并提供消息处理入口;
  • MessageQueueListener:实现了RocketMQ的接口MessageListenerConcurrently,是真正的消费者,负责消息消费处理。
  • MqSubManager:负责完成IMessageSub Bean的发现(所有实现类均为Spring的Bean对象),并通过MessageQueueListener实现对MQ的最终订阅。

Java如何获取List<String>中的String详解_第1张图片

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

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

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

public class MessageQueueListener implements MessageListenerConcurrently {

    private IMessageSub messageSub;
    
    public ConsumeConcurrentlyStatus consumeMessage(List 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 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,你知道它的各个组成部分叫什么名字吗?

  • List中的E称为类型参数变量,整个称为List泛型类型。
  • List中的Integer称为实际类型参数,整个List称为参数化的类型ParameterizedType。

Java如何获取List<String>中的String详解_第2张图片

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

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

Java如何获取List<String>中的String详解_第3张图片

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

  • Class类:描述具体类型,比较常见,可通过getClass()获取。
  • TypeVariable接口:描述类型变量(如:T extends Comparable ),通过getClass().getTypeParameters()。
  • WildcardType接口:描述通配符 (如:? super T )。
  • ParameterizedType接口:描述泛型类或接口类型(如:Comparable),可通过getClass().getGenericInterfaces()获取后筛选。
  • GenericArrayType接口:描述泛型数组(如:T[])

解决方案

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

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

  • 获取对象messageSub所属类(如:ConcreteMessageSub1)实现的接口列表;
  • 仅保留接口列表中的描述泛型接口的参数化类型对象(ParameterizedType);
  • 在参数化类型对象中获取类型为IMessageSub的描述对象,即我们需要的参数化类型对象;
  • 获取该参数化类型对象的第一个实际类型参数(接口声明只有一个泛型参数)。

用代码实现如下所示:

/**
 * 获取消息执行器范性类型
 *
 * @return 类型
 */
private Type getExecutorGenericType(IEventProcessor eventProcessor) {
    try {
        Optional 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的泛型是一种“语法糖”。以复用性更强的方式来提高开发效率,帮助开发人员在编译阶段识别系统存在的安全隐患,以更强的约束力来保证代码的健壮性。

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

到此这篇关于Java如何获取List<String>中的String的文章就介绍到这了,更多相关Java获取List<String>中String内容请搜索脚本之家以前的文章或继续浏览下面的相关文章希望大家以后多多支持脚本之家!

你可能感兴趣的:(Java如何获取List中的String详解)