Недоступные в языке возможности байткода Java

Рассказывает Rafael Winterhalter


После того, как я достаточно продолжительное время работал с байткодом Java и проводил некоторые изыскания в этой области, я хочу поделиться выводами, которые я сделал.

Выполнение в конструкторе кода до вызова суперконструктора или вспомогательного конструктора

В Java как языке программирования (далее — JPL, от Java Programming Language) вызов super() или this() должен быть первым выражением в конструкторе, но в байткоде Java (далее — JBC, от Java Byte Code) это не так. Вы можете добавить код перед этими вызовами, если выполняются следующие условия:

  • другой конструктор всё-таки вызван далее;
  • он вызван не внутри условного;
  • до вызова конструктора не считываются поля и не вызываются методы конструируемого объекта. Из этого происходит следующий вопрос.

Работа с полями сущности до вызова super() или this()

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

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

Выбор вызываемого конструктора (до Java 7u23)

Java не позволяет создавать конструкторы вроде такого:

Тем не менее, до 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 могла проверять, вызывает ли один конструктор другой корректный конструктор.

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

В 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 вы можете сделать что-то вроде

Вызов любого «метода по умолчанию»

В JBC можно вызвать default-метод, даже если он был переопределён.

Вызов метода родителя у объекта, не являющегося this

В JPL можно вызывать только методы своего ближайшего родителя или default-методы интерфейсов. В JBC возможен код наподобие:

Доступ к синтетическим членам

В байткоде к синтетическим членам можно обращаться напрямую.

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

Добавление некорректной информации о дженериках

Информация о дженериках хранится в классе в виде строк. Верификатор никак не проверяет её, а значит, можно сделать так, что следующие ассерты будут верными:

Вызов несуществующего метода и краш JVM

Вы можете вызвать любой метод у любого экземпляра. Обычно верификатор распознаёт это, но я заметил, что иногда, когда вы вызываете метод у (элемента) массива, некоторые версии JVM не распознают, что его не существует. Это, конечно, сомнительная возможность, но с помощью кода, который прошёл через javac, вы этого точно сделать не сможете. Разумеется, когда JVM дойдёт до этого места, её ждёт краш.

Оригинал: «Bytecode features not available in Java Language»

Пётр Соковых, транслятор двоичного кода в русский язык