ClassCastException is thrown by Java8 upon deserializing a lambda when following conditions are met:
Serializable lambdaTested on Oracle Java compiler and runtime versions 1.8.0_91. Please find test code on how to reproduce:
import java.io.*;
/**
 * @author Max Myslyvtsev
 * @since 7/6/16
 */
public class LambdaSerializationTest implements Serializable {
    static abstract class AbstractConverter implements Serializable {
        String convert(String input) {
            return doConvert(input);
        }
        abstract String doConvert(String input);
    }
    static class ConverterA extends AbstractConverter {
        @Override
        String doConvert(String input) {
            return input + "_A";
        }
    }
    static class ConverterB extends AbstractConverter {
        @Override
        String doConvert(String input) {
            return input + "_B";
        }
    }
    static class ConverterC extends AbstractConverter {
        @Override
        String doConvert(String input) {
            return input + "_C";
        }
    }
    interface MyFunction<T, R> extends Serializable {
        R call(T var);
    }
    public static void main(String[] args) throws Exception {
        System.out.println(System.getProperty("java.version"));
        ConverterA converterA = new ConverterA();
        ConverterB converterB = new ConverterB();
        ConverterC converterC = new ConverterC();
        giveFunction(converterA::convert);
        giveFunction(converterB::convert);
        giveFunction(converterC::convert);
    }
    private static void giveFunction(MyFunction<String, String> f) {
        f = serializeDeserialize(f);
        System.out.println(f.call("test"));
    }
    private static <T> T serializeDeserialize(T object) {
        try {
            ByteArrayOutputStream baos = new ByteArrayOutputStream();
            ObjectOutputStream oos = new ObjectOutputStream(baos);
            oos.writeObject(object);
            byte[] bytes = baos.toByteArray();
            ByteArrayInputStream bais = new ByteArrayInputStream(bytes);
            ObjectInputStream ois = new ObjectInputStream(bais);
            @SuppressWarnings("unchecked")
            T result = (T) ois.readObject();
            return result;
        } catch (Exception e) {
            throw new RuntimeException(e);
        }
    }
}
It gives following output:
1.8.0_91
test_A
Exception in thread "main" java.lang.RuntimeException: java.io.IOException: unexpected exception type
    at LambdaSerializationTest.serializeDeserialize(LambdaSerializationTest.java:68)
    at LambdaSerializationTest.giveFunction(LambdaSerializationTest.java:52)
    at LambdaSerializationTest.main(LambdaSerializationTest.java:47)
    at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
    at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)
    at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
    at java.lang.reflect.Method.invoke(Method.java:498)
    at com.intellij.rt.execution.application.AppMain.main(AppMain.java:144)
Caused by: java.io.IOException: unexpected exception type
    at java.io.ObjectStreamClass.throwMiscException(ObjectStreamClass.java:1582)
    at java.io.ObjectStreamClass.invokeReadResolve(ObjectStreamClass.java:1154)
    at java.io.ObjectInputStream.readOrdinaryObject(ObjectInputStream.java:1817)
    at java.io.ObjectInputStream.readObject0(ObjectInputStream.java:1353)
    at java.io.ObjectInputStream.readObject(ObjectInputStream.java:373)
    at LambdaSerializationTest.serializeDeserialize(LambdaSerializationTest.java:65)
    ... 7 more
Caused by: java.lang.reflect.InvocationTargetException
    at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
    at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)
    at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
    at java.lang.reflect.Method.invoke(Method.java:498)
    at java.lang.invoke.SerializedLambda.readResolve(SerializedLambda.java:230)
    at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
    at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)
    at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
    at java.lang.reflect.Method.invoke(Method.java:498)
    at java.io.ObjectStreamClass.invokeReadResolve(ObjectStreamClass.java:1148)
    ... 11 more
Caused by: java.lang.ClassCastException: LambdaSerializationTest$ConverterB cannot be cast to LambdaSerializationTest$ConverterA
    at LambdaSerializationTest.$deserializeLambda$(LambdaSerializationTest.java:7)
    ... 21 more
Upon decompiling this $deserializeLambda$ method with CFR following code is revealed:
private static /* synthetic */ Object $deserializeLambda$(SerializedLambda lambda) {
    switch (lambda.getImplMethodName()) {
        case "convert": {
            if (lambda.getImplMethodKind() == 5 && lambda.getFunctionalInterfaceClass().equals("LambdaSerializationTest$MyFunction") && lambda.getFunctionalInterfaceMethodName().equals("call") && lambda.getFunctionalInterfaceMethodSignature().equals("(Ljava/lang/Object;)Ljava/lang/Object;") && lambda.getImplClass().equals("LambdaSerializationTest$AbstractConverter") && lambda.getImplMethodSignature().equals("(Ljava/lang/String;)Ljava/lang/String;")) {
                return (MyFunction<String, String>)LambdaMetafactory.altMetafactory(null, null, null, (Ljava/lang/Object;)Ljava/lang/Object;, convert(java.lang.String ), (Ljava/lang/String;)Ljava/lang/String;)((ConverterA)((ConverterA)lambda.getCapturedArg(0)));
            }
            if (lambda.getImplMethodKind() == 5 && lambda.getFunctionalInterfaceClass().equals("LambdaSerializationTest$MyFunction") && lambda.getFunctionalInterfaceMethodName().equals("call") && lambda.getFunctionalInterfaceMethodSignature().equals("(Ljava/lang/Object;)Ljava/lang/Object;") && lambda.getImplClass().equals("LambdaSerializationTest$AbstractConverter") && lambda.getImplMethodSignature().equals("(Ljava/lang/String;)Ljava/lang/String;")) {
                return (MyFunction<String, String>)LambdaMetafactory.altMetafactory(null, null, null, (Ljava/lang/Object;)Ljava/lang/Object;, convert(java.lang.String ), (Ljava/lang/String;)Ljava/lang/String;)((ConverterB)((ConverterB)lambda.getCapturedArg(0)));
            }
            if (lambda.getImplMethodKind() != 5 || !lambda.getFunctionalInterfaceClass().equals("LambdaSerializationTest$MyFunction") || !lambda.getFunctionalInterfaceMethodName().equals("call") || !lambda.getFunctionalInterfaceMethodSignature().equals("(Ljava/lang/Object;)Ljava/lang/Object;") || !lambda.getImplClass().equals("LambdaSerializationTest$AbstractConverter") || !lambda.getImplMethodSignature().equals("(Ljava/lang/String;)Ljava/lang/String;")) break;
            return (MyFunction<String, String>)LambdaMetafactory.altMetafactory(null, null, null, (Ljava/lang/Object;)Ljava/lang/Object;, convert(java.lang.String ), (Ljava/lang/String;)Ljava/lang/String;)((ConverterC)((ConverterC)lambda.getCapturedArg(0)));
        }
    }
    throw new IllegalArgumentException("Invalid lambda deserialization");
}
So it appears that actual captured argument is not used to determine which exact lambda has to be deserialized. All 3 lambdas will satisfy 1st if condition and ConverterA will be assumed.
When debugging we can observe that in runtime lambda.getCapturedArg(0) is of a correct type (ConverterB when exception is thrown) and also it worth noting that cast is not needed since method to be invoked is present in base AbstractConverter class.
Is it expected behavior? If yes, what is recommended workaround?
Oracle has confirmed that it is a bug and assigned a following Bug ID: JDK-8161257
It is now visible on the official tracker: JDK-8161257
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With