Есть ли в настоящее время (Java 6) вещи, которые вы можете сделать в байт-коде Java, которые вы не можете сделать с языка Java?
Я знаю, что оба Тьюринга завершены, поэтому прочитайте "можно", поскольку "может делать значительно быстрее/лучше или просто по-другому".
Ответ 2
После долгой работы с байтовым кодом 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 верификатор VM HotSpot, однако, пропустил эту проверку, и именно поэтому это было возможно. Это использовалось несколькими инструментами генерации кода как своего рода хак, но не более чем законно реализовать такой класс.
Последний был просто ошибкой в этой версии компилятора. В новых версиях компилятора это снова возможно.
Определить класс без какого-либо конструктора
Компилятор 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
, или инициализатором класса с аргументами, хотя эти методы нельзя назвать.
Вызвать любой супер метод (до 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
. Этот код операции обычно используется для реализации суперпользователей, но вы можете повторно использовать код операции для реализации описанного поведения.
Аннотации мелкозернистого типа
В Java аннотации применяются в соответствии с их @Target
, которые объявляются аннотациями. Используя манипуляции с байтовым кодом, можно определить аннотации независимо от этого элемента управления. Кроме того, можно, например, аннотировать тип параметра без аннотирования параметра, даже если аннотация @Target
применяется к обоим элементам.
Определить любой атрибут для типа или его членов
В языке Java можно определять аннотации только для полей, методов или классов. В JBC вы можете вставлять любую информацию в классы Java. Чтобы использовать эту информацию, вы, однако, больше не можете полагаться на механизм загрузки классов Java, но вам нужно извлечь метаинформацию самостоятельно.
Переполнение и неявное назначение byte
, short
, char
и boolean
значений
Последние примитивные типы обычно не известны в JBC, а определяются только для типов массивов или для дескрипторов полей и методов. Внутри команд байтового кода все именованные типы занимают пространство 32 бит, которое позволяет представлять их как int
. Официально только типы int
, float
, long
и double
существуют в байтовом коде, для которого все требуется явное преобразование по правилу верификатора JVM.
Не выпускать монитор
Блок A 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 учитывается только второе условие. Первый, однако, не имеет значения.
Вызов супер метода для экземпляра, который не является this
Компилятор Java только позволяет вызывать метод супер (или интерфейс по умолчанию) в экземплярах this
. В байтовом коде также можно вызвать супер-метод для экземпляра того же типа, что и для следующего:
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
-encoded values. Поэтому можно определить информацию об общих типах, которые не соответствуют стиранию. В качестве позывной могут выполняться следующие утверждения:
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-компилируемой Java. 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, но это на самом деле не позволит вам добиться чего-то нового. Как указано в контексте, эти упомянутые "функциональные инструкции" теперь удаляются из набора юридических кодов операций, что делает их еще менее характерными.