关于反射:如果只有一个完全限定的名称,如何获取java类的二进制名称?

How to get the binary name of a java class, if one has only the fully qualified name?

反射类和方法以及类加载器等需要使用所谓的类的"二进制"名称。

问题是,如果一个人只有完全限定的名称,即在源代码中使用的名称,那么如何获得二进制名称。

例如:

1
2
3
4
5
package frege;
public static class RT {
    ....
    public static class X { .... }
}

类的完全限定名为frege.RT.X。然而,要获得类对象,需要编写:

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

而不是

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

因为X恰好是frege.RT的一个内类。

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

有没有更好/知名/标准的解决方案?我查看了ClassCLassLoaderjava.lang.reflect的API文档,但没有发现任何可用的文档。


现在听起来您想要从规范名称中获取完全限定名(FQN)。因为这与简单的名字不同,我将添加第二个答案。

如果出现规范名称冲突,sun javac命令将不会编译类。但是,通过单独编译,仍然可以得到两个具有相同规范名称的不同类。

一个例子:

文件src1comstack est.java

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
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());
    }
}

文件src2comstack estexamplecowhoof.java

1
2
3
package com.stack.Test.Example.Cow;

public class Hoof { }

然后编译和执行:

1
2
3
4
5
6
7
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

产生输出:

1
2
3
4
5
6
7
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,则抛出异常。

第三,接受唯一的映射是使用FQN的,然后只在指定的类加载器内,并适当地重新使用应用程序。我发现使用线程上下文类加载器作为默认类加载器很方便。


一个简单的名称省略了很多信息,并且有可能有许多具有相同简单名称的类。这可能使这成为不可能。例如:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
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());
        }
    }
}

这将产生输出:

1
2
3
4
5
6
7
8
9
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

如您所见,这三个本地类都有相同的简单名称。


我认为规范名称指定唯一类是一个安全的赌注。如上所述,javac不允许您在一个编译单元中创建两个具有相同规范名称的类。如果您有两个编译,那么您可能会在加载哪个类时遇到麻烦,但是在这一点上,我更担心库的包名与您的包名相冲突,这是除了恶意程序之外所有人都可以避免的。

基于这个原因,我认为假设你不会遇到这种情况是一个安全的赌注。沿着这些思路,对于那些感兴趣的人,我执行了OP的建议(将$s翻转到.s),并且在没有找到任何具有该规范名称的类,或者找到两个或更多具有该名称的类时,简单地抛出ClassNotFoundException

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
   /**
 * 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
 * discussed here.
 */

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在抛出的任何异常上自动中断,您将知道这种行为是正常的。