Lambda表达式的序列化和反序列化

15

下面这段代码会抛出异常:

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 /编译器的错误还是我对lambda的序列化和反序列化有所遗漏?


1
似乎是缓存错误?如果你交换主函数中的前两行,强制转换异常就会反转。 - Jorn Vernee
2
这里还有一个小问题:如果您在main中的序列化代码之前添加ToLongFunction<C> cc = (ToLongFunction<C>&Serializable) A :: value; cc.applyAsLong(new C());,则会在 fn1.applyAsLong 行上出现CCE。这似乎与@davidxxx的缓存建议一致。 - Andy Turner
3个回答

9

请看2016年提出的Open JDK问题:

Lambda反序列化导致ClassCastException

它非常精确地匹配了你的情况:

两个不同的类B和C,都扩展了同一个基类A,该基类有一个方法String f()。 为类型B的对象创建对方法f()的Supplier引用;将其称为bf [new B()::f]。 为类型C的对象创建对方法f()的Supplier引用;将其称为cf [new C()::f]。 序列化cf(ObjectOutputStream#writeObject)。 当反序列化cf(ObjectInputStream#readObject)时,抛出ClassCastException,指示类C无法转换为类B。 关于此问题有一个有趣的讨论,但最后一条评论由Dan Smith似乎解决了问题。

针对这个特定的测试案例,有一个重要的观察结果:方法引用的“限定类型”(即字节码命名的类)应该与调用的“限定类型”(即接收器的类型)相同。javac 错误地使用了声明类的类型。请参见JDK-8059632

修复该错误后,我认为不同的捕获类型问题就会消失。


虽然Dan Smith的观点是正确的,即修复JDK-8059632也将解决这个特定的例子,但是在仅修复JDK-8059632时仍可能构造其他失败的示例。 - Holger

0

我不知道如何解释,但是这种行为很特别,因为只使用一次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() 返回的对象与第一次调用返回的是同一个引用。
就好像第一个被缓存并在第二次调用中重复使用了。

看起来像是一个 bug。


但我注意到一个意外的事情,这很有趣,因为传递的不是同一个实例。在序列化之前,System.err.println(t) 显示它们是不同的。 - Andy Turner

0

如果你想让它工作,那么从 BaseClass 接口中删除 value() 的默认实现,并在实现类中重写它,就像这样:

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;
    }
}

2
一种更不具侵入性的解决方法是用 lambda 表达式替换方法引用。 - Marko Topolnik

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接