Как получить двоичное имя класса java, если у вас есть только полное имя?

Классы и методы отражения, а также классные загрузчики и т.д. нуждаются в так называемых "двоичных" именах классов для работы.

Вопрос в том, как получить двоичное имя, если у вас есть только полное имя, то есть имя, которое будет использоваться в исходном коде.

Например:

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, или больше нет . для замены.

Есть ли лучшее/хорошо известное/стандартное решение? Я просмотрел документы API для Class, CLassLoader и java.lang.reflect, но не нашел ничего полезного.

Ответ 1

Теперь кажется, что вы хотите получить полное имя (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, то только в пределах указанного загрузчика классов, и переработайте приложение соответствующим образом. Я считаю удобным использовать загрузчик классов контекста потока как загрузчик классов по умолчанию.

Ответ 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

Как вы можете видеть, все три локальных класса имеют одно и то же простое имя.

Ответ 3

Я думаю, что это безопасная ставка, что канонические имена определяют уникальные классы. Как уже упоминалось выше, javac не позволит вам создать два класса с тем же самым каноническим именем из одной единицы компиляции. Если у вас есть 2 компиляции, вы можете столкнуться с проблемами, связанными с тем, какой класс вы загружаете, но в этот момент меня больше беспокоит имя пакета библиотеки, сталкивающееся с вашими именами пакетов, что позволяет избежать всех, кроме вредоносных.

По этой причине, я думаю, что это безопасная ставка, чтобы предположить, что вы не столкнетесь с этим сценарием. Вдоль этих строк для тех, кто заинтересован, я внедрил предложение OP (перевернув $ в . s) и просто выбросив 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="#" onclick="location.href='http://stackoverflow.com/info/13331902/how-to-get-the-binary-name-of-a-java-class-if-one-has-only-the-fully-qualified'; return false;">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, но если вы когда-либо говорили затмение или intelliJ, чтобы автоматически ломать все брошенные исключения, вы будете знать такое поведение является номиналом для курса.