Сериализация и десериализация лямбда
Этот код ниже бросает
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;
}
}
Я надеюсь, что это помогает.