如何通过完整的类名获取Java类的二进制名称?

15

反射类和方法以及类加载器等需要使用类的所谓“二进制”名称才能正常工作。

问题是,如果只有完全限定名称(即在源代码中使用的名称),如何获取二进制名称。

例如:

package frege;
public static class RT {
    ....
    public static class X { .... }
}

该类的完全限定名称将是frege.RT.X。然而,要获取该类对象,需要编写:

Class.forName("frege.RT$X")

而不是

Class.forName("frege.RT.X")    // fails with ClassNotFoundException

因为 X 恰好是 frege.RT 的内部类,所以出现此问题。

一种可能的但笨拙的解决方案是从后往前逐个用 $ 替换 .,直到 Class.forName() 不再抛出 ClassNotFoundException 或没有更多的 . 可以替换。

是否有更好/更知名/标准的解决方案?我在 ClassCLassLoaderjava.lang.reflect 的 API 文档中查找,但没有找到可用的内容。


2
我不确定forName方法是否需要所谓的“二进制名称”。文档说它使用完全限定名称。 - Edwin Dalorzo
@EdwinDalorzo 你可以相信我。它无法找到简单名称的内部类。(至少在JDK 1.7.0中不行) - Ingo
很有趣。我现在不在电脑旁,但我会尽快尝试。这很奇怪,因为JLS中对完全限定名的定义不包括“二进制名称”的概念。内部类只是像其他类一样用点(.)表示。而Javadocs说该方法应该接收一个fqn。所以这个案例很有趣。 - Edwin Dalorzo
@EdwinDalorzo 您是正确的,API文档在这里显然有问题。它实际上说:“给定类或接口的完全限定名称(与getName返回的格式相同)”,并且在那里说:“如果此类对象表示不是数组类型的引用类型,则返回类的二进制名称,如Java™语言规范所指定。” - Ingo
好的,看起来Class.getName()返回的是二进制名称。这是一个非常有趣的问题。我还没有找到将您的规范名称转换为二进制名称的方法。我会收藏这个问题,因为我希望最终有人能够提供答案。 - Edwin Dalorzo
3个回答

13

现在看起来您想从规范名称中获取完全限定名称(FQN)。由于这与使用简单名称不同,我将添加第二个答案。

Sun javac命令将不会编译类,如果规范名称冲突会导致结果。然而,通过分别编译,您仍然可以获得两个具有相同规范名称的不同类。

一个例子:

文件src1 \ com \ stack \ Test.java

package com.stack;

public class Test {
    public static class Example {
        public static class Cow {
            public static class Hoof {
            }
        }
    }

    public static void main(String[] args) throws Exception {
        Class<?> cl1 = Class.forName("com.stack.Test$Example$Cow$Hoof");
        Class<?> cl2 = Class.forName("com.stack.Test.Example.Cow.Hoof");
        System.out.println(cl1.getName());
        System.out.println(cl1.getSimpleName());
        System.out.println(cl1.getCanonicalName());
        System.out.println();
        System.out.println(cl2.getName());
        System.out.println(cl2.getSimpleName());
        System.out.println(cl2.getCanonicalName());
    }
}

文件 src2\com\stack\Test\Example\Cow\Hoof.java

package com.stack.Test.Example.Cow;

public class Hoof { }

然后进行编译和执行:

set CLASSPATH=
mkdir bin1 bin2
javac -d bin1 -sourcepath src1 src1\com\stack\Test.java
javac -d bin2 -sourcepath src2 src2\com\stack\Test\Example\Cow\Hoof.java

set CLASSPATH=bin1;bin2
java com.stack.Test

产生输出:

com.stack.Test$Example$Cow$Hoof
Hoof
com.stack.Test.Example.Cow.Hoof

com.stack.Test.Example.Cow.Hoof
Hoof
com.stack.Test.Example.Cow.Hoof

因此,两个类具有相同的规范名称但不同的FQN。即使两个类具有相同的FQN和相同的规范名称,如果它们是通过不同的类加载器加载的,则仍然可以不同。

要解决您的问题,我看到有几种可行的方法。

首先,您可以指定匹配嵌套最少且在FQN中使用最少'$'字符的类。 更新 结果发现Sun javac正好与此相反,匹配嵌套最多的类。

其次,您可以测试所有可能的FQN,如果存在多个,则抛出异常。

第三,接受唯一的映射只在特定的类加载器内使用FQN,然后重新设计您的应用程序。我发现将线程上下文类加载器作为默认类加载器很方便。


感谢深入的讨论。但是,如果您将第三个程序与其中的 new com.stack.Test.Example.Cow.Hoof() 一起提供给 javac(并且两者都在您的类路径中),会发生什么呢?javac会选择哪一个? - Ingo
无论哪一个首先出现在类路径中,我的Sun javac始终使用com.stack.Test$Example$Cow$Hoof - Simon G.
我觉得值得注意的是,JavaFX的fxml类加载系统相当简单,因为它强制实施包名必须小写的约定。如果您将在fxml中引用的类放在大写字母的包中,则FXML加载器将无法找到它。 - Groostav

2
一个简单的名称省略了很多信息,可能有很多类具有相同的简单名称,这可能会导致无法实现。例如:
package stack;

/**
 * 
 * @author Simon Greatrix
 */
public class TestLocal {

    public Object getObject1() {
        class Thing {
            public String toString() { 
                return "I am a Thing";
            }
        }
        return new Thing();
    }

    public Object getObject2() {
        class Thing {
            public String toString() { 
                return "I am another Thing";
            }
        }
        return new Thing();
    }

    public Object getObject3() {
        class Thing {
            public String toString() { 
                return "I am a rather different Thing";
            }
        }
        return new Thing();
    }

    /**
     * @param args
     */
    public static void main(String[] args) {
        TestLocal test = new TestLocal();
        Object[] objects = new Object[] {
                test.getObject1(),                
                test.getObject2(),                
                test.getObject3()                
        };

        for(Object o : objects) {
            System.out.println("Object      : "+o);
            System.out.println("Simple Name : "+o.getClass().getSimpleName());
            System.out.println("Name        : "+o.getClass().getName());
        }
    }
}

这将产生以下输出:
Object      : I am a Thing
Simple Name : Thing
Name        : stack.TestLocal$1Thing
Object      : I am another Thing
Simple Name : Thing
Name        : stack.TestLocal$2Thing
Object      : I am a rather different Thing
Simple Name : Thing
Name        : stack.TestLocal$3Thing

正如您所看到的,所有三个本地类都具有相同的简单名称。


好的。但这不是我的问题。(我更新了问题以避免关于“简单”和“完全限定”名称的混淆。)实际上,我只谈论嵌套在其他类中的公共类,因此可以用像x.y.Z这样的名称在Java程序中命名的类。或者,换句话说:当一个人写foo.bar.Baz.X时,Java编译器如何知道这是一个嵌套类还是不是? - Ingo
1
更不用说,你可能有一个顶级类MyStuff包含一个Example内部类,以及一个名为MyStuff的包,其中包含一个顶级类Example,而仅使用简单名称MyStuff.Example无法区分这两者。 - cHao
@cHao 你是正确的,但这也是一个边界情况,其中类可以位于“未命名”包中。我们可以假设我只对位于命名包中的类感兴趣。 - Ingo
原来这种情况无法编译。当包和类具有相同的名称时,编译器会报告错误。 - Simon G.

1

我认为可以肯定地说,规范名称指定了唯一的类。如上所述,javac不允许在单个编译单元中创建具有相同规范名称的两个类。如果您有两个编译,则可能会遇到加载哪个类的问题,但此时,我更担心库的包名称与您的包名称冲突,这是通过除恶意外都要避免的。

因此,我认为可以放心地假设您不会遇到这种情况。沿着这些线路,对于那些感兴趣的人,我实现了OP的建议(将$翻转为.),并在找不到具有该规范名称的任何类或找到两个或多个具有该名称的类时,简单地抛出ClassNotFoundException

   /**
 * Returns the single class at the specified canonical name, or throws a {@link java.lang.ClassNotFoundException}.
 *
 * <p>Read about the issues of fully-qualified class paths vs the canonical name string
 * <a href="https://dev59.com/lGYr5IYBdhLWcg3w4t3m">discussed here</a>.
 */
public static <TStaticallyNeeded> Class<TStaticallyNeeded> classForCanonicalName(String canonicalName)
        throws ClassNotFoundException {

    if (canonicalName == null) { throw new IllegalArgumentException("canonicalName"); }

    int lastDotIndex = canonicalName.length();
    boolean hasMoreDots = true;

    String attemptedClassName = canonicalName;

    Set<Class> resolvedClasses = new HashSet<>();

    while (hasMoreDots) try {
        Class resolvedClass = Class.forName(attemptedClassName);
        resolvedClasses.add(resolvedClass);
    }
    catch (ClassNotFoundException e) {
        continue;
    }
    finally {
        if(hasMoreDots){
            lastDotIndex = attemptedClassName.lastIndexOf('.');
            attemptedClassName = new StringBuilder(attemptedClassName)
                    .replace(lastDotIndex, lastDotIndex + 1, "$")
                    .toString();
            hasMoreDots = attemptedClassName.contains(".");
        }
    }

    if (resolvedClasses.isEmpty()) {
        throw new ClassNotFoundException(canonicalName);
    }

    if (resolvedClasses.size() >= 2) {
        StringBuilder builder = new StringBuilder();
        for (Class clazz : resolvedClasses) {
            builder.append("'").append(clazz.getName()).append("'");
            builder.append(" in ");
            builder.append("'").append(
                    clazz.getProtectionDomain().getCodeSource() != null
                            ? clazz.getProtectionDomain().getCodeSource().getLocation()
                            : "<unknown code source>"
            ).append("'");
            builder.append(System.lineSeparator());
        }

        builder.replace(builder.length() - System.lineSeparator().length(), builder.length(), "");

        throw new ClassNotFoundException(
                "found multiple classes with the same canonical names:" + System.lineSeparator() +
                        builder.toString()
        );
    }

    return resolvedClasses.iterator().next();
}

我仍然非常不满意“预期”的流程是触发那个catch(NoClass) continue代码,但如果你曾经告诉过Eclipse或IntelliJ在抛出任何异常时自动中断,你就会知道这种行为是家常便饭。


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