Недоступные в языке возможности байткода Java
8К открытий8К показов
Рассказывает 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»
8К открытий8К показов