Сериализация и десериализация лямбда

Этот код ниже бросает

Exception in thread "main" java.lang.ClassCastException: test.Subclass2 cannot be cast to test.Subclass1
at test.LambdaTest.main(LambdaTest.java:17)

public class LambdaTest {

    public static void main(String[] args) throws IOException, ClassNotFoundException {
        ToLongFunction<B> fn1 = serde((ToLongFunction<B> & Serializable) B::value);
        ToLongFunction<C> fn2 = serde((ToLongFunction<C> & Serializable) C::value);
        fn1.applyAsLong(new B());
        fn2.applyAsLong(new C()); // Line 17 -- exception here!
    }

    private static <T extends Serializable> T serde(T t) throws IOException, ClassNotFoundException {
        ByteArrayOutputStream bos = new ByteArrayOutputStream();
        new ObjectOutputStream(bos).writeObject(t);
        ObjectInputStream ois = new ObjectInputStream(new ByteArrayInputStream(bos
                .toByteArray()));
        return (T) ois.readObject();
    }
}

class A {
    public long value() {
        return 0;
    }
}

class B extends A { }

class C extends A { }

Причина в том, что после сериализации и десериализации оба fn1 и fn2 оказываются в одном классе. Является ли это ошибкой JDK/компилятора или мне не хватает чего-то о сериализации и десериализации лямбда?

Ответ 1

Взгляните на этот вопрос Open JDK, поднятый еще в 2016 году:

Дезабилизация лямбда вызывает ClassCastException

Это вполне соответствует вашему сценарию:

  • Два (разных) класса, B и C, оба из которых расширяют один и тот же базовый класс A, который имеет метод String f().
  • Создайте ссылку Supplier для метода f() для объекта типа B; назовите это bf [ new B()::f ].
  • Создайте ссылку Supplier на метод f() для объекта типа C; cal this cf [ new C()::f ].
  • Сериализовать cf (ObjectOutputStream#writeObject)
  • Когда сериализованный cf десериализован (ObjectInputStream#readObject), ClassCastException говорящее, что класс C нельзя отнести к классу B

Там интересная дискуссия по этому вопросу, но последний комментарий Дэн Смит, похоже, прибил его:

Важное замечание для этого конкретного тестового примера: "квалификационный тип" (т.е. Класс, названный байт-кодом) ссылки на метод должен быть таким же, как и квалификационный тип вызова: тип получателя. javac неверно использовать тип объявляющего класса. См. JDK-8059632.

Исправьте эту ошибку, и я думаю, что проблема с различными захваченными типами исчезает.

Ответ 2

Я не знаю, как это объяснить, но поведение является особенным, потому что выполнение основного кода только с одним вызовом serde() работает для обоих случаев.

Вот так:

ToLongFunction<B> fn1 = serde((ToLongFunction<B> & Serializable) A::value);
fn1.applyAsLong(new B());

или это:

ToLongFunction<C> fn2 = serde((ToLongFunction<C> & Serializable) A::value);
fn2.applyAsLong(new C());

Хотя, когда два вызова выполняются в одной и той же программе, я замечаю ожидаемую вещь: значение параметра (T t) не совпадает для значения для двух случаев.
Но я замечаю неожиданную вещь: при втором вызове serde() объект, возвращаемый ois.readObject() является той же ссылкой, что и при первом ois.readObject().
Как будто первый был кэширован и повторно использован для второго вызова.

Кажется, это ошибка.

Ответ 3

Если вы хотите заставить его работать, удалите стандартную реализацию value() из интерфейса BaseClass и переопределите его в классах реализации следующим образом:

interface BaseClass {
    long value();
}
public class LambdaTest {

    public static void main(String[] args) throws IOException, ClassNotFoundException {
        ToLongFunction<Subclass1> fn1 = serDe((Serializable & ToLongFunction<Subclass1>) Subclass1::value);
        fn1.applyAsLong(new Subclass1());
        ToLongFunction<Subclass2> fn2 = serDe((Serializable & ToLongFunction<Subclass2>) Subclass2::value);
        fn2.applyAsLong(new Subclass2());
    }

    private static <T extends Serializable> void printType(T t) {
        System.out.println(t.getClass().getSimpleName());
    }

    private static <T extends Serializable> T serDe(T t) throws IOException, ClassNotFoundException {
        ByteArrayOutputStream bos = new ByteArrayOutputStream();
        new ObjectOutputStream(bos).writeObject(t);
        ObjectInputStream ois = new ObjectInputStream(new ByteArrayInputStream(bos.toByteArray()));
        T readT = (T) ois.readObject();
        ois.close();
        bos.close();
        return readT;
    }
}
class Subclass1 implements BaseClass {

    @Override
    public long value() {
        return 0;
    }
}
class Subclass2 implements BaseClass {

    @Override
    public long value() {
        return 1;
    }
}

Я надеюсь, что это помогает.