После долгой работы с байт-кодом Java и некоторых дополнительных исследований по этому вопросу, вот краткое изложение моих выводов:
Выполнить код в конструкторе перед вызовом супер-конструктора или вспомогательного конструктора
В языке программирования Java (JPL) первым оператором конструктора должен быть вызов суперконструктора или другого конструктора того же класса. Это не относится к байт-коду Java (JBC). В байт-коде абсолютно законно выполнять любой код перед конструктором, если:
- Другой совместимый конструктор вызывается через некоторое время после этого блока кода.
- Этот вызов не входит в условный оператор.
- Перед этим вызовом конструктора ни одно поле созданного экземпляра не читается, и ни один из его методов не вызывается. Это подразумевает следующий пункт.
Установите поля экземпляра перед вызовом супер-конструктора или вспомогательного конструктора
Как упоминалось ранее, совершенно правильно установить значение поля экземпляра перед вызовом другого конструктора. Существует даже устаревший хак, который позволяет использовать эту «функцию» в версиях Java до 6:
class Foo {
public String s;
public Foo() {
System.out.println(s);
}
}
class Bar extends Foo {
public Bar() {
this(s = "Hello World!");
}
private Bar(String helper) {
super();
}
}
Таким образом, поле может быть установлено до вызова супер-конструктора, что, однако, более невозможно. В JBC такое поведение все еще можно реализовать.
Разветвите вызов супер-конструктора
В Java невозможно определить вызов конструктора как
class Foo {
Foo() { }
Foo(Void v) { }
}
class Bar() {
if(System.currentTimeMillis() % 2 == 0) {
super();
} else {
super(null);
}
}
До Java 7u23, однако, верификатор HotSpot VM пропустил эту проверку, поэтому это было возможно. Это использовалось несколькими инструментами генерации кода как своего рода хак, но реализация такого класса уже недопустима.
Последний был просто ошибкой в этой версии компилятора. В новых версиях компилятора это снова возможно.
Определить класс без конструктора
Компилятор Java всегда реализует по крайней мере один конструктор для любого класса. В байт-коде Java это не требуется. Это позволяет создавать классы, которые не могут быть построены даже при использовании отражения. Тем не менее, использование по- sun.misc.Unsafe
прежнему позволяет создавать такие экземпляры.
Определите методы с одинаковой подписью, но с другим типом возврата
В JPL метод идентифицируется как уникальный по имени и типам необработанных параметров. В JBC необработанный тип возврата дополнительно рассматривается.
Определите поля, которые не отличаются по имени, но только по типу
Файл класса может содержать несколько полей с одинаковыми именами, если они объявляют другой тип поля. JVM всегда ссылается на поле как кортеж имени и типа.
Бросьте необъявленные проверенные исключения, не ловя их
Среда выполнения Java и байт-код Java не знают о концепции проверяемых исключений. Только компилятор Java проверяет, что проверенные исключения всегда либо перехватываются, либо объявляются, если они выброшены.
Использовать динамический вызов метода вне лямбда-выражений
Так называемый динамический вызов метода может использоваться для чего угодно, не только для лямбда-выражений Java. Использование этой функции позволяет, например, отключить логику выполнения во время выполнения. Многие динамические языки программирования, которые сводятся к JBC, улучшили свою производительность с помощью этой инструкции. В байт-коде Java вы также можете эмулировать лямбда-выражения в Java 7, где компилятор еще не допускал никакого использования динамического вызова метода, в то время как JVM уже поняла инструкцию.
Используйте идентификаторы, которые обычно не считаются законными
Вы когда-нибудь мечтали использовать пробелы и разрыв строки в имени вашего метода? Создайте свой собственный JBC и удачи в проверке кода. Единственный недопустимые символы для идентификаторов .
, ;
, [
и /
. Кроме того, методы, которые не названы <init>
или <clinit>
не могут содержать <
и >
.
Переназначить final
параметры или this
ссылку
final
параметры не существуют в JBC и, следовательно, могут быть переназначены. Любой параметр, включая this
ссылку, хранится только в простом массиве в JVM, что позволяет переназначить this
ссылку на индекс в 0
пределах одного фрейма метода.
Переназначить final
поля
Пока конечное поле назначается в конструкторе, допустимо переназначить это значение или даже вообще не присваивать значение. Следовательно, следующие два конструктора являются допустимыми:
class Foo {
final int bar;
Foo() { } // bar == 0
Foo(Void v) { // bar == 2
bar = 1;
bar = 2;
}
}
Для static final
полей даже разрешено переназначать поля вне инициализатора класса.
Обрабатывайте конструкторы и инициализатор класса так, как если бы они были методами
Это скорее концептуальная особенность, но конструкторы в JBC не трактуются иначе, чем обычные методы. Только верификатор JVM гарантирует, что конструкторы вызывают другой допустимый конструктор. Кроме этого, это просто соглашение об именах Java, что конструкторы должны вызываться и вызываться <init>
инициализатор класса <clinit>
. Помимо этой разницы, представление методов и конструкторов идентично. Как отметил Хольгер в комментарии, вы можете даже определить конструкторы с типами возвращаемых данных, отличными от void
инициализатора класса с аргументами, даже если невозможно вызвать эти методы.
Создать асимметричные записи * .
При создании записи
record Foo(Object bar) { }
javac сгенерирует файл класса с одним именем bar
, методом доступа bar()
и одним конструктором Object
. Кроме того, добавлен атрибут записи для bar
. Создавая запись вручную, можно создать конструктор другой формы, пропустить поле и реализовать средство доступа по-разному. В то же время, все еще возможно заставить API отражения полагать, что класс представляет фактическую запись.
Вызовите любой супер метод (до Java 1.1)
Однако это возможно только для версий Java 1 и 1.1. В JBC методы всегда отправляются с явным целевым типом. Это означает, что для
class Foo {
void baz() { System.out.println("Foo"); }
}
class Bar extends Foo {
@Override
void baz() { System.out.println("Bar"); }
}
class Qux extends Bar {
@Override
void baz() { System.out.println("Qux"); }
}
можно было реализовать Qux#baz
для вызова Foo#baz
при перепрыгивании Bar#baz
. Хотя все еще возможно определить явный вызов для вызова другой реализации супер-метода, чем у прямого суперкласса, это больше не имеет никакого эффекта в версиях Java после 1.1. В Java 1.1 это поведение контролировалось путем установки ACC_SUPER
флага, который включал бы то же поведение, которое только вызывает реализацию прямого суперкласса.
Определить не виртуальный вызов метода, который объявлен в том же классе
В Java невозможно определить класс
class Foo {
void foo() {
bar();
}
void bar() { }
}
class Bar extends Foo {
@Override void bar() {
throw new RuntimeException();
}
}
Приведенный выше код всегда приводит к тому, что RuntimeException
когда foo
вызывается экземпляр Bar
. Невозможно определить Foo::foo
метод для вызова его собственного bar
метода, который определен в Foo
. Поскольку bar
это не частный метод экземпляра, вызов всегда виртуальный. С байт - код, можно , однако определить вызов использовать INVOKESPECIAL
опкод , который непосредственно связывает bar
вызов метода в Foo::foo
к Foo
версии «S. Этот код операции обычно используется для реализации вызовов супер-методов, но вы можете повторно использовать код операции для реализации описанного поведения.
Мелкозернистые аннотации
В Java аннотации применяются в соответствии с тем, @Target
что аннотации объявляют. Используя манипулирование байтовым кодом, можно определять аннотации независимо от этого элемента управления. Кроме того, например, можно аннотировать тип параметра без аннотирования параметра, даже если @Target
аннотация применяется к обоим элементам.
Определите любой атрибут для типа или его членов
В языке Java можно определять аннотации только для полей, методов или классов. В JBC вы можете встраивать любую информацию в классы Java. Однако, чтобы использовать эту информацию, вы больше не можете полагаться на механизм загрузки классов Java, но вам нужно самостоятельно извлечь метаинформацию.
Перепускные и неявно Присвоить byte
, short
, char
и boolean
значения
Последние типы примитивов обычно не известны в JBC, но определяются только для типов массивов или для дескрипторов полей и методов. В инструкциях байт-кода все именованные типы занимают 32-битное пространство, что позволяет представлять их как int
. Официально, только int
, float
, long
и double
типы существуют в байт - код , который всем необходимо явное преобразование по правилу испытателя в JVM в.
Не выпускать монитор
synchronized
Блок на самом деле состоит из двух утверждений, одно приобретение и один , чтобы выпустить монитор. В JBC вы можете приобрести его, не выпуская его.
Примечание . В недавних реализациях HotSpot это приводит к IllegalMonitorStateException
завершению метода или к неявному освобождению, если метод завершается самим исключением.
Добавить более одного return
оператора в инициализатор типа
В Java даже тривиальный инициализатор типа, такой как
class Foo {
static {
return;
}
}
незаконно В байтовом коде инициализатор типа обрабатывается так же, как и любой другой метод, т. Е. Операторы return могут быть определены где угодно.
Создать неприводимые циклы
Компилятор Java преобразует циклы в операторы goto в байт-коде Java. Такие операторы могут использоваться для создания неприводимых циклов, чего никогда не делает компилятор Java.
Определить рекурсивный блок catch
В байт-коде Java вы можете определить блок:
try {
throw new Exception();
} catch (Exception e) {
<goto on exception>
throw Exception();
}
Подобное утверждение создается неявно при использовании synchronized
блока в Java, где любое исключение при освобождении монитора возвращает к инструкции по освобождению этого монитора. Как правило, в такой инструкции не должно возникать никаких исключений, но если она будет (например, устарела ThreadDeath
), монитор все равно будет освобожден.
Вызовите любой метод по умолчанию
Компилятор Java требует выполнения нескольких условий, чтобы разрешить вызов метода по умолчанию:
- Метод должен быть самым конкретным (не должен быть переопределен подчиненным интерфейсом, который реализован любым типом, включая супертипы).
- Тип интерфейса метода по умолчанию должен быть реализован непосредственно классом, вызывающим метод по умолчанию. Однако, если интерфейс
B
расширяет интерфейс, A
но не переопределяет метод A
, метод все равно может быть вызван.
Для байт-кода Java учитывается только второе условие. Первый, однако, не имеет значения.
Вызовите метод super для экземпляра, который не this
Компилятор Java позволяет вызывать метод super (или интерфейс по умолчанию) только в случаях this
. В байтовом коде также возможно вызвать метод super для экземпляра того же типа, подобного следующему:
class Foo {
void m(Foo f) {
f.super.toString(); // calls Object::toString
}
public String toString() {
return "foo";
}
}
Доступ к синтетическим членам
В байт-коде Java можно получить прямой доступ к синтетическим членам. Например, рассмотрим, как в следующем примере Bar
осуществляется доступ к внешнему экземпляру другого экземпляра:
class Foo {
class Bar {
void bar(Bar bar) {
Foo foo = bar.Foo.this;
}
}
}
Как правило, это верно для любой синтетической области, класса или метода.
Определить несинхронизированную информацию общего типа
Хотя среда выполнения Java не обрабатывает универсальные типы (после того, как компилятор Java применяет стирание типов), эта информация все еще привязывается к скомпилированному классу как метаинформация и становится доступной через API отражения.
Верификатор не проверяет согласованность этих String
значений, закодированных в метаданных. Следовательно, можно определить информацию об универсальных типах, которая не соответствует стиранию. Как следствие, следующие утверждения могут быть верными:
Method method = ...
assertTrue(method.getParameterTypes() != method.getGenericParameterTypes());
Field field = ...
assertTrue(field.getFieldType() == String.class);
assertTrue(field.getGenericFieldType() == Integer.class);
Кроме того, подпись может быть определена как недопустимая, так что возникает исключение времени выполнения. Это исключение выдается, когда к информации обращаются впервые, поскольку она оценивается лениво. (Аналогично значениям аннотации с ошибкой.)
Добавлять метаинформацию только для определенных методов
Компилятор Java позволяет встраивать имя параметра и информацию модификатора при компиляции класса с parameter
включенным флагом. В формате файла класса Java эта информация сохраняется для каждого метода, что позволяет встраивать такую информацию о методе только для определенных методов.
Запутать вещи и крушение вашей JVM
Например, в байт-коде Java вы можете определить, что вызывать любой метод любого типа. Обычно верификатор будет жаловаться, если тип не знает такого метода. Однако, если вы вызываете неизвестный метод в массиве, я обнаружил ошибку в какой-то версии JVM, когда верификатор пропустит это, и ваша JVM завершит работу после вызова инструкции. Хотя это вряд ли особенность, но технически это то, что невозможно с помощью Java скомпилированного javac . У Java есть своего рода двойная проверка. Первая проверка применяется компилятором Java, вторая - JVM при загрузке класса. Пропустив компилятор, вы можете найти слабое место в проверке верификатора. Хотя это скорее общее утверждение, чем особенность.
Аннотируйте тип получателя конструктора, когда нет внешнего класса
Начиная с Java 8, нестатические методы и конструкторы внутренних классов могут объявлять тип получателя и аннотировать эти типы. Конструкторы классов верхнего уровня не могут аннотировать свой тип получателя, так как большинство из них не объявляют его.
class Foo {
class Bar {
Bar(@TypeAnnotation Foo Foo.this) { }
}
Foo() { } // Must not declare a receiver type
}
Однако, поскольку Foo.class.getDeclaredConstructor().getAnnotatedReceiverType()
он возвращает AnnotatedType
представление Foo
, можно включить аннотации типов для Foo
конструктора России непосредственно в файл класса, где эти аннотации позднее читаются API-интерфейсом отражения.
Использовать неиспользуемые / устаревшие инструкции байт-кода
Поскольку другие назвали это, я включу это также. Java была ранее использование подпрограмм по JSR
и RET
отчетности. JBC даже знал свой собственный тип обратного адреса для этой цели. Однако использование подпрограмм усложнило статический анализ кода, поэтому эти инструкции больше не используются. Вместо этого компилятор Java будет дублировать код, который он компилирует. Тем не менее, это в основном создает идентичную логику, поэтому я на самом деле не считаю это достижением чего-то другого. Точно так же вы можете, например, добавитьNOOP
инструкция байтового кода, которая также не используется компилятором Java, но это также не позволит вам достичь чего-то нового. Как указано в контексте, эти упомянутые «инструкции по функциям» теперь удалены из набора допустимых кодов операций, что делает их еще менее функциональными.