Inconsistent handling of lambda deserialization for Object method references on interfaces

XMLWordPrintable

    • Type: Bug
    • Resolution: Unresolved
    • Priority: P4
    • None
    • Affects Version/s: 27
    • Component/s: tools

      The handling of Object method references on interfaces in lambda serialization is inconsistent.

      Consider the following example, if method references are created for I1::hashCode and I2::hashCode:

      ```
        interface I1 extends Serializable {}
        interface I2 extends I1 {
          @Override
          public int hashCode();
        }
      ```

      Debugging shows the tests in the generated deserialization method aren't consistent with the runtime values in the SerializedLambda instance. Uncommenting the printf statements in [1] and comparing with the corresponding SerializedLambda getters at runtime show the following behaviour.

      [1] https://github.com/openjdk/jdk/blob/7c979c148724ab7de650593caa22df8405d740e5/src/jdk.compiler/share/classes/com/sun/tools/javac/comp/LambdaToMethod.java#L747-L754

      The deserialization method generates two cases for:

      *implMethodKind: 9
      *implClass: 'SerializableObjectMethods$I2'
      *implMethodName: 'hashCode'
      *implMethodSignature: '()I'

      and:

      *implMethodKind: '5'
      *implClass: 'java/lang/Object'
      *implMethodName: 'hashCode'
      *implMethodSignature: '()I'

      At runtime, both lambda's SerializedLambda forms both match the second case, and they get merged together:

      *implMethodKind: '5'
      *implClass: 'java/lang/Object'
      *implMethodName: 'hashCode'
      *implMethodSignature: '()I'

      This bug is related to a number of other issues:

      JDK-8208752 (Calling a deserialized Lambda might fail with ClassCastException) also involves two lambdas with the same implementation method but a different getInstantiatedMethodType getting incorrectly merged together. I noticed this while investigating that bug, because making the generated deserialize method test getInstantiatedMethodType regresses handling of object methods on interfaces, due to this issue.

      This behaviour involves logic [1] to handle object methods on interfaces in lambda serialization, which was added to fix JDK-8282080 (Lambda deserialization fails for Object method references on interfaces), which was a follow-on to JDK-8272564 (Incorrect attribution of method invocations of Object methods on interfaces).

      [1] https://github.com/openjdk/jdk/blame/7c979c148724ab7de650593caa22df8405d740e5/src/jdk.compiler/share/classes/com/sun/tools/javac/comp/LambdaToMethod.java#L705-L712

      One way to make the deserialization method consistent with the runtime SerializedLambdas would be to have both use the original referenced class (e.g. I1 and I2 instead of Object), but that is JDK-8172817, which is blocked on JDK-8068253.

      Another potentially less disruptive change to resolve this specific issue until JDK-8172817 happens, would be to update the logic added to LambdaToMethod to be consistent about using declaring classes for Object methods on interfaces, instead of using referenced classes for interfaces that redeclare the Object methods. That would effectively do the same JVMS 5.4.3.3 resolution that is done at runtime, but specifically for the case of Object methods on interfaces.

      ---

      Repro:

      ```
      import java.io.ByteArrayInputStream;
      import java.io.ByteArrayOutputStream;
      import java.io.ObjectInputStream;
      import java.io.ObjectOutputStream;
      import java.lang.invoke.MethodHandles;
      import java.lang.invoke.MethodType;
      import java.lang.invoke.SerializedLambda;

      public class SerialTester {

        private final MethodHandles.Lookup lookup;

        protected SerialTester(MethodHandles.Lookup lookup) {
          this.lookup = lookup;
        }

        @SuppressWarnings("unchecked")
        public <T> T serialDeserial(T object) {
          try {
            ByteArrayOutputStream baos = new ByteArrayOutputStream();
            ObjectOutputStream oos = new ObjectOutputStream(baos);
            oos.writeObject(object);
            oos.close();
            ByteArrayInputStream bais = new ByteArrayInputStream(baos.toByteArray());
            ObjectInputStream ois = new ObjectInputStream(bais);
            T result = (T) ois.readObject();
            ois.close();
            return result;
          } catch (Exception e) {
            throw new AssertionError(e);
          }
        }

        public String formatSerializedLambda(Object value) {
          SerializedLambda sl;
          try {
            sl =
                (SerializedLambda)
                    lookup
                        .findVirtual(
                            value.getClass(), "writeReplace", MethodType.methodType(Object.class))
                        .invoke(value);
          } catch (Throwable e) {
            throw new LinkageError(e.getMessage(), e);
          }
          return """
          *functionalInterfaceClass: '%s'
          *functionalInterfaceMethodName: '%s'
          *functionalInterfaceMethodSignature: '%s'
          *implMethodKind: '%s'
          *implClass: '%s'
          *implMethodName: '%s'
          *implMethodSignature: '%s'
          *instantiatedMethodType: '%s'
          """
              .formatted(
                  sl.getFunctionalInterfaceClass(),
                  sl.getFunctionalInterfaceMethodName(),
                  sl.getFunctionalInterfaceMethodSignature(),
                  sl.getImplMethodKind(),
                  sl.getImplClass(),
                  sl.getImplMethodName(),
                  sl.getImplMethodSignature(),
                  sl.getInstantiatedMethodType());
        }
      }
      ```

      ```
      import java.io.Serializable;
      import java.lang.invoke.MethodHandles;

      public class SerializableObjectMethods extends SerialTester {

        SerializableObjectMethods() {
          super(MethodHandles.lookup());
        }

        interface I1 extends Serializable {}

        interface I2 extends I1 {

          @Override
          public int hashCode();
        }

        interface F<T, R> extends Serializable {

          R apply(T t);
        }

        public static void main(String[] args) throws Exception {
          new SerializableObjectMethods().run();
        }

        void run() throws Exception {
          test((F<I1, Integer>) I1::hashCode, new I1() {});
          test((F<I2, Integer>) I2::hashCode, new I2() {});
        }

        <T> void test(F<T, ?> f, T arg) throws Exception {
          System.err.println("*** before ***");
          System.err.println(formatSerializedLambda(f));
          f = serialDeserial(f);
          System.err.println("*** after ***");
          System.err.println(formatSerializedLambda(f));
        }
      }
      ```

      # Running with the printf debugging in LambdaToMethod enabled shows that two deserialization cases are generated, and only one is ever taken, because the impl* state isn't consistent between the deserialization method and the runtime SerializedLambdas for I2:

      $ java SerializableObjectMethods.java
      +++++++++++++++++
      *functionalInterfaceClass: 'SerializableObjectMethods$F'
      *functionalInterfaceMethodName: 'apply'
      *functionalInterfaceMethodSignature: '(Ljava/lang/Object;)Ljava/lang/Object;'
      *implMethodKind: 5
      *implClass: 'java/lang/Object'
      *implMethodName: 'hashCode'
      *implMethodSignature: '()I'
      +++++++++++++++++
      *functionalInterfaceClass: 'SerializableObjectMethods$F'
      *functionalInterfaceMethodName: 'apply'
      *functionalInterfaceMethodSignature: '(Ljava/lang/Object;)Ljava/lang/Object;'
      *implMethodKind: 9
      *implClass: 'SerializableObjectMethods$I2'
      *implMethodName: 'hashCode'
      *implMethodSignature: '()I'
      *** before ***
      *functionalInterfaceClass: 'SerializableObjectMethods$F'
      *functionalInterfaceMethodName: 'apply'
      *functionalInterfaceMethodSignature: '(Ljava/lang/Object;)Ljava/lang/Object;'
      *implMethodKind: '5'
      *implClass: 'java/lang/Object'
      *implMethodName: 'hashCode'
      *implMethodSignature: '()I'
      *instantiatedMethodType: '(LSerializableObjectMethods$I1;)Ljava/lang/Integer;'

      *** after ***
      *functionalInterfaceClass: 'SerializableObjectMethods$F'
      *functionalInterfaceMethodName: 'apply'
      *functionalInterfaceMethodSignature: '(Ljava/lang/Object;)Ljava/lang/Object;'
      *implMethodKind: '5'
      *implClass: 'java/lang/Object'
      *implMethodName: 'hashCode'
      *implMethodSignature: '()I'
      *instantiatedMethodType: '(LSerializableObjectMethods$I1;)Ljava/lang/Integer;'

      *** before ***
      *functionalInterfaceClass: 'SerializableObjectMethods$F'
      *functionalInterfaceMethodName: 'apply'
      *functionalInterfaceMethodSignature: '(Ljava/lang/Object;)Ljava/lang/Object;'
      *implMethodKind: '5'
      *implClass: 'java/lang/Object'
      *implMethodName: 'hashCode'
      *implMethodSignature: '()I'
      *instantiatedMethodType: '(LSerializableObjectMethods$I2;)Ljava/lang/Integer;'

      *** after ***
      *functionalInterfaceClass: 'SerializableObjectMethods$F'
      *functionalInterfaceMethodName: 'apply'
      *functionalInterfaceMethodSignature: '(Ljava/lang/Object;)Ljava/lang/Object;'
      *implMethodKind: '5'
      *implClass: 'java/lang/Object'
      *implMethodName: 'hashCode'
      *implMethodSignature: '()I'
      *instantiatedMethodType: '(LSerializableObjectMethods$I1;)Ljava/lang/Integer;'

            Assignee:
            Liam Miller-Cushon
            Reporter:
            Liam Miller-Cushon
            Votes:
            0 Vote for this issue
            Watchers:
            2 Start watching this issue

              Created:
              Updated: