Функции байт-кода недоступны на языке Java
Существуют ли в настоящее время (Java 6) вещи, которые вы можете сделать в байт-коде Java, которые вы не можете сделать из языка Java?
Я знаю, что оба Тьюринга завершены, поэтому читать "можно сделать", как "можно сделать значительно быстрее / лучше, или просто по-другому".
Я думаю о дополнительных байт-кодах, таких как invokedynamic
, который не может быть сгенерирован с использованием Java, за исключением того, что этот предназначен для будущей версии.
9 ответов
Насколько я знаю, в байт-кодах, поддерживаемых Java 6, отсутствуют основные функции, которые также недоступны из исходного кода Java. Основная причина этого заключается в том, что байт-код Java был разработан с учетом языка Java.
Однако есть некоторые функции, которые не создаются современными компиляторами Java:
ACC_SUPER
флаг:Это флаг, который может быть установлен для класса и указывает, как конкретный угловой случай
invokespecial
Байт-код обрабатывается для этого класса. Он устанавливается всеми современными компиляторами Java (где "современный" ->= Java 1.1, если я правильно помню), и только древние компиляторы Java создавали файлы классов, где это не было установлено. Этот флаг существует только по причинам обратной совместимости. Обратите внимание, что начиная с Java 7u51, ACC_SUPER полностью игнорируется по соображениям безопасности.jsr
/ret
байткоды.Эти байт-коды использовались для реализации подпрограмм (в основном для реализации
finally
блоки). Они больше не производятся с Java 6. Причина их устаревания заключается в том, что они значительно усложняют статическую проверку без большой выгоды (т. Е. Используемый код почти всегда может быть повторно реализован с обычными переходами с очень небольшими накладными расходами).Наличие двух методов в классе, которые отличаются только типом возвращаемого значения.
Спецификация языка Java не допускает двух методов в одном и том же классе, если они различаются только по типу возвращаемого значения (т. Е. Одно и то же имя, один и тот же список аргументов,...). Спецификация JVM, однако, не имеет такого ограничения, поэтому файл класса может содержать два таких метода, просто нет способа создать такой файл класса с помощью обычного компилятора Java. В этом ответе есть хороший пример / объяснение.
После долгой работы с байт-кодом 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
или инициализатор класса с аргументами, даже если невозможно вызвать эти методы.
Вызовите любой супер метод (до 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.
Не выпускать монитор
synchronized
Блок на самом деле состоит из двух операторов: один для получения и один для выпуска монитора. В JBC вы можете приобрести его, не выпуская его.
Примечание. В недавних реализациях HotSpot это приводит к IllegalMonitorStateException
в конце метода или до неявного освобождения, если метод завершается самим исключением.
Добавить более одного return
оператор инициализатора типа
В Java даже тривиальный инициализатор типа, такой как
class Foo {
static {
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-компилируемой 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, но это также не позволит вам достичь чего-то нового. Как указано в контексте, эти упомянутые "инструкции по функциям" теперь удалены из набора допустимых кодов операций, что делает их еще менее функциональными.
Вот некоторые функции, которые могут быть реализованы в байт-коде Java, но не в исходном коде Java:
Выдает проверенное исключение из метода, не объявляя, что метод его выбрасывает. Проверенные и непроверенные исключения - это то, что проверяется только компилятором Java, а не JVM. Из-за этого, например, Scala может генерировать проверенные исключения из методов, не объявляя их. Хотя с Java-дженериками есть обходной путь, называемый хитрым броском.
Наличие в классе двух методов, которые отличаются только типом возвращаемого значения, как уже упоминалось в ответе Йоахима: спецификация языка Java не допускает двух методов в одном классе, если они различаются только по типу возвращаемого значения (т. Е. Одно и то же имя, один и тот же список аргументов,...). Спецификация JVM, однако, не имеет такого ограничения, поэтому файл класса может содержать два таких метода, просто нет способа создать такой файл класса с помощью обычного компилятора Java. В этом ответе есть хороший пример / объяснение.
GOTO
может использоваться с метками для создания собственных управляющих структур (кромеfor
while
так далее)- Вы можете переопределить
this
локальная переменная внутри метода - Комбинируя оба из них, вы можете создать оптимизированный байт-код создания хвостового вызова (я делаю это в JCompilo)
В качестве связанного момента вы можете получить имя параметра для методов, если оно скомпилировано с помощью debug ( Paranamer делает это, читая байт-код
Может быть, раздел 7А в этом документе представляет интерес, хотя речь идет о подводных камнях, а не о функциях байт-кода.
В языке Java первым оператором в конструкторе должен быть вызов конструктора суперкласса. Байт-код не имеет этого ограничения, вместо этого правило состоит в том, что конструктор суперкласса или другой конструктор в том же классе должен вызываться для объекта перед доступом к членам. Это должно позволить больше свободы, таких как:
- Создайте экземпляр другого объекта, сохраните его в локальной переменной (или в стеке) и передайте его в качестве параметра конструктору суперкласса, сохраняя при этом ссылку на эту переменную для другого использования.
- Вызовите другие конструкторы в зависимости от условия. Это должно быть возможно: как условно вызвать другой конструктор в Java?
Я не проверял их, поэтому, пожалуйста, поправьте меня, если я ошибаюсь.
Я написал оптимизатор байт-кода, когда был I-Play (он был разработан, чтобы уменьшить размер кода для приложений J2ME). Одной из функций, которую я добавил, была возможность использовать встроенный байт-код (аналогично встроенному языку ассемблера в C++). Мне удалось уменьшить размер функции, которая была частью библиотечного метода, с помощью инструкции DUP, поскольку мне нужно значение в два раза. У меня также были нулевые байтовые инструкции (если вы вызываете метод, который принимает символ, и вы хотите передать int, который, как вы знаете, не нужно приводить, я добавил int2char(var), чтобы заменить char (var), и он удалил бы Инструкция i2c для уменьшения размера кода. Я также сделал это с плавающей точкой a = 2.3; с плавающей точкой b = 3.4; с плавающей точкой c = a + b; и это будет преобразовано в фиксированную точку (быстрее, а некоторые J2ME этого не сделали) поддержка плавающей запятой).
То, что вы можете делать с байтовым кодом, а не с простым Java-кодом, - это генерировать код, который можно загружать и запускать без компилятора. Многие системы имеют JRE, а не JDK, и если вы хотите генерировать код динамически, может быть лучше, если не проще, генерировать байт-код вместо кода Java, прежде чем его можно будет использовать.
В Java, если вы пытаетесь переопределить открытый метод защищенным методом (или любым другим ограничением доступа), вы получаете ошибку: "попытка назначить более слабые права доступа". Если вы делаете это с помощью байт-кода JVM, то с верификатором все в порядке, и вы можете вызывать эти методы через родительский класс, как если бы они были публичными.