聊一聊 - 泛型检索实现

292 阅读11分钟

前言

Java 编译器在编译泛型代码时,会将所有泛型类型参数替换为它们的限定类型或 Object 类型,并且插入必要的类型转换,这被称为泛型擦除(Generic Type Erasure)。但是,如果我们希望在编译时拿到泛型类型,就需要借助一些特殊的技术。

举个例子哈,一种常见的技术是使用 Annotation Processing Tool(APT),在编译期间生成代码。我们可以编写一个继承自泛型类或实现泛型接口的包装类,然后使用 APT 在编译期间对这个包装类进行解析,从而获取泛型类型所对应的数据类型。

那么我们需要如何实现获取泛型类型所对应的数据类型的逻辑呢?我搜索了 Google 和 Stack Overflow,但是没有找到基于 Java 的通用泛型检索算法实现。因此,我将在本文中介绍一种自己实现的泛型检索算法,以供大家参考和选择。

实现原理

泛型类

在进入泛型检索算法的原理之前,我们需要先了解 Java 中泛型类继承的形式,以及明确我们所需要实现的泛型检索的效果。

public class SuperGenericClass<T,F>{
    ...
}
​
​
public class CurrentClass extends SuperGenericClass<String,Double>{
    ...
}

在上述代码中,CurrentClass 是一个普通类,它继承自 SuperGenericClass 泛型类。由于父类 SuperGenericClass 使用了两个泛型类型 T 和 F,而在 CurrentClass 中指定了这两个泛型类型,分别为 StringDouble。因此,我们可以通过检索 CurrentClass 的继承实现关系,来获取泛型类型所对应的数据类型。具体来说,我们可以得到以下结果:

  • T 对应 String
  • F 对应 Double

这样,我们就可以在编译期间拿到泛型类型所对应的数据类型,以便进行相关操作。

在 Java 中,类确实只能单继承,但是接口可以多继承。因此,我们可以将类和接口的继承关系都看做一个有向图,其中类是一个有向链,而接口可以有多个父接口,因此是一个有向无环图(DAG)。节点代表一个类或接口,边表示类或接口之间的继承关系。节点上承载一个加载了泛型类型的数组,用于存储泛型类型信息。

因此,我们可以把问题转化为从一个节点开始,依次沿着边向上遍历直到到达根节点为止。在遍历过程中,我们不断将当前节点上的泛型类型数组中的类型信息传递到上一个节点,直至到达根节点,完成泛型类型检索的过程。

类-链表

图A

在上图的首尾节点中,再插入一个节点,并在这个节点中也实现了一个泛型类型指定数据类型的过程,那么对于泛型类型的检索是否还是一样的呢?

类-链表-三

图B

根据上图所示,转化过程还是相差无几。那么我们就加一点难度,在中间这个节点中,再加入一个泛型类型,并且改变泛型顺序,如下图所示:

类-链表-多

图C

根据图B和图C,读者可能已经注意到,在图B中,F 在 Node 的 Generic 数组中的索引位置为 0。而在图C中,F 在Node 的 Generic 数组中的索引位置为 1。这引出了第二个问题:在传递泛型类型的过程中,需要指定泛型类型在数组中的索引位置,并且在继承关系中过滤掉其他泛型类型。因此,更新后的结构如下所示:

类-链表-继承

查找泛型的伪代码如下:

public GenericInfo[] findByClass(Node targetClass) {
    // 核实操作
    ...
​
    // 递归,从尾节点开始往头节点记录
    GenericInfo[] parentGenericInfo = next.findByClass(targetClass);
    // 到达尾节点后,将尾节点的目标配置泛型结构数据返回给上一个节点,从而开始递归填充泛型数据。
    if(parentGenericInfo == null){
        return targetClass.getTargetGeneric();
    }
​
    // 将需要检索的目标节点中的泛型,与当前节点的泛型实现关联。
    for (GenericInfo info : parentGenericInfo) {
        // 若当前泛型类型已关联数据类型,则记录数据类型。
        if(info.isDataType()){
            recordGeneric(info);
            continue;
        }
      
        // 否则
        // 1. 记录泛型与泛型的对照关系,如图中Node节点,两个F的坐标和值的映射
        // 2. 记录泛型和数据类型的对照关系,如图中Node节点,T对应了String类型
        // 得到泛型在下一节点绑定的位置
        int position = info.getPosition();
        // 在当前类的泛型信息
        GenericTypeEnum typeEnum = findTypeByPosition(position);
        // 当前类中定义的泛型位置
        int currentPosition = findCurrentPositionByPosition(position);
        // 记录这个位置上所绑定的泛型和位置信息,到record数组中
        recordGenericTypeAndPosition(typeEnum,currentPosition);
    }
​
    // 将记录泛型关系的数组返回到上一个节点
    return record;
}

泛型接口

有了上述泛型类的查询逻辑做铺垫,我们再来看看泛型接口是如何去搜索的?

在 Java 中,一个接口可以继承自另一个或多个接口,所以无法使用链表来展示泛型检索,需要使用树来进行检索。核心逻辑与泛型类相似,首先找到目标泛型接口节点作为回溯起点,然后依次将泛型信息记录返回到根节点。

第一步,先搜索到根节点的所有子节点上,核实是否是目标节点,或者记录了目标节点的上级节点。如下图所示:

接口-树

我们发现,并没有查到目标节点,并且当前也并不知晓 Node2 这个节点下存在我们的目标节点。因此,我们需要再往下进行检索。那么我们以那个节点优先,往下检索呢?这里我才用的是优先实现 泛型类的检索逻辑 ,从类的继承这条链表上进行优先检索,所以我把这定义为 全局深搜

第二步,我们来到了 Node2 这个节点,核实这个节点的所有子节点,我们发现还是没有找到,同理再次查找 Node4 这个节点。

接口-树1

第三步,我们来到 Node4 这个节点,核实这个节点的所有子节点,发现节点 Node10 就是我们的目标节点。

接口-树2

第四步,以 Node10 这个节点开始,开始回溯记录节点信息,这又和泛型类的检索逻辑一致。

接口-树3

第五步,如果我们的目标节点不是在 Node10 ,而是在 Node9 ,那么我们在查找完Node5后,依旧没有找到目标节点,我们就需要在回溯的过程中,核实接口的继承关系。

第六步,依次从 Node5 回到 Node4 ,发现Node10,并没有继承其他接口,所以在回到 Node2,Node2依旧没有实现其他接口,于是回到RootNode,RootNode中 依次查找 Node1和 Node3的子节点信息。于是我们在Node3 下找到了Node9,因此重新开始从 Node9 -> Node3 -> RootNode 执行泛型回溯逻辑。

接口-树4

优化算法

在上述检索过程中,最坏的情况下,我们需要把所有的节点都搜索一遍,所以时间复杂度为O(n),而且每次检索都是O(n),这个效率明显是可以进一步优化的。我的思路是,用空间换时间,将已搜索过的节点名字为键(在类或接口上,就是它的全类名),以泛型记录信息为值,记录缓存。

当下次再次检索到同一个节点时,这个节点存在泛型信息,则直接使用,不许检索其子节点。若这个节点不存在节点信息,则直接过滤。从而平均复杂度就可以降到 O(1) 。

编译时泛型检索

现在我们回到编译时,通过核心逻辑来实现下泛型检索。这里我以泛型类为例,泛型接口大家可以来看源码。

第一步:核实当前类是否被检索过,优化效率。

// 当前类的全类名
String qualifiedName = element.getQualifiedName().toString();
​
// 核实当前类是否执行过检索
Map<String, RetrievalClassModel> retrievalMap = retrieval.retrievalClassMap();
// 当前的检索记录
RetrievalClassModel currentModel = retrievalMap.get(qualifiedName);
if (currentModel != null && currentModel.isCompeted()) {
    return currentModel;
}

第二步:核实当前是类,并且父类存在,以确保功能搜索方向没有错(当前是「泛型类」检索类)。

// 当前类必需是类,并且父类必需存在,最终要继承目标类
if (isInterfaceOrNotHasParentClass(element)) {
    return null;
}

第三步:检索并且记录当前类信息

  • 加载父类检索信息,存在则直接关联到当前类上,无需再度向上检索,不存在继续执行检索。
  • 核实是否是目标类,是则与当前类关联,否则继续检索。
  • 核实是否在排除包中,是则说明搜索不到,返回null,否则继续检索。
  • 回到第一步,检索父类信息
/**
 * 检索得到当前类的泛型存储信息,需要得到当前类的泛型存储信息:
 * 1.父类必然是 TypeElement 类型元素。
 * 2.核实父类是否已加载泛型存储数据,有则根据是否泛型填充完整,做关联行为,
 * 若填充完整,则浅拷贝,共用同一个存储对象。
 * 若不完整,则深拷贝,防止更改时影响父类的存储元素。
 * 3.核实是否是目标节点类,是则遍历关联泛型信息。
 * 4.核实父类是否是在过滤包中,是则返回null,无需再次查找。
 * 5.父类信息检索不到,则向祖父类检索 回到 searchClassGenerics()方法
 * 6.从父类得到数据后,将泛型数据关联「同类-层级关联」+「继承类-坐标关联」。
 *
 * @param element 当前类的元素
 * @param types   类型工具
 * @return RetrievalClassModel 检索泛型数据信息
 */
private RetrievalClassModel retrievalCurrentClass(TypeElement element, Types types) {
    TypeMirror superTypeMirror = element.getSuperclass();
    if (superTypeMirror == null) {
        return null;
    }
​
    Element superElement = types.asElement(superTypeMirror);
    if (!(superElement instanceof TypeElement)) {
        return null;
    }
​
    // 获取父类信息
    Map<String, RetrievalClassModel> classMap = retrieval.retrievalClassMap();
    TypeElement superTypeElement = (TypeElement) superElement;
    // 父类RetrievalClassModel
    String superclassName = superTypeElement.getQualifiedName().toString();
    RetrievalClassModel superRetrievalModel = classMap.get(superclassName);
    // 当前类的RetrievalClassModel
    String qualifiedName = element.getQualifiedName().toString();
    RetrievalClassModel currentModel = classMap.get(qualifiedName);
​
    // 先核实一步,若存在,可减少后续目标节点和过滤节点的盘点耗时
    RetrievalClassModel checkLoaded = GenericsRecordUtils.checkLoaded(superRetrievalModel, currentModel, element, retrieval);
    if (checkLoaded != null && checkLoaded.isCompeted()) {
        return checkLoaded;
    }
​
    // 目标节点
    String targetClassName = retrieval.canonicalName();
    if (Objects.equals(superclassName, targetClassName)) {
        // 目标节点
        RetrievalClassModel nodeClass = GenericsRecordUtils.traverseTargetGenerics(superTypeMirror, qualifiedName, retrieval);
        if (nodeClass == null) {
            return null;
        }
        GenericsRecordUtils.appendBindPosition(nodeClass, element.getTypeParameters());
        return nodeClass;
    }
​
​
    // 根节点/需要过滤的节点
    Set<String> filterablePackages = retrieval.filterablePackages();
    for (String filterablePackage : filterablePackages) {
        if (superclassName.startsWith(filterablePackage)) {
            return null;
        }
    }
​
    // 得到父类检索信息
    RetrievalClassModel superClassModel = searchGenerics(superTypeElement, types);
    if (superClassModel == null) {
        return null;
    }
​
    // 将泛型数据关联「同类-层级关联」+「继承类-坐标关联」
    return GenericsRecordUtils.traverseNodeAndBindPosition(superTypeMirror, currentModel, superClassModel, element, retrieval);
}

第四步,关联行为,分为「同类-层级关联」和「继承类-坐标关联」。

  • 在 ParentClass 类中的 形式类 TargetClass<K,String> 和 实际类 TargetClass<T,K> 进行泛型关联。

    • 情况一:将泛型类型和泛型类型关联,TargetClass<K,String>中的K 和 TargetClass<T,K> 的T关联。
    • 情况二:将数据类型 String 与 K 绑定。
    • 由此,在 ParentClass 实际关联的 TargetClass 泛型为 T 和 K=String
  • 继承类-坐标关联:

    • 同样在 ParentClass 类中,ParentClass<K,Model> 和 TargetClass<K,String> 要进行位置上的关联。
    • 由「泛型关联」可知 TargetClass<K,String> 中的K,实际上是「目标类」中的T,
    • 所以对等,要将 ParentClass中的K 等效为T,从而记录格式并且向子类传递。
/**
 * 「同类-层级关联」+「继承类-坐标关联」
 *
 * @param superTypeMirror 父类类型
 * @param currentModel    当前类的泛型数据存储
 * @param superClassModel 父类的泛型数据存储
 * @param element         当前类的类型元素
 * @return 父类检索信息绑定到当前类
 */
public static RetrievalClassModel traverseNodeAndBindPosition(TypeMirror superTypeMirror,
                                                              RetrievalClassModel currentModel,
                                                              RetrievalClassModel superClassModel,
                                                              TypeElement element,
                                                              IRetrieval retrieval) {
​
    // 采用深拷贝
    IGenericsRecord record = cloneBySuperRecord(retrieval,
            superClassModel.getRecord());
    currentModel.bindGenericsRecord(record);
​
    traverseNodeGenerics(superTypeMirror, currentModel, superClassModel);
    appendBindPosition(currentModel, element.getTypeParameters());
    return currentModel;
}
​
/**
 * 遍历目标泛型集合
 * 将当前制定的实体类型 填充到
 * 目标类的泛型上
 * 流程
 * 1. 获取当前类的父类形式泛型
 * 2. 获取真实父类的泛型类型
 * 3. 匹配形式泛型kind == TypeKind.DECLARED,代表可以填充
 * 4. 不匹配的记录到对照表中
 */
public static RetrievalClassModel traverseTargetGenerics(TypeMirror superTypeMirror,
                                                         String currentQualifiedName,
                                                         IRetrieval retrieval) {
    if (!(superTypeMirror instanceof DeclaredType)) {
        return null;
    }
​
    DeclaredType declaredType = (DeclaredType) superTypeMirror;
​
    TypeElement superElement = (TypeElement) declaredType.asElement();
    List<? extends TypeParameterElement> superParameters = superElement.getTypeParameters();
    List<? extends TypeMirror> typeArguments = declaredType.getTypeArguments();
    // 未设置泛型,或设置的泛型个数不一致
    if (typeArguments.isEmpty() || superParameters.size() != typeArguments.size()) {
        return null;
    }
​
    // 「同类-层级关联」
    Map<String, RetrievalClassModel> classMap = retrieval == null ? new HashMap<>() : retrieval.retrievalClassMap();
    RetrievalClassModel classModel = classMap.get(currentQualifiedName);
    for (int index = 0; index < superParameters.size(); index++) {
        TypeParameterElement element = superParameters.get(index);
        String typeName = element.asType().toString();
​
        TypeMirror mirror = typeArguments.get(index);
        if (mirror.getKind() == TypeKind.DECLARED) {
            classModel.addTargetGenericsRecord(typeName, mirror);
        } else {
            classModel.recordType(mirror.toString(), RetrievalClassModel.PREFIX + typeName);
        }
    }
    return classModel;
}
​
/**
 * 追加绑定泛型类型的位置,「继承类-坐标关联」
 * AClass<T> extends BClass<T>{
 * <p>
 * }
 * 绑定T的位置为0
 *
 * @param nodeClass         节点Class信息
 * @param currentParameters 当前类的泛型参数 AClass<T> 中的T
 */
public static void appendBindPosition(RetrievalClassModel nodeClass, List<? extends TypeParameterElement> currentParameters) {
    int index = 0;
    for (TypeParameterElement parameter : currentParameters) {
        nodeClass.bindPosition(parameter.asType().toString(), index);
        index++;
    }
}

完整代码:github.com/Sheedon/Com…

结束语

在实际开发中,需要进行泛型检索的情况比较少,但当我们在构思一种解决方案并且没有提供API供我们使用时,将结构简化为数据结构和基础算法是一个解决问题的有效方法。本文旨在向读者介绍如何通过将复杂结构转化为数据结构和基础算法来解决问题,并通过泛型检索的例子进行说明。当然,这只是为各位提供了一个思路,如果读者有更好的解决方案,也欢迎分享给我们。