Подтвердить что ты не робот

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

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

(Comparator<ObjectNode> & Serializable) SOME_COMPARATOR::compare

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

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

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

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

4b9b3361

Ответ 1

Начало работы

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

import java.io.Serializable;
import java.util.Comparator;

public final class Generic {

    // Bad implementation, only used as an example.
    public static final Comparator<Integer> COMPARATOR = (a, b) -> (a > b) ? 1 : -1;

    public static Comparator<Integer> reference() {
        return (Comparator<Integer> & Serializable) COMPARATOR::compare;
    }

    public static Comparator<Integer> explicit() {
        return (Comparator<Integer> & Serializable) (a, b) -> COMPARATOR.compare(a, b);
    }

}

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

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

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

  public static final Comparator<Integer> COMPARATOR;    

  public static Comparator<Integer> 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<Integer> 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.

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

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

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

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

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

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

Методы реализации

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

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

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

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

Важной частью этого является "захваченные аргументы". Оглядываясь на дизассемблированный байт-код, invokedynamic инструкция для ссылки на метод, безусловно, выглядит как захват компаратора (#0:compare:(LComparator;)LComparator;, в отличие от явной лямбда, #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)

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

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

  public static Comparator<Integer> 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$explicit$d34e1a25$1, которая, в свою очередь, вызывает COMPARATOR#compare. Этот слой косвенности означает, что он не захватывает ничего, что не является Serializable (или что-то вообще, если быть точным), и поэтому безопасно сериализовать. В ссылочном выражении метода напрямую используется COMPARATOR (значение которого затем передается методу начальной загрузки):

  public static Comparator<Integer> 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<Integer> COMPARATOR = (Serializable & Comparator<Integer>) (a, b) -> a > b ? 1 : -1;

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

Ответ 2

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

SOME_COMPARATOR::compare

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

(a,b) → SOME_COMPARATOR.compare(a,b)

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

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

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

Object o = "First";

void run() {
    Supplier<String> ref = o::toString; 
    Supplier<String> 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 reference кода, где getClass вызывается для значения SOME_COMPARATOR которое вызовет исключение, если оно равно NULL:

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

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

Ссылки на методы в коде, который соответствует Eclipse 4.4.1, однако, не вызывают исключения в этой ситуации. В Eclipse, похоже, есть ошибка.