JDK-8024931 : Serialization of object with writeReplace and cyclic reference
  • Type: Bug
  • Component: core-libs
  • Sub-Component: java.io:serialization
  • Affected Version: 1.4.2,8
  • Priority: P4
  • Status: Open
  • Resolution: Unresolved
  • OS: linux
  • Submitted: 2013-09-17
  • Updated: 2018-09-11
The Version table provides details related to the release that this issue/RFE will be addressed.

Unresolved : Release in which this issue/RFE will be addressed.
Resolved: Release in which this issue/RFE has been resolved.
Fixed : Release in which this issue/RFE has been fixed. The release containing this fix may be available for download as an Early Access Release or a General Availability Release.

To download the current JDK release, click here.
Other
tbdUnresolved
Related Reports
Relates :  
Relates :  
Description
FULL PRODUCT VERSION :
1.8.0-ea-b106

A DESCRIPTION OF THE PROBLEM :
A lambda that captures a value that refers back to that lambda cannot be properly deserialized. This is caused by the usage of readResolve() in java.lang.invoke.SerializedLambda.


ACTUAL -
The cyclic reference is not resolved, an instance of java.lang.invoke.SerializedLambda is used where a lambda is expected.

ERROR MESSAGES/STACK TRACES THAT OCCUR :
first example:
java.lang.ClassCastException: cannot assign instance of java.lang.invoke.SerializedLambda to field SerialLambda.supplier of type SerialLambda$SerializableIntSupplier in instance of SerialLambda
at java.io.ObjectStreamClass$FieldReflector.setObjFieldValues(ObjectStreamClass.java:2089)
...
at java.io.ObjectInputStream.readObject(ObjectInputStream.java:371)
at SerialLambda.serialCopy(SerialLambda.java:32)
at SerialLambda.go(SerialLambda.java:19)

second example:
Exception in thread "main" java.lang.ClassCastException: java.lang.invoke.SerializedLambda cannot be cast to SerialLambda$SerializableIntSupplier
at SerialLambda.lambda$go2$9b75$0(SerialLambda.java:24)
at SerialLambda$$Lambda$2.getAsInt(Unknown Source)
at SerialLambda.go2(SerialLambda.java:26)

REPRODUCIBILITY :
This bug can be reproduced always.

---------- BEGIN SOURCE ----------
import java.io.*;
import java.util.*;
import java.util.function.IntSupplier;

/** @author Wouter Coekaerts */
public class SerialLambda implements Serializable {
  interface SerializableIntSupplier extends IntSupplier, Serializable {}

  public static void main(String[] args) throws Exception {
    new SerialLambda().go();
    go2();
  }

  SerializableIntSupplier supplier;

  void go() throws Exception {
    supplier = this::hashCode;
    System.out.println(serialCopy(this).supplier.getAsInt());
    System.out.println(serialCopy(this.supplier).getAsInt());
  }

  static void go2() throws Exception {
    List<SerializableIntSupplier> list = new ArrayList<>();
    list.add(() -> list.get(0).hashCode());
    System.out.println(serialCopy(list).get(0).getAsInt());
    System.out.println(serialCopy(list.get(0)).getAsInt());
  }

  @SuppressWarnings("unchecked")
  static <T> T serialCopy(T o) throws Exception {
    ByteArrayOutputStream ba = new ByteArrayOutputStream();
    new ObjectOutputStream(ba).writeObject(o);
    return (T) new ObjectInputStream(new ByteArrayInputStream(ba.toByteArray())).readObject();
  }
}
---------- END SOURCE ----------
Comments
Reduced to P4 and removed fix-version of 8. This is a long-standing problem and the linked bugs are also P4.
16-10-2013

Seems related to JDK-6208166 and JDK-4957674, which both deal with the serialization mechanism's inability to deal with certain cases involving circularity.
07-10-2013

What we have is a serializable object X which holds a reference to another serializable object Y, where Y is serialized through writeReplace/readResolve, and intermediate/replaced version of Y holds a reference to X. If we serialize and deserialize X, everything works fine. But if we only serialize/deserialize Y, serialization tries to write the intermediate result back to the field of X.
07-10-2013

This is not a lambda-specific issue. I've reproduced this with no lambda involved in the following TestNG test case, and it gets the same CCE: import java.io.ByteArrayInputStream; import java.io.ByteArrayOutputStream; import java.io.ObjectInputStream; import java.io.ObjectOutputStream; import java.io.Serializable; import java.util.function.IntSupplier; import org.testng.annotations.Test; import static org.testng.Assert.assertEquals; /** * SerTest * * @author Brian Goetz */ @Test public class SerTest { public void testHasher() throws Exception { Hasher h = new Hasher(new Integer(3)); assertEquals(3, h.getAsInt()); assertEquals(3, serialCopy(h).getAsInt()); } public void testHolder() throws Exception { Hasher h = new Hasher(new Integer(4)); HasherHolder hh = new HasherHolder(); hh.supplier = h; assertEquals(4, hh.supplier.getAsInt()); assertEquals(4, serialCopy(hh).supplier.getAsInt()); assertEquals(4, serialCopy(hh.supplier).getAsInt()); } public void testHolderSelfRef() throws Exception { HasherHolder hh = new HasherHolder(); int result = hh.hashCode(); Hasher h = new Hasher(hh); hh.supplier = h; serialCopy(hh).supplier.getAsInt(); serialCopy(hh.supplier).getAsInt(); } @SuppressWarnings("unchecked") static <T> T serialCopy(T o) throws Exception { ByteArrayOutputStream ba = new ByteArrayOutputStream(); ObjectOutputStream oos = new ObjectOutputStream(ba); oos.writeObject(o); oos.close(); byte[] bytes = ba.toByteArray(); return (T) new ObjectInputStream(new ByteArrayInputStream(bytes)).readObject(); } } class HasherHolder implements Serializable { IntSupplier supplier; } class Hasher implements Serializable, IntSupplier { private final Object o; Hasher(Object o) { this.o = o; } @Override public int getAsInt() { return o.hashCode(); } private Object writeReplace() { return new HasherSerRep(o); } private static class HasherSerRep implements Serializable { private final Object o; private HasherSerRep(Object o) { this.o = o; } private Object readResolve() { return new Hasher(o); } } }
07-10-2013