如何在Java中查找给定类的所有子类?

238

如何在Java中查找给定类的所有子类(或给定接口的所有实现者)?

目前,我有一种方法来完成这个任务,但我发现它相当低效(至少可以这么说)。

该方法是:

  1. 获取类路径上存在的所有类名的列表
  2. 加载每个类并测试以查看它是否是所需类或接口的子类或实现者

在Eclipse中,有一个很好的功能称为Type Hierarchy可以高效地显示此信息。

如何以编程方式完成这项工作?


2
虽然基于Reflections和Spring的解决方案看起来很有趣,但我需要一些简单的解决方案,它没有依赖关系。看来我的原始代码(稍加调整)是正确的选择。 - Avrom
1
你肯定可以递归地使用getSuperClass方法吧? - user723720
我特别想找到给定类的所有子类。getSuperClass不能告诉你一个类有哪些子类,只能获取特定子类的直接超类。此外,Class上的isAssignableFrom方法更适合您建议的内容(无需递归)。 - Avrom
2
这个问题与许多其他重复问题有关联,但它并不包含任何有用的、纯Java的答案。唉... - Eric Duminil
@EricDuminil 请看Lyfing的回答。使用纯Java,受限搜索。正是我想要的。 - Art Swri
19个回答

150

使用纯Java进行类扫描并不容易。

Spring框架提供了一个名为ClassPathScanningCandidateComponentProvider的类可以实现您所需的功能。以下示例将在org.example.package包中查找MyClass的所有子类。

ClassPathScanningCandidateComponentProvider provider = new ClassPathScanningCandidateComponentProvider(false);
provider.addIncludeFilter(new AssignableTypeFilter(MyClass.class));

// scan in org.example.package
Set<BeanDefinition> components = provider.findCandidateComponents("org/example/package");
for (BeanDefinition component : components)
{
    Class cls = Class.forName(component.getBeanClassName());
    // use class cls found
}

这种方法的额外好处是使用字节码分析器来查找候选者,这意味着它不会加载它扫描到的所有类。


26
创建ClassPathScanningCandidateComponentProvider时,应将False作为参数传递以禁用默认过滤器。默认过滤器将匹配其他类型的类,例如任何带有@Component注释的类。我们只希望在此处启用AssignableTypeFilter。 - MCDS
1
你说这不容易,但如果我们想要用纯Java来实现它,我们该怎么做呢? - Aequitas

80

除了您描述的方法,没有其他方法可以实现它。想一想 - 没有扫描类路径上的每个类,如何知道哪些类扩展ClassX?

Eclipse只能在按下“在类型层次结构中显示”按钮时告诉您关于超类和子类的信息,因为它已经在该点加载了所有类型数据(因为它不断编译您的类,知道类路径上的所有内容等),从而表现出似乎是“高效”的时间。


32
现在有一个名为org.reflections的简单库,它可以帮助完成此任务和其他常见的反射任务。使用这个库,您只需调用 reflections.getSubTypesOf(aClazz)) 即可。 链接 - Enwired
@matt b - 如果它必须扫描所有类,这是否意味着当您的项目中有很多类时会出现性能下降,即使只有少数类是子类化您的类? - LeTex
1
没错。它只涉及所有类。你可以定义自己的扫描器,尽管你可以加速它,比如排除某些包,你知道你的类不会被扩展,或者只是打开类文件并检查在类的常量部分中查找类名,避免让反射扫描器读取更多与所需引用到你(直接)超类无关的类信息。间接地,你需要进一步扫描。所以这是目前最好的。 - Martin Kersten
fforw的答案对我有用,应该标记为正确答案。显然,这可以通过类路径扫描实现。 - Farrukh Najmi
1
你错了,关于Burningwave库的回复请见下文。 - user2100029
无论是否有库来完成它,他们都是正确的。你认为这个库是做什么/使用什么? - Matthew Read

54

仅使用内置的Java Reflections API 是不可能实现这个功能的。

存在一个项目可以进行必要的扫描和索引类路径,以便您可以访问此信息...

Reflections

一种基于运行时元数据分析的Java库,类似于Scannotations

Reflections扫描您的类路径,索引元数据,并允许您在运行时查询它,并可能为项目中的许多模块保存和收集该信息。

使用Reflections,您可以查询以下元数据:

  • 获取某个类型的所有子类型
  • 获取带有某些注释的所有类型
  • 获取带有某些注释的所有类型,包括匹配注释参数
  • 获取某些方法的所有注释

(声明:我没有使用过它,但该项目的描述似乎完全符合您的需求。)


2
有趣。该项目似乎有一些依赖项,但它们的文档似乎没有提到。目前发现的依赖项包括:javaassist、log4J和XStream。 - Avrom
3
我使用Maven引入了这个项目,它运行得很好。获取子类实际上是第一个源代码示例,只有两行代码 :-) - KarlsFriend
1
仅使用内置的Java Reflections API是否不可能,或者只是非常不方便? - Flow
1
当您使用Reflections并部署应用程序WAR到GlassFish时,请小心! Guava库存在冲突,部署将失败,并显示错误CDI deployment failure:WELD-001408 - 请参见GLASSFISH-20579以获取更多详细信息。在这种情况下,FastClasspathScanner是一个解决方案。 - lu_ko
1
org.reflections并没有得到很好的维护(没有定期发布),存在许多未解决的问题和拉取请求。最好使用ClassGraph或Springs ClassPathScanningCandidateComponentProvider。 - xtermi2
显示剩余3条评论

18

尝试使用ClassGraph(免责声明,我是作者)。ClassGraph支持在运行时或构建时扫描给定类的子类,但还支持更多功能。ClassGraph可以在内存中构建整个类图的抽象表示(所有类、注释、方法、方法参数和字段),适用于类路径上的所有类或选定包中的类,并且您可以随意查询此类图。ClassGraph支持比其他任何扫描器更多的类路径规范机制和类加载器,并且与新的JPMS模块系统无缝配合,因此如果您的代码基于ClassGraph,则您的代码将具有最大的可移植性。在此处查看API。


12
不要忘记,生成的类的Javadoc将包括已知的子类列表(对于接口,则包括已知的实现类)。

3
这是完全不正确的,超类不应该依赖于它们的子类,即使是在Javadoc或注释中也不行。 - hunter
1
@hunter 我不同意。JavaDoc 包含已知子类的列表是完全正确的。当然,“已知”可能不包括您正在寻找的类,但对于某些用例来说,它已经足够了。 - Qw3ry
无论如何,您可能会错过一些类:我可以在类路径中加载新的jar(在运行时),之前发生的每个检测都将失败。 - Qw3ry

9

我知道我晚了几年才参加这个聚会,但我碰到了同样的问题并试图解决它。如果您正在编写Eclipse插件(因此可以利用其缓存等功能),则可以在程序上使用Eclipse的内部搜索来查找实现接口的类。以下是我的(非常粗糙的)第一次尝试:

  protected void listImplementingClasses( String iface ) throws CoreException
  {
    final IJavaProject project = <get your project here>;
    try
    {
      final IType ifaceType = project.findType( iface );
      final SearchPattern ifacePattern = SearchPattern.createPattern( ifaceType, IJavaSearchConstants.IMPLEMENTORS );
      final IJavaSearchScope scope = SearchEngine.createWorkspaceScope();
      final SearchEngine searchEngine = new SearchEngine();
      final LinkedList<SearchMatch> results = new LinkedList<SearchMatch>();
      searchEngine.search( ifacePattern, 
      new SearchParticipant[]{ SearchEngine.getDefaultSearchParticipant() }, scope, new SearchRequestor() {

        @Override
        public void acceptSearchMatch( SearchMatch match ) throws CoreException
        {
          results.add( match );
        }

      }, new IProgressMonitor() {

        @Override
        public void beginTask( String name, int totalWork )
        {
        }

        @Override
        public void done()
        {
          System.out.println( results );
        }

        @Override
        public void internalWorked( double work )
        {
        }

        @Override
        public boolean isCanceled()
        {
          return false;
        }

        @Override
        public void setCanceled( boolean value )
        {
        }

        @Override
        public void setTaskName( String name )
        {
        }

        @Override
        public void subTask( String name )
        {
        }

        @Override
        public void worked( int work )
        {
        }

      });

    } catch( JavaModelException e )
    {
      e.printStackTrace();
    }
  }

到目前为止,我看到的第一个问题是我只捕获直接实现接口的类,而不是它们所有的子类 - 但一点递归从未伤害过任何人。

3
原文:OR, it turns out you don't have to make your own search. You can just get an ITypeHierarchy directly from your IType by calling .newTypeHierarchy() on it: http://dev.eclipse.org/newslists/news.eclipse.tools.jdt/msg05036.html翻译:或者说,你不必自己进行搜索。只需从IType调用.newTypeHierarchy()即可直接获得ITypeHierarchy:http://dev.eclipse.org/newslists/news.eclipse.tools.jdt/msg05036.html - Curtis
我来晚了... OP仅仅提到Eclipse可以做到他想要的,而他的“编程方式”的陈述排除了Eclipse作为解决方案的一部分。 - user3481644

9
我在几年前做过这个。最可靠的方法(即使用官方Java API而没有外部依赖项)是编写自定义doclet来生成可以在运行时读取的列表。
您可以像这样从命令行运行它:
javadoc -d build -doclet com.example.ObjectListDoclet -sourcepath java/src -subpackages com.example

或者像这样从Ant运行它:

<javadoc sourcepath="${src}" packagenames="*" >
  <doclet name="com.example.ObjectListDoclet" path="${build}"/>
</javadoc>

以下是基本代码:

public final class ObjectListDoclet {
    public static final String TOP_CLASS_NAME =  "com.example.MyClass";        

    /** Doclet entry point. */
    public static boolean start(RootDoc root) throws Exception {
        try {
            ClassDoc topClassDoc = root.classNamed(TOP_CLASS_NAME);
            for (ClassDoc classDoc : root.classes()) {
                if (classDoc.subclassOf(topClassDoc)) {
                    System.out.println(classDoc);
                }
            }
            return true;
        }
        catch (Exception ex) {
            ex.printStackTrace();
            return false;
        }
    }
}

为了简化,我删除了命令行参数解析,并且将内容写入 System.out 而不是文件。


1
虽然在编程方面使用这可能会棘手,但我必须说——这是一种 聪明 的方法! - Janaka Bandara

8

请注意其他答案中提到的限制,您还可以使用 openpojo 的 PojoClassFactory可在 Maven 上获得),方法如下:

for(PojoClass pojoClass : PojoClassFactory.enumerateClassesByExtendingType(packageRoot, Superclass.class, null)) {
    System.out.println(pojoClass.getClazz());
}

其中packageRoot是您希望搜索的包的根字符串(例如"com.mycompany"或仅"com"),而Superclass是您的超类型(这也适用于接口)。


迄今为止,从建议的解决方案中选择这个是最快和最优雅的。 - KidCrippler
非常感谢。非常感激。这是一个简单易用的好解决方案。我用它来获取代码中的所有可序列化类,并通过生成一个测试和一些插入代码来修复缺失的serialVersionUID。能够做到这一点真是太棒了!https://gist.github.com/arberg/d867185288b0f36aa908f45d6225c40c - undefined

5
根据您的特定需求,在某些情况下,Java的服务加载器机制可以实现您想要的功能。
简而言之,它允许开发人员通过将其列在JAR/WAR文件的META-INF/services目录中的文件中来显式地声明一个类是某个其他类的子类(或实现某个接口)。然后可以使用java.util.ServiceLoader类进行发现,该类在给定Class对象时,将生成该类的所有已声明子类的实例(或者如果Class表示一个接口,则表示实现该接口的所有类)。
这种方法的主要优点是无需手动扫描整个类路径以查找子类 - 所有发现逻辑都包含在ServiceLoader类中,并且它仅加载在META-INF/services目录下明确声明的类(而不是类路径上的每个类)。
但是,它仍然存在一些缺点:
- 它不能找到所有子类,只能找到明确声明的子类。因此,如果您需要真正找到所有的子类,这种方法可能不够。 - 它要求开发人员在META-INF/services目录下明确声明类。这对开发人员来说是额外的负担,并且容易出错。 - ServiceLoader.iterator()生成子类实例,而不是它们的Class对象。这会导致两个问题:
- 您无法控制子类的构造方式 - 使用无参构造函数创建实例。 - 因此,子类必须有一个默认构造函数,或者必须显式声明无参构造函数。
Java 9明显将解决其中一些缺点(特别是有关子类实例化的缺点)。
示例
假设您要查找实现接口com.example.Example的类:
package com.example;

public interface Example {
    public String getStr();
}

com.example.ExampleImpl实现了该接口:

package com.example;

public class ExampleImpl implements Example {
    public String getStr() {
        return "ExampleImpl's string.";
    }
}

您可以通过创建文件 META-INF/services/com.example.Example 并将文本 com.example.ExampleImpl 放置其中来声明类 ExampleImplExample 的实现。
然后,您可以通过以下方式获取每个 Example 的实现实例(包括 ExampleImpl 的实例):
ServiceLoader<Example> loader = ServiceLoader.load(Example.class)
for (Example example : loader) {
    System.out.println(example.getStr());
}

// Prints "ExampleImpl's string.", plus whatever is returned
// by other declared implementations of com.example.Example.

3
需要翻译的内容如下:

值得注意的是,这只能找到当前类路径上存在的所有子类。假设目前您正在查看的内容可以接受,而且很可能您已经考虑过这一点,但是如果您曾经发布了一个非final类到公共场合(不同程度的“公共场合”),那么其他人编写的您不知道的子类也完全有可能存在。

因此,如果您想要查看所有子类,因为您想要进行更改并查看它如何影响子类的行为,请记住您无法看到的子类。理想情况下,所有非私有方法和类本身都应该有良好的文档记录;根据该文档进行更改,而不改变方法/非私有字段的语义,您的更改应该是向后兼容的,至少对于遵循您定义的超类的子类来说是这样的。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接