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

(de)serialization circular references with HashSets fail in .hashCode()

    Details

      Description

      ADDITIONAL SYSTEM INFORMATION :
      Same issue with Java 1.8.0_151 and 11.0.1+13. I'm running the test from IntelliJ:

      IntelliJ IDEA 2019.1 (Ultimate Edition)
      Build #IU-191.6183.87, built on March 27, 2019
      JRE: 1.8.0_202-release-1483-b39 amd64
      JVM: OpenJDK 64-Bit Server VM by JetBrains s.r.o
      Linux 4.15.0-47-generic

      A DESCRIPTION OF THE PROBLEM :
      ```java
      public static class TestClass implements Serializable {

          String name;
          Set<TestClass> others = new HashSet();

          TestClass(String n) { name = n; }


          @Override
          public int hashCode() {
              System.out.println("name=" + name);
              return name.hashCode();
          }

          @Override
          public boolean equals(Object other) {
              return (other instanceof TestClass) &&
                     Objects.equals(this.name, ((TestClass) other).name);
          }

          private static final long serialVersionUID = 20141029195011L;
      }

      @Test
      public void testDeserialization() {
          TestClass alice = new TestClass("Alice");
          TestClass bob = new TestClass("Bob");
          alice.others.add(bob);
          bob.others.add(alice);

          assertEquals(alice, serializeDeserialize(alice));
      }
      ```
      Code for serializeDeserialize is pasted into code box below.

      Output
      ```
      name=Bob
      name=Alice
      name=null

      java.lang.NullPointerException
      at MyTest$TestClass.hashCode(ComparatorContractTest.java:69)
      at java.util.HashMap.hash(HashMap.java:339)
      at java.util.HashMap.put(HashMap.java:612)
      at java.util.HashSet.readObject(HashSet.java:342)
      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.invokeReadObject(ObjectStreamClass.java:1170)
      at java.io.ObjectInputStream.readSerialData(ObjectInputStream.java:2178)
      at java.io.ObjectInputStream.readOrdinaryObject(ObjectInputStream.java:2069)
      at java.io.ObjectInputStream.readObject0(ObjectInputStream.java:1573)
      at java.io.ObjectInputStream.defaultReadFields(ObjectInputStream.java:2287)
      at java.io.ObjectInputStream.readSerialData(ObjectInputStream.java:2211)
      at java.io.ObjectInputStream.readOrdinaryObject(ObjectInputStream.java:2069)
      at java.io.ObjectInputStream.readObject0(ObjectInputStream.java:1573)
      at java.io.ObjectInputStream.readObject(ObjectInputStream.java:431)
      at java.util.HashSet.readObject(HashSet.java:341)
      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.invokeReadObject(ObjectStreamClass.java:1170)
      at java.io.ObjectInputStream.readSerialData(ObjectInputStream.java:2178)
      at java.io.ObjectInputStream.readOrdinaryObject(ObjectInputStream.java:2069)
      at java.io.ObjectInputStream.readObject0(ObjectInputStream.java:1573)
      at java.io.ObjectInputStream.defaultReadFields(ObjectInputStream.java:2287)
      at java.io.ObjectInputStream.readSerialData(ObjectInputStream.java:2211)
      at java.io.ObjectInputStream.readOrdinaryObject(ObjectInputStream.java:2069)
      at java.io.ObjectInputStream.readObject0(ObjectInputStream.java:1573)
      at java.io.ObjectInputStream.readObject(ObjectInputStream.java:431)
      at org.organicdesign.testUtils.Serialization.serializeDeserialize(Serialization.java:38)
      at MyTest.testDeserialization(ComparatorContractTest.java:88)
      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 org.junit.runners.model.FrameworkMethod$1.runReflectiveCall(FrameworkMethod.java:50)
      at org.junit.internal.runners.model.ReflectiveCallable.run(ReflectiveCallable.java:12)
      at org.junit.runners.model.FrameworkMethod.invokeExplosively(FrameworkMethod.java:47)
      at org.junit.internal.runners.statements.InvokeMethod.evaluate(InvokeMethod.java:17)
      at org.junit.runners.ParentRunner.runLeaf(ParentRunner.java:325)
      at org.junit.runners.BlockJUnit4ClassRunner.runChild(BlockJUnit4ClassRunner.java:78)
      at org.junit.runners.BlockJUnit4ClassRunner.runChild(BlockJUnit4ClassRunner.java:57)
      at org.junit.runners.ParentRunner$3.run(ParentRunner.java:290)
      at org.junit.runners.ParentRunner$1.schedule(ParentRunner.java:71)
      at org.junit.runners.ParentRunner.runChildren(ParentRunner.java:288)
      at org.junit.runners.ParentRunner.access$000(ParentRunner.java:58)
      at org.junit.runners.ParentRunner$2.evaluate(ParentRunner.java:268)
      at org.junit.runners.ParentRunner.run(ParentRunner.java:363)
      at org.junit.runner.JUnitCore.run(JUnitCore.java:137)
      at com.intellij.junit4.JUnit4IdeaTestRunner.startRunnerWithArgs(JUnit4IdeaTestRunner.java:68)
      at com.intellij.rt.execution.junit.IdeaTestRunner$Repeater.startRunnerWithArgs(IdeaTestRunner.java:47)
      at com.intellij.rt.execution.junit.JUnitStarter.prepareStreamsAndStart(JUnitStarter.java:242)
      at com.intellij.rt.execution.junit.JUnitStarter.main(JUnitStarter.java:70)
      ```

      It looks like both objects are deserialized correctly (because they are printed out), but then something tries to deserialize one object a third time and it gets initialized with nulls (or is not initialized).

      STEPS TO FOLLOW TO REPRODUCE THE PROBLEM :
      Cut and paste the test in the description into a Java file and run it.

      EXPECTED VERSUS ACTUAL BEHAVIOR :
      EXPECTED -
      The test should pass that the objects match before and after serialization.
      ACTUAL -
      The objects are not deserialized due to the circular reference and possibly trying to read more objects than were serialized.

      ---------- BEGIN SOURCE ----------
      public static class TestClass implements Serializable {

          String name;
          Set<TestClass> others = new HashSet<>();

          TestClass(String n) { name = n; }


          @Override
          public int hashCode() {
              System.out.println("name=" + name);
              return name.hashCode();
          }

          @Override
          public boolean equals(Object other) {
              return (other instanceof TestClass) &&
                     Objects.equals(this.name, ((TestClass) other).name);
          }

          private static final long serialVersionUID = 20141029195011L;
      }

      @Test
      public void testDeserialization() {
          TestClass alice = new TestClass("Alice");
          TestClass bob = new TestClass("Bob");
          alice.others.add(bob);
          bob.others.add(alice);

          assertEquals(alice, serializeDeserialize(alice));
      }

      public static <T> T serializeDeserialize(T obj) {

          // This method was started by sblommers. Thanks for your help!
          // Mistakes are Glen's.
          // https://github.com/GlenKPeterson/Paguro/issues/10#issuecomment-242332099

          // Write
          ByteArrayOutputStream baos = new ByteArrayOutputStream();

          try {
              ObjectOutputStream oos = new ObjectOutputStream(baos);
              oos.writeObject(obj);

              final byte[] data = baos.toByteArray();

              // Read
              ByteArrayInputStream baip = new ByteArrayInputStream(data);
              ObjectInputStream ois = new ObjectInputStream(baip);
              return (T) ois.readObject();
          } catch (IOException | ClassNotFoundException e) {
              throw new RuntimeException(e);
          }
      }

      ---------- END SOURCE ----------

      CUSTOMER SUBMITTED WORKAROUND :
      I guess you could write your own serialization/deserialization or add a serialization proxy, or your own collection, or your own serialization library. Not really eager to do any of those.

      FREQUENCY : always


        Attachments

          Issue Links

            Activity

              People

              • Assignee:
                chegar Chris Hegarty
                Reporter:
                tongwan Andrew Wang
              • Votes:
                0 Vote for this issue
                Watchers:
                4 Start watching this issue

                Dates

                • Created:
                  Updated: