ClassCastException is thrown by Java8 when lambda is deserialized under the following conditions:
- The parent class has a method that is referenced to automatically create a
Serializable lambda - There are several child classes that extend it, and there are several ways to use the above method as a method reference, but with different child classes
- After using the method, the link is serialized and deserialized.
- All method references are used in the same capture class.
Tested with Oracle Java compiler and version 1.8.0_91. Please find a test code on how to reproduce:
import java.io.*; 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); } } }
He gives the following conclusion:
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
After decompiling this method $deserializeLambda$ with CFR, the following code will appear:
private static 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 seems that the actual captured argument is not used to determine which lambda should be deserialized. All 3 lambdas will satisfy the 1st if condition, and ConverterA will be accepted.
During debugging, we can notice that at runtime lambda.getCapturedArg(0) is of the correct type ( ConverterB when an exception is thrown), and it is worth noting that the cast is not required, since the method to be called is present in the AbstractConverter base class.
Is behavior expected? If so, what is the recommended workaround?