Функции Bytecode недоступны на языке Java

Есть ли в настоящее время (Java 6) вещи, которые вы можете сделать в байт-коде Java, которые вы не можете сделать с языка Java?

Я знаю, что оба Тьюринга завершены, поэтому прочитайте "можно", поскольку "может делать значительно быстрее/лучше или просто по-другому".

Я думаю о дополнительных байткодах, таких как invokedynamic, которые не могут быть сгенерированы с использованием Java, за исключением того, что они предназначены для будущей версии.

Ответ 1

Насколько я знаю, в байт-кодах, поддерживаемых Java 6, нет основных функций, которые также не доступны из исходного кода Java. Основная причина этого заключается в том, что байт-код Java был разработан с учетом языка Java.

Есть некоторые функции, которые не производятся современными компиляторами Java, однако:

  • ACC_SUPER flag:

    Это флаг, который может быть установлен в классе, и указывает, как обрабатывается конкретный краевой регистр invokespecial байт-кода для этого класса. Он задается всеми современными компиляторами Java (где "modern" is >= Java 1.1, если я правильно помню), и только древние компиляторы Java создавали файлы классов, где это не было установлено. Этот флаг существует только для соображений обратной совместимости. Обратите внимание, что, начиная с Java 7u51, ACC_SUPER полностью игнорируется из-за соображений безопасности.

  • Байт-коды jsr/ret.

    Эти байт-коды использовались для реализации подпрограмм (в основном для реализации блоков finally). Они больше не воспроизводятся с Java 6. Причиной их устаревания является то, что они усложняют статическую проверку очень без большого выигрыша (т.е. Используемый код почти всегда может быть повторно реализован с обычными переходами с очень небольшими накладными расходами).

  • Наличие двух методов в классе, которые отличаются только типом возврата.

    Спецификация языка Java не позволяет использовать два метода в одном классе, если они отличаются только их возвращаемым типом (например, одним и тем же именем, одним и тем же списком аргументов,...). Однако спецификация JVM не имеет такого ограничения, поэтому файл класса может содержать два таких метода, просто невозможно создать такой файл класса с использованием обычного Java-компилятора. Там хороший пример/объяснение в этом ответе.

Ответ 2

После долгой работы с байтовым кодом 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 верификатор VM HotSpot, однако, пропустил эту проверку, и именно поэтому это было возможно. Это использовалось несколькими инструментами генерации кода как своего рода хак, но не более чем законно реализовать такой класс.

Последний был просто ошибкой в ​​этой версии компилятора. В новых версиях компилятора это снова возможно.

Определить класс без какого-либо конструктора

Компилятор 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.

Не выпускать монитор

Блок

A synchronized на самом деле состоит из двух операторов, один для их приобретения и один для выпуска монитора. В JBC вы можете приобрести один, не выпуская его.

Примечание. В недавних реализациях HotSpot это вместо этого приводит к IllegalMonitorStateException в конце метода или к неявной версии, если метод завершается самим исключением.

Добавить несколько операторов return в инициализатор типа

В Java даже тривиальный инициализатор типа, например

class Foo {
  static {
    return;
  }
}

является незаконным. В байтовом коде инициализатор типа обрабатывается так же, как и любой другой метод, т.е. Операторы 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 учитывается только второе условие. Первый, однако, не имеет значения.

Вызов супер метода для экземпляра, который не является 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 -encoded values. Поэтому можно определить информацию об общих типах, которые не соответствуют стиранию. В качестве позывной могут выполняться следующие утверждения:

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, но это на самом деле не позволит вам добиться чего-то нового. Как указано в контексте, эти упомянутые "функциональные инструкции" теперь удаляются из набора юридических кодов операций, что делает их еще менее характерными.

Ответ 3

Вот некоторые функции, которые можно выполнить в байт-коде Java, но не в исходном коде Java:

  • Выбрасывание проверенного исключения из метода, не объявляя, что метод выбрасывает его. Проверенные и непроверенные исключения - это вещь, которая проверяется только компилятором Java, а не JVM. Из-за этого, например, Scala может выдавать проверенные исключения из методов без их объявления. Хотя с Java-дженериками существует обходное решение, называемое скрытый бросок.

  • Имея два метода в классе, которые отличаются только типом возврата,, как уже упоминалось в Ответ Joachim: Язык Java спецификация не позволяет использовать два метода в том же классе, когда они отличаются только их возвращаемым типом (то есть с тем же именем, одним и тем же списком аргументов,...). Однако спецификация JVM не имеет такого ограничения, поэтому файл класса может содержать два таких метода, просто невозможно создать такой файл класса с использованием обычного Java-компилятора. Там хороший пример/объяснение в этом ответе.

Ответ 4

  • GOTO может использоваться с метками для создания собственных структур управления (кроме for while и т.д.)
  • Вы можете переопределить локальную переменную this внутри метода
  • Объединяя оба из них, вы можете создать оптимизированный байт-код создания хвостового вызова (я делаю это в JCompilo)

В качестве связанной точки вы можете получить имя параметра для методов, если скомпилировано с помощью debug (Paranamer делает это, читая байт-код

Ответ 5

Может быть, раздел 7A в этот документ представляет интерес, хотя он касается байт-кода ловушек, а не байт-кода функции.

Ответ 6

В языке Java первый оператор в конструкторе должен быть вызовом конструктора суперкласса. Байт-код не имеет этого ограничения, вместо этого правило заключается в том, что конструктор супер-класса или другой конструктор в том же классе должен быть вызван для объекта перед доступом к членам. Это должно позволить больше свободы, например:

  • Создайте экземпляр другого объекта, сохраните его в локальной переменной (или стеке) и передайте его как параметр конструктору суперкласса, сохраняя при этом ссылку в этой переменной для другого использования.
  • Вызов других конструкторов на основе условия. Это должно быть возможно: Как условно назвать другой конструктор на Java?

Я не тестировал их, поэтому, пожалуйста, исправьте меня, если я ошибаюсь.

Ответ 7

Что-то, что вы можете сделать с байтовым кодом, а не простым кодом Java, - это генерировать код, который может загружаться и запускаться без компилятора. Многие системы имеют JRE, а не JDK, и если вы хотите генерировать код динамически, может быть лучше, если не проще, генерировать байт-код вместо Java-кода, который должен быть скомпилирован до его использования.

Ответ 8

Я написал оптимизатор байт-кода, когда был I-Play (он был разработан для уменьшения размера кода для приложений J2ME). Одна из возможностей, которую я добавил, - это возможность использовать встроенный байт-код (аналогично встроенному ассемблеру на С++). Мне удалось уменьшить размер функции, которая была частью библиотечного метода, используя инструкцию DUP, так как мне нужно значение дважды. У меня также были инструкции с нулевым байтом (если вы вызываете метод, который принимает char, и вы хотите передать int, который, как вам известно, не нужно использовать, я добавил int2char (var) для замены char (var) и он удалит инструкцию i2c, чтобы уменьшить размер кода.Я также сделал это float a = 2.3; float b = 3.4; float c = a + b; и это будет преобразовано в неподвижную точку (быстрее, а также некоторые J2ME не поддерживали с плавающей запятой).