Uploaded image for project: 'JDK'
  1. JDK
  2. JDK-8161257

Java8 Lambda Deserialization - ClassCastException

    Details

    • Subcomponent:
    • CPU:
      generic
    • OS:
      generic

      Description

      FULL PRODUCT VERSION :
      javac 1.8.0_91
      java version "1.8.0_91"
      Java(TM) SE Runtime Environment (build 1.8.0_91-b14)
      Java HotSpot(TM) 64-Bit Server VM (build 25.91-b14, mixed mode)

      ADDITIONAL OS VERSION INFORMATION :
      Mac OS X Darwin Kernel Version 14.5.0: Thu Apr 21 20:40:54 PDT 2016; root:xnu-2782.50.3~1/RELEASE_X86_64 x86_64

      A DESCRIPTION OF THE PROBLEM :
      ClassCastException is thrown by Java8 upon deserializing a lambda when following conditions are met:
      - Parent class has a method, reference to which is used to automatically create a Serializable lambda
      - There are several child classes that extend it and there are several usages of above method as a method reference, but with different child classes
      - After method reference is consumed it is serialized and the deserialized
      - All method references are used within the same capturing class

      Please see attached code and actual output for more details.

      Also I have made some additional analysis. 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");
      }

      We observe that LambdaSerializationTest$AbstractConverter is used as implClass instead of concrete LambdaSerializationTest$ConverterX. So all 3 lambdas will satisfy 1st if condition and ConverterA is always assumed.

      STEPS TO FOLLOW TO REPRODUCE THE PROBLEM :
      Please run the attached test case and observe the output.

      EXPECTED VERSUS ACTUAL BEHAVIOR :
      EXPECTED -
      Output:
      1.8.0_91
      test_A
      test_B
      test_C
      ACTUAL -
      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

      ERROR MESSAGES/STACK TRACES THAT OCCUR :
      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

      REPRODUCIBILITY :
      This bug can be reproduced always.

      ---------- BEGIN SOURCE ----------
      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);
              }
          }

      }
      ---------- END SOURCE ----------

      CUSTOMER SUBMITTED WORKAROUND :
      There are following workarounds available:
      - Do not use such method references in same capturing class, but rather spread it by different (possibly inner) capturing classes.
      - Do not use method references as actual arguments, but use inner classes instead.



        Attachments

          Issue Links

            Activity

              People

              • Assignee:
                psandoz Paul Sandoz
                Reporter:
                webbuggrp Webbug Group
              • Votes:
                0 Vote for this issue
                Watchers:
                4 Start watching this issue

                Dates

                • Created:
                  Updated:
                  Resolved: