Преобразование lambdas в Java 8

Java 8, как представляется, создает классы для представления лямбда-выражений. Например, код:

  Runnable r = app::doStuff;

Манифест, грубо говоря, как:

  // $FF: synthetic class
  final class App$$Lambda$1 implements Runnable {
    private final App arg$1;

    private App$$Lambda$1(App var1) {
        this.arg$1 = var1;
    }

    private static Runnable get$Lambda(App var0) {
        return new App$$Lambda$1(var0);
    }

    public void run() {
        this.arg$1.doStuff();
    }
  }

Как я понимаю, код создается во время выполнения. Теперь предположим, что кто-то хотел ввести код в метод run указанного класса. Эксперименты до сих пор дают смесь NoClassDefFound и VerifyError:

java.lang.NoClassDefFoundError: App$$Lambda$2
    at App$$Lambda$2/1329552164.run(Unknown Source)
    at App.main(App.java:9)
Caused by: java.lang.ClassNotFoundException: App$$Lambda$2
    at java.net.URLClassLoader.findClass(URLClassLoader.java:381)
    at java.lang.ClassLoader.loadClass(ClassLoader.java:424)
    at sun.misc.Launcher$AppClassLoader.loadClass(Launcher.java:331)
    at java.lang.ClassLoader.loadClass(ClassLoader.java:357)
    ... 2 more

Это работает против:

$ java -version
java version "1.8.0_51"
Java(TM) SE Runtime Environment (build 1.8.0_51-b16)
Java HotSpot(TM) 64-Bit Server VM (build 25.51-b03, mixed mode)

Это даже до нажатия любого нового байт-кода в класс.

Ожидается ли это? Пахнет как ошибка JDK, но я рад, что ошибаюсь!

Здесь репозиторий Github, иллюстрирующий поведение

Ответ 1

Для меня это похоже на ошибку в JVM. Системный загрузчик системы пытается найти преобразованный класс по его имени. Однако лямбда-выражения загружаются через загрузку анонимного класса, где выполняется следующее условие:

clazz.getClassLoader()
     .loadClass(clazz.getName().substring(0, clazz.getName().indexOf('/')))

дает a ClassNotFoundException, что приводит к NoClassDefError. Класс не считается реальным классом, и такие анонимные классы, например, не передаются в ClassFileTransformer вне ретрансляции.

В целом, API-интерфейс инструментария чувствует себя немного ошибкой при работе с анонимными классами. Точно так же LambdaForm передаются ClassFileTransformer, но со всеми аргументами, но classFileBuffer установлен на null, что нарушает контракт класса трансформатора.

Для вашего примера проблема заключается в том, что вы возвращаете null; проблема исчезает, когда возвращается classFileBuffer то, что нет-op. Это не то, что предлагает ClassFileTransformer, где возвращение null является рекомендуемым способом:

правильно сформированный буфер файла класса (результат преобразования) или null, если преобразование не выполняется.

Для меня это похоже на ошибку в HotSpot. Вы должны сообщить об этой проблеме в OpenJDK.

В целом, вполне возможно использовать анонимно загруженные классы, как я демонстрирую в своей библиотеке манипулирования кодом Byte Buddy. Это требует некоторых неудачных настроек по сравнению с обычными инструментами, но среда выполнения поддерживает его. Вот пример, который успешно работает как unit test в библиотеке:

Callable<String> lambda = () -> "foo";

Instrumentation instrumentation = ByteBuddyAgent.install();
ClassReloadingStrategy classReloadingStrategy = ClassReloadingStrategy.of(instrumentation)
    .preregistered(lambda.getClass());
ClassFileLocator classFileLocator = ClassFileLocator.AgentBased.of(instrumentation, 
     lambda.getClass());

assertThat(lambda.call(), is("foo"));

new ByteBuddy()
  .redefine(lambda.getClass(), classFileLocator)
  .method(named("call"))
  .intercept(FixedValue.value("bar"))
  .make()
  .load(lambda.getClass().getClassLoader(), classReloadingStrategy);

assertThat(lambda.call(), is("bar"));

Ответ 2

Подача ошибок была принята людьми в Oracle и отслеживается как JDK-8145964. Это не совсем решение, но, по-видимому, это реальная проблема времени выполнения.