В чем разница между привязкой лямбды и метода на уровне выполнения?

Я испытал проблему, которая происходила с использованием ссылки на метод, но не с lambdas. Этот код был следующим:

(Comparator & Serializable) SOME_COMPARATOR::compare 

или, с лямбдой,

 (Comparator & Serializable) (a,b) -> SOME_COMPARATOR.compare(a,b) 

Семантически это строго одно и то же, но на практике оно отличается, поскольку в первом случае я получаю исключение в одном из classов сериализации Java. Мой вопрос заключается не в этом исключении, потому что фактический код работает в более сложном контексте, который оказался странным поведением с сериализацией, поэтому было бы слишком сложно ответить, если бы я дал более подробную информацию.

То, что я хочу понять, – это различие между этими двумя способами создания lambda-выражения.

Начиная

Чтобы исследовать это, мы начинаем со следующего classа:

 import java.io.Serializable; import java.util.Comparator; public final class Generic { // Bad implementation, only used as an example. public static final Comparator COMPARATOR = (a, b) -> (a > b) ? 1 : -1; public static Comparator reference() { return (Comparator & Serializable) COMPARATOR::compare; } public static Comparator explicit() { return (Comparator & Serializable) (a, b) -> COMPARATOR.compare(a, b); } } 

После компиляции мы можем разобрать его, используя:

javap -c -p -s -v Generic.class

Удаляя ненужные части (и некоторые другие беспорядки, такие как полностью квалифицированные типы и инициализация COMPARATOR ), мы остаемся с

  public static final Comparator COMPARATOR; public static Comparator reference(); 0: getstatic #2 // Field COMPARATOR:LComparator; 3: dup 4: invokevirtual #3 // Method Object.getClass:()LClass; 7: pop 8: invokedynamic #4, 0 // InvokeDynamic #0:compare:(LComparator;)LComparator; 13: checkcast #5 // class Serializable 16: checkcast #6 // class Comparator 19: areturn public static Comparator explicit(); 0: invokedynamic #7, 0 // InvokeDynamic #1:compare:()LComparator; 5: checkcast #5 // class Serializable 8: checkcast #6 // class Comparator 11: areturn private static int lambda$explicit$d34e1a25$1(Integer, Integer); 0: getstatic #2 // Field COMPARATOR:LComparator; 3: aload_0 4: aload_1 5: invokeinterface #44, 3 // InterfaceMethod Comparator.compare:(LObject;LObject;)I 10: ireturn BootstrapMethods: 0: #61 invokestatic invoke/LambdaMetafactory.altMetafactory:(Linvoke/MethodHandles$Lookup;LString;Linvoke/MethodType;[LObject;)Linvoke/CallSite; Method arguments: #62 (LObject;LObject;)I #63 invokeinterface Comparator.compare:(LObject;LObject;)I #64 (LInteger;LInteger;)I #65 5 #66 0 1: #61 invokestatic invoke/LambdaMetafactory.altMetafactory:(Linvoke/MethodHandles$Lookup;LString;Linvoke/MethodType;[LObject;)Linvoke/CallSite; Method arguments: #62 (LObject;LObject;)I #70 invokestatic Generic.lambda$explicit$df5d232f$1:(LInteger;LInteger;)I #64 (LInteger;LInteger;)I #65 5 #66 0 

Сразу же мы видим, что байт-код для метода reference() отличается от байт-кода для explicit() . Однако заметная разница на самом деле не актуальна , но методы бутстрапа интересны.

Сайт invokedynamic call связан с методом с помощью метода начальной загрузки , который является методом, заданным компилятором для динамически типизированного языка, который один раз вызывает JVM для связывания сайта.

( Поддержка виртуальной машины Java для языков , отличных от Java , подчеркивает их)

Это код, отвечающий за создание CallSite, используемого лямбдой . Method arguments перечисленные ниже, каждый метод бутстрапа – это значения, переданные в качестве вариационного параметра (например, args ) LambdaMetaFactory # altMetaFactory .

Формат аргументов метода

  1. samMethodType – тип подписи и возврата метода, который должен быть реализован объектом функции.
  2. implMethod – дескриптор прямого метода, описывающий метод реализации, который должен быть вызван (с подходящей адаптацией типов аргументов, возвращаемых типов и захваченных аргументов, предшествующих аргументам вызова) во время вызова.
  3. instantiatedMethodType – Тип подписи и возврата, который должен выполняться динамически во время вызова. Это может быть то же самое, что и samMethodType, или может быть специализацией.
  4. flags указывает дополнительные параметры; это побитовое ИЛИ желаемых флагов. Определенные флаги: FLAG_BRIDGES, FLAG_MARKERS и FLAG_SERIALIZABLE.
  5. bridgeCount – это количество дополнительных сигнатур методов, которые должен реализовывать объект функции, и присутствует тогда и только тогда, когда установлен флаг FLAG_BRIDGES.

В обоих случаях здесь bridgeCount равен 0, и поэтому нет 6, которые в противном случае были бы bridges – список дополнительных сигнатур дополнительных переменных для реализации (учитывая, что bridgeCount равен 0, я не совсем уверен, почему установлен FLAG_BRIDGES) ,

Сопоставляя вышеприведенные аргументы, получаем:

  1. Подписи типа и возвращаемого значения (Ljava/lang/Object;Ljava/lang/Object;)I , который является типом возвращаемого значения сравнения Comparator # , из-за стирания общего типа.
  2. Вызывается метод, когда эта lambda вызывается (это другое).
  3. Тип подписи и возврата lambda, который будет проверяться при вызове lambda: (LInteger;LInteger;)I (обратите внимание, что они не стираются, потому что это часть спецификации lambda).
  4. Флаги, которые в обоих случаях представляют собой состав FLAG_BRIDGES и FLAG_SERIALIZABLE (т.е. 5).
  5. Количество сигнатур мостового метода, 0.

Мы можем видеть, что FLAG_SERIALIZABLE установлен для обоих lambdas, так что это не так.

Методы внедрения

Метод реализации для ссылки на метод lambda – Comparator.compare:(LObject;LObject;)I , но для явной лямбды это Generic.lambda$explicit$df5d232f$1:(LInteger;LInteger;)I Рассматривая parsingку, мы видим, что первая является, по существу, встроенной версией последней. Единственная другая заметная разница – это типы параметров метода (которые, как упоминалось ранее, объясняются стиранием типового типа).

Когда lambda действительно сериализуема?

Вы можете сериализовать lambda-выражение, если его целевой тип и его захваченные аргументы сериализуемы.

Лямбда-выражения (Учебники Java ™)

Важная часть этого – «захваченные аргументы». Оглядываясь назад на дизассемблированный байт-код, invokedynamic инструкция для ссылки на метод, конечно, выглядит так, будто он захватывает Comparator ( #0:compare:(LComparator;)LComparator; в отличие от явного lambda, #1:compare:()LComparator; ).

Подтверждение захвата – проблема

ObjectOutputStream содержит поле extendedDebugInfo , которое мы можем установить с помощью -Dsun.io.serialization.extendedDebugInfo=true VM:

$ java -Dsun.io.serialization.extendedDebugInfo = true Общий

Когда мы пытаемся снова сериализовать лямбды, это дает очень

 Exception in thread "main" java.io.NotSerializableException: Generic$$Lambda$1/321001045 - element of array (index: 0) - array (class "[LObject;", size: 1) /* ! */ - field (class "invoke.SerializedLambda", name: "capturedArgs", type: "class [LObject;") // <--- !! - root object (class "invoke.SerializedLambda", SerializedLambda[capturingClass=class Generic, functionalInterfaceMethod=Comparator.compare:(LObject;LObject;)I, implementation=invokeInterface Comparator.compare:(LObject;LObject;)I, instantiatedMethodType=(LInteger;LInteger;)I, numCaptured=1]) at java.io.ObjectOutputStream.writeObject0(ObjectOutputStream.java:1182) /* removed */ at java.io.ObjectOutputStream.writeObject(ObjectOutputStream.java:348) at Generic.main(Generic.java:27) 

Что на самом деле происходит

Из вышесказанного видно, что явная lambda не захватывает ничего, в то время как эталонная lambda метода. Еще раз взглянув на байт-код, это ясно:

  public static Comparator explicit(); 0: invokedynamic #7, 0 // InvokeDynamic #1:compare:()LComparator; 5: checkcast #5 // class java/io/Serializable 8: checkcast #6 // class Comparator 11: areturn 

Что, как видно выше, имеет способ реализации:

  private static int lambda$explicit$d34e1a25$1(java.lang.Integer, java.lang.Integer); 0: getstatic #2 // Field COMPARATOR:Ljava/util/Comparator; 3: aload_0 4: aload_1 5: invokeinterface #44, 3 // InterfaceMethod java/util/Comparator.compare:(Ljava/lang/Object;Ljava/lang/Object;)I 10: ireturn 

Явная lambda на самом деле вызывает lambda$explicit$d34e1a25$1 , что, в свою очередь, вызывает COMPARATOR#compare lambda$explicit$d34e1a25$1 COMPARATOR#compare . Этот слой косвенности означает, что он не захватывает ничего, что не является Serializable (или что-то вообще, если быть точным), и поэтому безопасно сериализовать. В ссылочном выражении метода используется COMPARATOR (значение которого затем передается методу начальной загрузки):

  public static Comparator reference(); 0: getstatic #2 // Field COMPARATOR:LComparator; 3: dup 4: invokevirtual #3 // Method Object.getClass:()LClass; 7: pop 8: invokedynamic #4, 0 // InvokeDynamic #0:compare:(LComparator;)LComparator; 13: checkcast #5 // class java/io/Serializable 16: checkcast #6 // class Comparator 19: areturn 

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

Исправить

Я стесняюсь называть это ошибкой компилятора (я ожидаю, что отсутствие косвенности служит оптимизацией), хотя это очень странно. Исправление тривиально, но уродливое; добавление явного броска для COMPARATOR при объявлении:

 public static final Comparator COMPARATOR = (Serializable & Comparator) (a, b) -> a > b ? 1 : -1; 

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

Я хочу добавить тот факт, что на самом деле существует семантическая разница между привязкой лямбды и метода к методу экземпляра (даже если у них такой же контент, как в вашем случае, и без учета сериализации):

SOME_COMPARATOR::compare

Эта форма оценивается lambda-объектом, который закрывается над значением SOME_COMPARATOR во время оценки (то есть содержит ссылку на этот объект). Он будет проверять, является ли SOME_COMPARATOR нулю во время оценки и затем SOME_COMPARATOR исключение нулевого указателя. Он не будет получать изменения в поле, созданное после его создания.

(a,b) -> SOME_COMPARATOR.compare(a,b)

Эта форма оценивается lambda-объектом, который будет получать доступ к значению поля SOME_COMPARATOR при вызове . Он закрыт над this , так как SOME_COMPARATOR является полем экземпляра. При вызове он будет получать доступ к текущему значению SOME_COMPARATOR и использовать его, потенциально бросая исключение нулевого указателя в это время.

демонстрация

Такое поведение можно увидеть из следующего небольшого примера. Остановив код в отладчике и проверив поля lambda, можно проверить, что они закрыты.

 Object o = "First"; void run() { Supplier ref = o::toString; Supplier lambda = () -> o.toString(); o = "Second"; System.out.println("Ref: " + ref.get()); // Prints "First" System.out.println("Lambda: " + lambda.get()); // Prints "Second" } 

Спецификация языка Java

JLS описывает это поведение ссылок на методы в 15.13.3 :

Целевой ссылкой является значение ExpressionName или Primary, определяемое при оценке ссылочного выражения метода.

А также:

Во-первых, если ссылочное выражение метода начинается с ExpressionName или Primary, это подвыражение оценивается. Если подвыражение оценивается как null , возникает NullPointerException

В коде Tobys

Это можно увидеть в списке цитирования Tobys, где getClass вызывается на значение SOME_COMPARATOR которое вызывает исключение, если оно равно null:

 4: invokevirtual #3 // Method Object.getClass:()LClass; 

(Или, я думаю, я действительно не специалист по байтовому коду.)

Однако ссылки на методы в коде, который соответствует Eclipse 4.4.1, не генерируют исключение в этой ситуации. Кажется, что у Eclipse есть ошибка.

  • Сериализация OpenCV Mat_
  • Использование JsonConvert.DeserializeObject для десериализации Json в class C # POCO
  • Невозможно сериализовать словарь с помощью сложного ключа с помощью Json.net
  • Существуют ли эквиваленты C ++ для функций протокола ввода-вывода протокола Buffer в Java?
  • Почему, когда конструктор аннотируется с помощью @JsonCreator, его аргументы должны быть аннотированы с помощью @JsonProperty?
  • Сериализация / десериализация пользовательской коллекции с дополнительными свойствами с помощью Json.Net
  • Как использовать пользовательский Serializer с Jackson?
  • Преобразование записей в последовательную форму данных для отправки по HTTP
  • Jackson ObjectMapper - указать порядок сортировки свойств объекта
  • Можете ли вы определить, был ли объект, десериализованный объект, отсутствием поля с classом JsonConvert в Json.NET
  • Json.Net: свойство Serialize / Deserialize в качестве значения, а не как объект
  • Давайте будем гением компьютера.