Недоступні в мові можливості байткоду Java


Дізнайтесь більше про нові кар'єрні можливості в EchoUA. Цікаві проекти, ринкова оплата, гарний колектив. Надсилайте резюме та приєднуйтеся до нас.

Розповідає Rafael Winterhalter


Після того, як я тривалий час працював з байткодом Java і проводив дослідження в цій області, тому хочу поділитися висновками, до яких я дійшов.

Виконання в конструкторі коду до виклику суперконструктора або допоміжного конструктора

У Java як мові програмування (далі – JPL, від Java Programming Language) виклик super () чи this () має бути першим виразом у конструкторі, але в байткоді Java (далі – JBC, від Java Byte Code) це не так. Ви можете додати код перед цими викликами, якщо виконуються наступні умови:

  • інший конструктор все-таки викликаний далі;
  • він викликаний не всередині умовного;
  • до виклику конструктора не прочитуються поля і не викликаються методи конструйованого об’єкта. З викладеного постає запитання.

 

Робота з полями сутності до виклику super () чи this ()?

До шостої версії в Java був наступний експлойт, який дозволяв зробити вищеописане:

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 (); }}

Тепер у JPL така робота з полями неможлива, але все ще можлива засобами JBC.

Вибір конструктора, що викликається (до Java 7u23)

Java не дозволяє створювати конструктори на зразок такого:

class Foo { Foo (){ } Foo (Void v) { }}class Bar () extends Foo { Bar (){ if (System.currentTimeMillis () % 2 == 0) { super (); } else { super (null); } }}

Проте до Java 7u23 верифікатор HotSpot VM’s пропускав цю перевірку. Тепер це пофіксили.

Створення класу без конструктора

У JPL це неможливо – хоч один конструктор, але завжди наслідується. За допомогою JBC можна зробити так, що створити екземпляр класу буде неможливо, навіть якщо використати рефлексію (щоправда sun.misc.unsafe все одно дозволяє зробити це).

Створення методів з однаковими сигнатурами, але різними поверненими типами

У JPL методи ідентифікуються за ім’ям і набором параметрів, а в JBC – ще і поверненим типом.

Виклик винятків, що перевіряються (checked), без вказівки throws чи конструкції try...catch

Перевірку того, чи всі винятки, що перевіряються, спіймані (чи вказані за допомогою throws), здійснює компілятор, це не залежить від Java Runtime або JBC.

Використання динамічного виклику методів поза лямбда-виразами

Так званий динамічний виклик методів може бути використаний для чого завгодно, а не тільки для лямбда-виразів. Його використання дозволяє, наприклад, міняти логіку програми під час виконання. Багато динамічних мов програмування, які ґрунтуються на JBC, покращали за рахунок цієї інструкції. У JBC Ви можете також використати лямбда-вирази в Java 7, коли компілятор ще не міг обробляти динамічний виклик методів, а JVM – могла.

Використання ідентифікаторів, які зазвичай заборонені

Хотіли додати в ім’я Вашого методу пропуск або перенесення рядка? Створіть свій JBC, і вдалої відладки! Заборонені тільки символи “”., “;”, “[” і “/”. Якщо метод називається не <init> чи <clinit>, його ім’я не може містити символів “<” і “>”.

Перепризначення final полів, final параметрів і this

Параметра final у JBC немає, і будь-який параметр, враховуючи this (у нульовому індексі) може бути змінений. Константне поле також може бути змінене, якщо воно вже було визначене в конструкторі (для static поля цього не потрібно).

Виклик методу в null

У JBC Ви можете викликати будь-який нестатичний метод null, і він відмінно працюватиме, якщо в ньому немає виклику this.

Використання конструкторів та ініціалізаторів  як методів

У JBC конструктори та ініціалізатори нічим не відрізняються від методів, єдине – вони повинні мати імена <init> і <clinit> відповідно, – щоб JVM могла перевіряти, чи викликає один конструктор інший коректний конструктор.

Невіртуальний виклик методу з того самого класу

class Foo { void foo (){ bar (); } void bar (){ }}class Bar extends Foo { @Override void bar (){ throw new RuntimeException (); }}

У JPL виклик new Bar::foo() завжди викликатиме Runtime Exception. Не можна зробити так, щоб метод Foo::foo() завжди викликав bar () зі свого класу. Однак Ви можете реалізувати таку поведінку на JBC за допомогою опкоду INVOKESPECIAL, який зазвичай використовується для виклику батьківських методів.

Призначення довільного мета-атрибута

У JPL Ви можете додавати анотації тільки до полів, методів і класів. У JBC Ви можете вбудувати будь-яку інформацію в клас. Щоправда, для її використання Вам доведеться витягати її самостійно, не покладаючись на механізм завантаження класів Java.

Перенаповнення і неявне визначення значень byte, short, char і boolean

Технічно, всередині байткоду є тільки типи int, float, long і double, що дає простір для багатьох неприпустимих для JPL операцій.

Рекурсивний блок catch

У байткоді Java Ви можете зробити щось подібне до

try { throw new Exception ();} catch (Exception e) {  throw Exception ();}

Виклик будь-якого “методу за умовчанням”

У JBC можна викликати default-метод, навіть якщо він був перевизначений.

Виклик методу батька в об’єкта, що не є this

У JPL можна викликати тільки методи свого найближчого батька або default-методи інтерфейсів. У JBC можливий код на кшталт:

class Foo { void m (Foo f) {///Тип має бути одним і тим же
f.super.toString (); // викликає Object::toString } public String toString (){ return " foo"; }}

Доступ до синтетичних членів

У байткоді до синтетичних членів можна звертатися безпосередньо.

class Foo { class Bar { void bar (Bar bar) { Foo foo = bar.Foo.this; } }}

Справедливо також для синтетичних полів, методів і класів.

Додавання некоректної інформації про дженеріки

Інформація про дженеріки зберігається в класі у вигляді рядків. Верифікатор не перевіряє її, тобто можна зробити так, що наступні асерти будуть правильними:

Method method =...assertTrue (method.getParameterTypes ()!= method.getGenericParameterTypes());Field field =...assertTrue (field.getFieldType () == String.class);
assertTrue (field.getGenericFieldType () == Integer.class);

Виклик неіснуючого методу і краш JVM

Ви можете викликати будь-який метод у будь-якого екземпляра. Зазвичай верифікатор розпізнає це, але я помітив, що іноді, коли Ви викликаєте метод в (елемента) масиву, деякі версії JVM не розпізнають, що його не має. Це, звичайно, сумнівна можливість, але за допомогою коду, який пройшов через javac, Ви цього точно зробити не зможете. Зрозуміло, коли JVM дійде до цього місця, на неї чекає краш.

Оригінал: “Bytecode features not available in Java Language

Київ, Харків, Одеса, Дніпро, Запоріжжя, Кривий Ріг, Вінниця, Херсон, Черкаси, Житомир, Хмельницький, Чернівці, Рівне, Івано-Франківськ, Кременчук, Тернопіль, Луцьк, Ужгород, Кам'янець-Подільський, Стрий - за статистикою саме з цих міст програмісти найбільше переїжджають працювати до Львова. А Ви розглядаєте relocate?


Залишити відповідь

Ваша e-mail адреса не оприлюднюватиметься. Обов’язкові поля позначені *