Після довгої роботи з байт-кодом 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
, методом accessor імені 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;
}
}
є незаконним. У байт-коді ініціалізатор типу трактується так само, як і будь-який інший метод, тобто оператори повернення можуть бути визначені де завгодно.
Створіть невідворотні петлі
Компілятор Java перетворює петлі в goto заяви в байт-коді Java. Такі заяви можна використовувати для створення невідворотних циклів, що компілятор Java ніколи не робить.
Визначте рекурсивний блок лову
У байт-коді 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
кодованих значень. Тому можна визначити інформацію про загальні типи, яка не відповідає стиранню. Як підступність, такі правдиві твердження можуть бути правдивими:
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 завершиться після запуску інструкції. Навряд чи це особливість , хоча, але це технічно то , що ні представляється можливим з JAVAC скомпільований 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
конструктора 's безпосередньо у файл класу, де ці анотації згодом читаються API відображення.
Використовуйте інструкції з невикористаного / застарілого байтового коду
Оскільки інші назвали його, я також включу його. Java раніше використовувала підпрограми за допомогою JSR
і RET
заяви. JBC навіть знав власний тип зворотної адреси для цієї мети. Однак використання підпрограм зробило надто складний аналіз статичного коду, тому ці інструкції більше не використовуються. Натомість компілятор Java буде дублювати код, який він компілює. Однак, це в основному створює ідентичну логіку, тому я не дуже вважаю, щоб досягти чогось іншого. Так само ви можете, наприклад, додатиNOOP
інструкція по байтовому коду, яка також не використовується компілятором Java, але це також не дозволить вам досягти чогось нового. Як було зазначено в контексті, ці згадані "інструкції щодо функцій" тепер вилучені з набору юридичних опкодів, що робить їх ще менше функцією.