Могут ли файлы классов Java использовать зарезервированные ключевые слова в качестве имен?

Я знаю, что Java-компилируемый-язык-программирования — это не одно и то же, что Java-формат байт-кода для выполнения JVM. Существуют примеры того, что допустимо в формате .class, но не допустимо в исходном коде .java, например классы без конструктора и синтетические методы.

  1. Если мы вручную создадим файл .class с зарезервированным ключевым словом языка Java (например, int, while) в качестве имени класса, метода или поля, примет ли его виртуальная машина Java для загрузки?

  2. Если класс загружен, означает ли это, что единственный способ получить доступ к этому классу или члену — через отражение Java, потому что имя синтаксически недопустимо в языке программирования Java?


person Nayuki    schedule 27.05.2015    source источник
comment
Для # 2 да, поскольку вы не могли использовать ключевое слово Java незаконным образом. Я не понимаю, почему № 1 был бы невозможен, поскольку ключевые слова являются только конструкцией языка.   -  person Dave Newton    schedule 27.05.2015
comment
Дополнительное примечание (также упомянутое в одном ответе): некоторые обфускаторы используют тот факт, что это возможно. Они заменяют имена методов/классов/переменных в файлах .class ключевыми словами, такими как do, while и т. д., чтобы затруднить декомпиляцию кода. (Его все еще можно декомпилировать, но полученный код в основном бесполезен)   -  person Marco13    schedule 28.05.2015
comment
@Marco13: но это всего лишь мера предотвращения наивных атак. Это легко исправить, снова переименовав идентификаторы, на этот раз создав легальные имена перед декомпиляцией. По иронии судьбы, для этого могут быть использованы сами обфускаторы, поскольку они обеспечивают операцию переименования, которая обычно настраивается, поэтому им можно приказать генерировать допустимые имена.   -  person Holger    schedule 28.05.2015


Ответы (4)


Единственные ограничения на имена классов на уровне байт-кода заключаются в том, что они не могут содержать символы [, . или ; и их длина не может превышать 65535 байт. Помимо прочего, это означает, что вы можете свободно использовать зарезервированные слова, пробелы, специальные символы, Unicode или даже странные вещи, такие как символы новой строки.

Теоретически вы даже можете использовать нулевые символы в имени класса, но поскольку нулевой символ в имени файла быть не может, вы не можете включить такой файл класса в банку. Однако вы можете создать и загрузить его динамически.

Вот пример того, что вы можете сделать (написано на ассемблере Krakatau):

; Entry point for the jar
.class Main
.super java/lang/Object

.method public static main : ([Ljava/lang/String;)V
    .limit stack 10
    .limit locals 10
    invokestatic int                                hello ()V
    invokestatic "-42"                              hello ()V
    invokestatic ""                                 hello ()V
    invokestatic "  some  whitespace and \t tabs"   hello ()V
    invokestatic "new\nline"                        hello ()V
    invokestatic 'name with "Quotes" in it'         hello ()V
    return
.end method
.end class


.class int
.super java/lang/Object
.method public static hello : ()V
    .limit stack 2
    .limit locals 0
    getstatic java/lang/System out Ljava/io/PrintStream;
    ldc "Hello from int"
    invokevirtual java/io/PrintStream println (Ljava/lang/Object;)V
    return
.end method
.end class

.class "-42"
.super java/lang/Object
.method public static hello : ()V
    .limit stack 2
    .limit locals 0
    getstatic java/lang/System out Ljava/io/PrintStream;
    ldc "Hello from -42"
    invokevirtual java/io/PrintStream println (Ljava/lang/Object;)V
    return
.end method
.end class

; Even the empty string can be a class name!
.class ""
.super java/lang/Object
.method public static hello : ()V
    .limit stack 2
    .limit locals 0
    getstatic java/lang/System out Ljava/io/PrintStream;
    ldc "Hello from "
    invokevirtual java/io/PrintStream println (Ljava/lang/Object;)V
    return
.end method
.end class

.class "  some  whitespace and \t tabs"
.super java/lang/Object
.method public static hello : ()V
    .limit stack 2
    .limit locals 0
    getstatic java/lang/System out Ljava/io/PrintStream;
    ldc "Hello from   some  whitespace and \t tabs"
    invokevirtual java/io/PrintStream println (Ljava/lang/Object;)V
    return
.end method
.end class

.class "new\nline"
.super java/lang/Object
.method public static hello : ()V
    .limit stack 2
    .limit locals 0
    getstatic java/lang/System out Ljava/io/PrintStream;
    ldc "Hello from new\nline"
    invokevirtual java/io/PrintStream println (Ljava/lang/Object;)V
    return
.end method
.end class

.class 'name with "Quotes" in it'
.super java/lang/Object
.method public static hello : ()V
    .limit stack 2
    .limit locals 0
    getstatic java/lang/System out Ljava/io/PrintStream;
    ldc "Hello from name with \"Quotes\" in it"
    invokevirtual java/io/PrintStream println (Ljava/lang/Object;)V
    return
.end method
.end class

Вывод выполнения:

Hello from int
Hello from -42
Hello from
Hello from   some  whitespace and        tabs
Hello from new
line
Hello from name with "Quotes" in it

См. ответ Хольгера для точной цитаты правил из спецификации JVM.

person Antimony    schedule 28.05.2015
comment
Действительно интересно! Какой синтаксис вы используете для своего кода? Это что-то, что можно скомпилировать в файлы .class с помощью существующего инструмента? - person Nayuki; 28.05.2015
comment
Помимо того факта, что спецификация JVM говорит об этом, почему существуют какие-либо ограничения на символы в имени идентификатора? И почему эти 3 символа? Имена не играют никакой роли в интерпретации или компиляции кода AFAIK. [Кто сказал, что в имени файла не может быть нуля? Я не думал, что Unix или NTFS заботятся о том, чтобы вы погрузились достаточно глубоко. Конечно, трудно печатать с клавиатуры.] - person Ira Baxter; 28.05.2015
comment
@ Ира, ты обнаружишь, что Unix заботится о нуле. - person Paul Draper; 28.05.2015
comment
@PaulDraper: А. Загрязнение струны C. OK. - person Ira Baxter; 28.05.2015
comment
@Ira Baxter: спецификация JVM не заботится о файловых системах. Всегда предполагается, что могут быть альтернативные формы хранения или схемы искажения имен для решения проблемы неподдерживаемых символов. Имейте в виду, что когда была изобретена Java, поддержка Unicode в файловых системах не была стандартной. Тем не менее, поддержка Unicode в именах Java была с самого начала. Я сделал ответ, объясняя, почему эти символы не разрешены. - person Holger; 28.05.2015
comment
@Наюки Я использовал синтаксис сборки Кракатау. Вы можете собрать его в файлы классов с помощью Krakatau (github.com/Storyyeller/Krakatau). Он основан на синтаксисе Jasmin, но имеет больше возможностей. - person Antimony; 28.05.2015

Да, вы можете использовать зарезервированные слова. Слова предназначены только для компилятора. Они не появляются в сгенерированном байтовом коде.

Пример использования зарезервированных слов Java находится в языке Scala на основе JVM. Scala имеет другие конструкции и синтаксис, чем Java, но компилируется в байт-код Java для работы на JVM.

Это легальная Scala:

class `class`

Это определяет класс с именем class с конструктором без аргументов. Запуск javap (дизассемблера) на скомпилированном файле class.class показывает

public class class {
    public class();
}

Scala может сделать то же самое с любым другим зарезервированным словом Java.

class int
class `while`
class goto

Их также можно использовать для имен методов или полей.

Как вы и подозревали, вы не сможете использовать эти классы из Java, кроме отражения. Вы можете использовать их из аналогично "настраиваемого" файла класса, например. из файла класса, созданного компилятором Scala.

Таким образом, это ограничение javac (компилятора), а не java (среда VM/runtime).

person Paul Draper    schedule 27.05.2015
comment
javap не является компилятором, - person Tobias; 27.05.2015
comment
Давайте придерживаться дизассемблера ;) - person Tobias; 27.05.2015
comment
Очень приятно, я не знал, что Scala изначально позволяет вам злоупотреблять зарезервированными ключевыми словами Java в качестве имен =) - person Nayuki; 27.05.2015
comment
Я бы назвал javap дизассемблером, потому что он выводит только мнемонику байт-кода. Я играл с настоящими декомпиляторами (такими как DJ), которые берут файлы классов и генерируют исходный код Java. - person Nayuki; 27.05.2015
comment
@NayukiMinase, разница между ними заключается в том, создает ли он исходный код (декомпилятор) или просто читаемое представление (дизассемблер). javap производит только подписи - как исходный код - и опускает реализацию, в отличие от других декомпиляторов (например, DJ) и дизассемблеров (например, IDA), поэтому трудно сказать. Похоже, разборка - более распространенное слово, поэтому я изменю его. - person Paul Draper; 27.05.2015
comment
Официально это дизассемблер. Если вы используете флаг -c, он фактически дизассемблируется, а не просто отображает структуру. - person Tobias; 27.05.2015
comment
Технически это ограничение языка Java, а не javac. Любой совместимый компилятор Java будет применять те же ограничения. - person Antimony; 28.05.2015

Ограничения на имена зафиксированы в спецификации JVM:

§4.2.1. Имена двоичных классов и интерфейсов

Имена классов и интерфейсов, которые появляются в структурах файлов классов, всегда представлены в полностью определенной форме, известной как двоичные имена (JLS §13.1). Такие имена всегда представлены в виде структур CONSTANT_Utf8_info (§4.4.7) и, таким образом, могут быть получены, если нет дополнительных ограничений, из всего кодового пространства Unicode…

По историческим причинам синтаксис двоичных имен, которые появляются в структурах файлов классов, отличается от синтаксиса двоичных имен, описанных в JLS §13.1. В этой внутренней форме точки ASCII (.), которые обычно разделяют идентификаторы, составляющие двоичное имя, заменяются косой чертой ASCII (/). Сами идентификаторы должны быть неквалифицированными именами (§4.2.2).

§4.2.2. Неполные имена

Имена методов, полей, локальных переменных и формальных параметров хранятся как неполные имена. Неполное имя должно содержать по крайней мере одну кодовую точку Unicode и не должно содержать никаких символов ASCII . ; [ / (то есть точки, точки с запятой, левой квадратной скобки или косой черты).

Имена методов имеют дополнительные ограничения, поэтому, за исключением имен специальных методов <init> и <clinit> (§2.9), они не должны содержать символы ASCII < или > (то есть левая угловая скобка или правая угловая скобка).

Итак, ответ таков: есть лишь несколько символов, которые нельзя использовать на двоичном уровне. Во-первых, / — это разделитель пакетов. Затем нельзя использовать ; и [, поскольку они имеют особое значение в подписи полей и метод подписи, которые могут содержать имена типов. В этих подписях [ начинает тип массива, а ; отмечает конец имени ссылочного типа.

Нет четкой причины, по которой . запрещен. Он не используется в JVM и имеет значение только в пределах общие подписи, но если вы используете общие подписи, имена типов дополнительно ограничиваются тем, что им не разрешено содержать <, >, :, а также эти символы имеют особое значение внутри общие подписи тоже.

Следовательно, нарушение спецификации путем использования . в идентификаторах не влияет на основную функцию JVM. Обфускаторы это делают. Полученный код работает, но вы можете столкнуться с проблемами с Reflection при запросе подписей универсального типа. Кроме того, преобразование двоичных имен в исходное имя путем замены всех /s на .s станет необратимым, если двоичное имя содержит .s.


Интересно, что было предложение поддерживать все возможные идентификаторы в синтаксисе Java (см. пункт 3, «экзотические идентификаторы»), но в финальную версию Java 7 он не попал. И, похоже, сейчас никто не предпринимает новых попыток его внедрить.


Существует дополнительное техническое ограничение: имена не могут иметь Измененное представление UTF-8 длиннее 65 535 байт, поскольку число байтов хранится как беззнаковое короткое значение.

person Holger    schedule 28.05.2015
comment
Это фантастический ответ, подкрепленный техническими доказательствами. Мне особенно нравится, как вы объяснили, почему каждый зарезервированный символ особенный, и что происходит с периодом, находящимся в подвешенном состоянии. Спасибо. - person Nayuki; 28.05.2015

  1. Ключевые слова известны только компилятору. Компилятор переводит их в адекватный байт-код. Таким образом, они не существуют во время выполнения скомпилированного байт-кода и, следовательно, не проверяются JVM.
  2. Конечно, вы не можете получить доступ к членам класса, неизвестным во время компиляции. Но вы можете использовать рефлексию для этой цели, если вы уверены, что такой член класса будет существовать в скомпилированном коде (вы будете «творить» их там), потому что доступ по рефлексии не проверяется компилятором.
person Sergei    schedule 27.05.2015
comment
(Небольшие извинения, я изменил свою нумерацию) - person Nayuki; 27.05.2015