ClassCastException: cannot assign SerializedLambda to field with ClassLoader

Peter Levart peter.levart at gmail.com
Tue Mar 26 08:06:36 UTC 2019


As I have suspected, this code works (see that there's no custom class 
loaders involved as they are unimportant in this situation):

package test;

import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.io.Serializable;

public class SerializedLambdaTest implements Runnable {
     public void run() { new MyCode().run(); }

     public interface SerializableRunnable extends Runnable, Serializable {
     }

     public static class MyCode implements SerializableRunnable {
         Object runnable2 = (SerializableRunnable)
             () -> System.out.println("HELLO" + this.getClass());

         private byte[] serialize(Object o) {
             ByteArrayOutputStream baos;
             try (
                 ObjectOutputStream oos
                     = new ObjectOutputStream(baos = new
                     ByteArrayOutputStream())) {
                 oos.writeObject(o);
             } catch (IOException e) {
                 throw new RuntimeException(e);
             }
             return baos.toByteArray();
         }

         private <T> T deserialize(byte[] bytes) {
             try (
                 ObjectInputStream ois
                     = new ObjectInputStream(new
ByteArrayInputStream(bytes))) {
                 return (T) ois.readObject();
             } catch (IOException | ClassNotFoundException e) {
                 throw new RuntimeException(e);
             }
         }

         @Override
         public void run() {
             System.out.println("                this: " + this);
             SerializableRunnable deSerializedThis
                 = deserialize(serialize(this));
             System.out.println("    deSerializedThis: "
                                + deSerializedThis);

             SerializableRunnable runnable = (SerializableRunnable) 
runnable2;
             System.out.println("            runnable: " + runnable);
             SerializableRunnable deSerializedRunnable
                 = deserialize(serialize(runnable));
             System.out.println("deSerializedRunnable: "
                                + deSerializedRunnable);
         }
     }

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



...the original code failed with a very descriptive message:

Exception in thread "main" java.lang.ClassCastException: cannot assign 
instance of java.lang.invoke.SerializedLambda to field 
test.SerializedLambdaTest$MyCode.runnable2 of type 
test.SerializedLambdaTest$SerializableRunnable in instance of 
test.SerializedLambdaTest$MyCode
     at 
java.base/java.io.ObjectStreamClass$FieldReflector.setObjFieldValues(ObjectStreamClass.java:2190)
     at 
java.base/java.io.ObjectStreamClass$FieldReflector.checkObjectFieldValueTypes(ObjectStreamClass.java:2153)
     at 
java.base/java.io.ObjectStreamClass.checkObjFieldValueTypes(ObjectStreamClass.java:1407)
     at 
java.base/java.io.ObjectInputStream.defaultCheckFieldValues(ObjectInputStream.java:2371)
     at 
java.base/java.io.ObjectInputStream.readSerialData(ObjectInputStream.java:2278)
     ...


which suggests that my claims are correct...

Peter

On 3/26/19 8:54 AM, Peter Levart wrote:
> Hi Seth,
>
> I think you stumbled on a "defect" of Java Serialization mechanism. 
> Namely, the inability for it to correctly deserialize object graphs 
> containing a cycle when an object in such cycle uses the 
> "readResolve()" special method to replace the deserialized object with 
> a replacement object. SerializedLambda is a class with instances that 
> are 1st deserialized from stream then "replaced" with real lambda 
> objects via the "readResolve()" method which constructs and returns 
> the "real" lambda object. The deserialized SerializedLambda object is 
> nevertheless attempted to be (temporarily) assigned to the field that 
> would hold the final lambda object, which fails as the filed is of 
> different type than SerializedLambda (or supertype).
>
> You can check my claims by changing the type of the field holding the 
> lambda to java.lang.Object and see if it works... (It should if my 
> memory serves me well :-)
>
> This is a known defect of Java serialization and is not specific to 
> serialized lambda.
>
> There were some unsuccessful attempts to fix it during (re)discovery 
> when lambdas were being added to Java.
>
> Regards, Peter
>
> On 3/26/19 5:03 AM, seth lytle wrote:
>> it's a nested class, but it's loaded with the same class loader as the
>> toplevel.
>> which is what i thought you thought was the problem when
>>
>> you wrote:
>>> this test loads a nested type into a different classloader to its
>> enclosing type
>>
>> and:
>>> it is a requirement that all nest members be defined in the same
>>> package - which implies the same classloader
>>
>>
>>
>> On Mon, Mar 25, 2019 at 11:40 PM David Holmes <david.holmes at oracle.com>
>> wrote:
>>
>>> On 26/03/2019 1:06 pm, seth lytle wrote:
>>>> i still get the same stack trace after breaking the example out 
>>>> into two
>>>> files. does that sufficiently address the nesting issue ?
>>> ??
>>>
>>>       public static class MyCode ...
>>>
>>> is still a nested class.
>>>
>>> David
>>>
>>>> file: SerializedLambdaTest
>>>> package test;
>>>>
>>>> import java.io.ByteArrayInputStream;
>>>> import java.io.ByteArrayOutputStream;
>>>> import java.io.IOException;
>>>> import java.io.ObjectInputStream;
>>>> import java.io.ObjectOutputStream;
>>>> import java.io.Serializable;
>>>>
>>>> // from: https://bugs.openjdk.java.net/browse/JDK-8008770
>>>> public class SerializedLambdaTest implements Runnable {
>>>>       public void run() { new MyCode().run(); }
>>>>       public interface SerializableRunnable extends 
>>>> Runnable,Serializable
>>> {
>>>>       }
>>>>       public static class MyCode implements SerializableRunnable {
>>>>           SerializableRunnable runnable2 = ()
>>>>                   -> {
>>>>               System.out.println("HELLO"+this.getClass());
>>>>           };
>>>>           private byte[] serialize(Object o) {
>>>>               ByteArrayOutputStream baos;
>>>>               try (
>>>>                        ObjectOutputStream oos
>>>>                       = new ObjectOutputStream(baos = new
>>>> ByteArrayOutputStream())) {
>>>>                   oos.writeObject(o);
>>>>               } catch (IOException e) {
>>>>                   throw new RuntimeException(e);
>>>>               }
>>>>               return baos.toByteArray();
>>>>           }
>>>>           private <T> T deserialize(byte[] bytes) {
>>>>               try (
>>>>                        ObjectInputStream ois
>>>>                       = new ObjectInputStream(new
>>>> ByteArrayInputStream(bytes))) {
>>>>                   return (T) ois.readObject();
>>>>               } catch (IOException|ClassNotFoundException e) {
>>>>                   throw new RuntimeException(e);
>>>>               }
>>>>           }
>>>>           @Override
>>>>           public void run() {
>>>>               System.out.println("                this: "+this);
>>>>               SerializableRunnable deSerializedThis
>>>>                       = deserialize(serialize(this));
>>>>               System.out.println("    deSerializedThis: "
>>>>                       +deSerializedThis);
>>>>
>>>>               SerializableRunnable runnable = runnable2;
>>>>               System.out.println("            runnable: "+runnable);
>>>>               SerializableRunnable deSerializedRunnable
>>>>                       = deserialize(serialize(runnable));
>>>>               System.out.println("deSerializedRunnable: "
>>>>                       +deSerializedRunnable);
>>>>           }
>>>>       }
>>>> }
>>>>
>>>>
>>>> file: MyClassLoader
>>>> package test;
>>>>
>>>> import java.io.IOException;
>>>> import java.io.InputStream;
>>>>
>>>> class MyClassLoader extends ClassLoader {
>>>>       MyClassLoader(ClassLoader parent) {
>>>>           super(parent);
>>>>       }
>>>>       @Override
>>>>       protected Class<?> loadClass(String name,boolean resolve) throws
>>>> ClassNotFoundException {
>>>>           if (name.startsWith("test."))
>>>>               synchronized (getClassLoadingLock(name)) {
>>>>                   Class<?> c = findLoadedClass(name);
>>>>                   if (c==null) c = findClass(name);
>>>>                   if (resolve) resolveClass(c);
>>>>                   return c;
>>>>               }
>>>>           else return super.loadClass(name,resolve);
>>>>       }
>>>>       @Override
>>>>       protected Class<?> findClass(String name) throws
>>>> ClassNotFoundException {
>>>>           String path = name.replace('.','/').concat(".class");
>>>>           try (final InputStream is = getResourceAsStream(path)) {
>>>>               if (is!=null) {
>>>>                   byte[] bytes = is.readAllBytes();
>>>>                   return defineClass(name,bytes,0,bytes.length);
>>>>               }
>>>>               else throw new ClassNotFoundException(name);
>>>>           } catch (IOException e) {
>>>>               throw new ClassNotFoundException(name,e);
>>>>           }
>>>>       }
>>>>       public static void main(String[] args) throws Exception {
>>>>           ClassLoader myCl = new MyClassLoader(
>>>>                   MyClassLoader.class.getClassLoader()
>>>>           );
>>>>           Class<?> myCodeClass = Class.forName(
>>>>                   "test.SerializedLambdaTest",
>>>>                   true,
>>>>                   myCl
>>>>           );
>>>>           Runnable myCode = (Runnable) myCodeClass.newInstance();
>>>>           myCode.run();
>>>>       }
>>>> }
>>>>
>>>>
>>>>
>>>>
>>>> On Mon, Mar 25, 2019 at 10:34 PM David Holmes <david.holmes at oracle.com
>>>> <mailto:david.holmes at oracle.com>> wrote:
>>>>
>>>>      Hi Seth,
>>>>
>>>>      On 26/03/2019 12:16 pm, seth lytle wrote:
>>>>       > i haven't changed the assignment from the original test case
>>>>      (which was
>>>>       > accepted as valid at the time). i haven't gone through it in
>>>>      depth, but
>>>>       > assume that it's ok and used it since people are already 
>>>> familiar
>>>>      with
>>>>       > it. it appears to me that that example uses reflection to
>>>>      allocate an
>>>>       > instance using a definitive class loader and then uses a 
>>>> runnable
>>>>      for
>>>>       > the serialization/deserialization cycle
>>>>       >
>>>>       > the class cast exception happens not at the example level, but
>>> deep
>>>>       > inside `readObject` when it's doing an internal assignment
>>>>       >
>>>>       > perhaps my choice of words "into a new classloader" was
>>>>      insufficient or
>>>>       > misleading. in both examples that i've come up with so far, 
>>>> the
>>>>      class
>>>>       > loader calls defineClass directly without first delegating to
>>>>      super (for
>>>>       > a subset of the classes). so "into a definitive classloader"
>>>>      might have
>>>>       > been a better choice
>>>>
>>>>      I think the basic problem is that this test loads a nested 
>>>> type into
>>> a
>>>>      different classloader to its enclosing type. I can easily imagine
>>> this
>>>>      messing up the code that gets generated to implement the lambda
>>>>      expression - but I'd need to examine that code in detail to see
>>> exactly
>>>>      why (others may know this more readily than I do). It's 
>>>> unclear to me
>>>>      whether this would be considered a bug or a "don't do that"
>>> situation.
>>>>      As of JDK 11, nested types define "nests" at the VM level (see
>>> JEP-181)
>>>>      and it is a requirement that all nest members be defined in 
>>>> the same
>>>>      package - which implies the same classloader.
>>>>
>>>>      David
>>>>
>>>>       >
>>>>       >
>>>>       >
>>>>       >
>>>>       >
>>>>       > On Mon, Mar 25, 2019 at 9:34 PM David Holmes
>>>>      <david.holmes at oracle.com <mailto:david.holmes at oracle.com>
>>>>       > <mailto:david.holmes at oracle.com
>>>>      <mailto:david.holmes at oracle.com>>> wrote:
>>>>       >
>>>>       >     Hi Seth,
>>>>       >
>>>>       >     On 26/03/2019 11:22 am, seth lytle wrote:
>>>>       >      > if a lambda is a field and captures `this`, and it's
>>>>      deserialized
>>>>       >     into a
>>>>       >      > new class loader, it throws a ClassCastException.
>>>>       >
>>>>       >     Not sure I follow. If you load a class into a different
>>>>      classloader
>>>>       >     then
>>>>       >     you get a different type. It might appear the same to 
>>>> you but
>>>>      it is a
>>>>       >     distinct type, so you can't assign across different 
>>>> instances
>>>>      loaded by
>>>>       >     different classloaders.
>>>>       >
>>>>       >     Cheers,
>>>>       >     David
>>>>       >     -----
>>>>       >
>>>>       >      > i came across this bug
>>>>       >      > independently while writing a test, but then found 
>>>> an old
>>>>      openjdk
>>>>       >     bug with
>>>>       >      > a similar issue and tweaked the test case (source 
>>>> below).
>>>>      my version
>>>>       >      > differs only in
>>>>       >      > 1. moved the lambda to a field
>>>>       >      > 2. reference `this` in it
>>>>       >      > 3. uses readAllBytes instead of the internal method
>>>>       >      > 4. formatting
>>>>       >      >
>>>>       >      > running with java 11 or 12 (or java 8 using a 
>>>> substitute
>>> for
>>>>       >     readAllBytes)
>>>>       >      > results in:
>>>>       >      >                  this:
>>>>      test.SerializedLambdaTest$MyCode at 8efb846
>>>>       >      >      deSerializedThis:
>>>>      test.SerializedLambdaTest$MyCode at 2b71fc7e
>>>>       >      >              runnable:
>>>>       >      >
>>>>       >
>>>>
>>> test.SerializedLambdaTest$MyCode$$Lambda$1/0x0000000801188440 at 37bba400
>>>>       >      > Exception in thread "main" 
>>>> java.lang.ClassCastException:
>>>>      cannot
>>>>       >     assign
>>>>       >      > instance of java.lang.invoke.SerializedLambda to field
>>>>       >      > test.SerializedLambdaTest$MyCode.runnable2 of type
>>>>       >      > test.SerializedLambdaTest$SerializableRunnable in 
>>>> instance
>>> of
>>>>       >      > test.SerializedLambdaTest$MyCode
>>>>       >      > at
>>>>       >      > java.base/java.io <http://java.io>
>>>>       >
>>>>        <http://java.io
>>>> .ObjectStreamClass$FieldReflector.setObjFieldValues(ObjectStreamClass.java:2190) 
>>>>
>>>>       >      > at
>>>>       >      > java.base/java.io <http://java.io>
>>>>       >
>>>>        <http://java.io
>>>> .ObjectStreamClass$FieldReflector.checkObjectFieldValueTypes(ObjectStreamClass.java:2153) 
>>>>
>>>>       >      > at
>>>>       >      > java.base/java.io <http://java.io>
>>>>       >
>>>>        <http://java.io
>>>> .ObjectStreamClass.checkObjFieldValueTypes(ObjectStreamClass.java:1407) 
>>>>
>>>>       >      > at
>>>>       >      > java.base/java.io <http://java.io>
>>>>       >
>>>>        <http://java.io
>>>> .ObjectInputStream.defaultCheckFieldValues(ObjectInputStream.java:2371) 
>>>>
>>>>       >      > at
>>>>       >      > java.base/java.io <http://java.io>
>>>>       >
>>>>        <http://java.io
>>>> .ObjectInputStream.readSerialData(ObjectInputStream.java:2278)
>>>>       >      > at
>>>>       >      > java.base/java.io <http://java.io>
>>>>       >
>>>>        <http://java.io
>>>> .ObjectInputStream.readOrdinaryObject(ObjectInputStream.java:2087)
>>>>       >      > at
>>>>       >      > java.base/java.io <http://java.io>
>>>>       >
>>>>        <http://java.io
>>>> .ObjectInputStream.readObject0(ObjectInputStream.java:1594)
>>>>       >      > at
>>>>       >      > java.base/java.io <http://java.io>
>>>>       >
>>>>        <http://java.io
>>>> .ObjectInputStream.readArray(ObjectInputStream.java:1993)
>>>>       >      > at
>>>>       >      > java.base/java.io <http://java.io>
>>>>       >
>>>>        <http://java.io
>>>> .ObjectInputStream.readObject0(ObjectInputStream.java:1588)
>>>>       >      > at
>>>>       >      > java.base/java.io <http://java.io>
>>>>       >
>>>>        <http://java.io
>>>> .ObjectInputStream.defaultReadFields(ObjectInputStream.java:2355)
>>>>       >      > at
>>>>       >      > java.base/java.io <http://java.io>
>>>>       >
>>>>        <http://java.io
>>>> .ObjectInputStream.readSerialData(ObjectInputStream.java:2249)
>>>>       >      > at
>>>>       >      > java.base/java.io <http://java.io>
>>>>       >
>>>>        <http://java.io
>>>> .ObjectInputStream.readOrdinaryObject(ObjectInputStream.java:2087)
>>>>       >      > at
>>>>       >      > java.base/java.io <http://java.io>
>>>>       >
>>>>        <http://java.io
>>>> .ObjectInputStream.readObject0(ObjectInputStream.java:1594)
>>>>       >      > at
>>>>       >      > java.base/java.io <http://java.io>
>>>>       >
>>>>        <http://java.io
>>>> .ObjectInputStream.readObject(ObjectInputStream.java:430)
>>>>       >      > at
>>>>       >      >
>>>>       >
>>>>
>>> test.SerializedLambdaTest$MyCode.deserialize(SerializedLambdaTest.java:35)
>>>>       >      > at
>>>> test.SerializedLambdaTest$MyCode.run(SerializedLambdaTest.java:51)
>>>>       >      > at
>>>> test.SerializedLambdaTest.main(SerializedLambdaTest.java:66)
>>>>       >      >
>>>>       >      >
>>>>       >      >
>>>>       >      > https://bugs.openjdk.java.net/browse/JDK-8008770
>>>>       >      >
>>>>       >      >
>>>>       >      > package test;
>>>>       >      >
>>>>       >      > import java.io.ByteArrayInputStream;
>>>>       >      > import java.io.ByteArrayOutputStream;
>>>>       >      > import java.io.IOException;
>>>>       >      > import java.io.InputStream;
>>>>       >      > import java.io.ObjectInputStream;
>>>>       >      > import java.io.ObjectOutputStream;
>>>>       >      > import java.io.Serializable;
>>>>       >      >
>>>>       >      > // from: 
>>>> https://bugs.openjdk.java.net/browse/JDK-8008770
>>>>       >      > public class SerializedLambdaTest {
>>>>       >      >      public interface SerializableRunnable extends
>>>>       >     Runnable,Serializable {
>>>>       >      >      }
>>>>       >      >      public static class MyCode implements
>>>>      SerializableRunnable {
>>>>       >      >          SerializableRunnable runnable2 = ()
>>>>       >      >                  -> {
>>>>       >      > System.out.println("HELLO"+this.getClass());
>>>>       >      >          };
>>>>       >      >          private byte[] serialize(Object o) {
>>>>       >      >              ByteArrayOutputStream baos;
>>>>       >      >              try (
>>>>       >      > ObjectOutputStream oos
>>>>       >      >                      = new ObjectOutputStream(baos = 
>>>> new
>>>>       >      > ByteArrayOutputStream())) {
>>>>       >      >                  oos.writeObject(o);
>>>>       >      >              } catch (IOException e) {
>>>>       >      >                  throw new RuntimeException(e);
>>>>       >      >              }
>>>>       >      >              return baos.toByteArray();
>>>>       >      >          }
>>>>       >      >          private <T> T deserialize(byte[] bytes) {
>>>>       >      >              try (
>>>>       >      >                       ObjectInputStream ois
>>>>       >      >                      = new ObjectInputStream(new
>>>>       >      > ByteArrayInputStream(bytes))) {
>>>>       >      >                  return (T) ois.readObject();
>>>>       >      >              } catch 
>>>> (IOException|ClassNotFoundException
>>> e) {
>>>>       >      > throw new RuntimeException(e);
>>>>       >      >              }
>>>>       >      >          }
>>>>       >      >          @Override
>>>>       >      >          public void run() {
>>>>       >      > System.out.println("                this:
>>>>      "+this);
>>>>       >      >              SerializableRunnable deSerializedThis
>>>>       >      >                      = deserialize(serialize(this));
>>>>       >      >              System.out.println(" deSerializedThis: "
>>>>       >      > +deSerializedThis);
>>>>       >      >
>>>>       >      >              SerializableRunnable runnable = runnable2;
>>>>       >      > System.out.println("            runnable:
>>>>      "+runnable);
>>>>       >      >              SerializableRunnable deSerializedRunnable
>>>>       >      >                      = 
>>>> deserialize(serialize(runnable));
>>>>       >      > System.out.println("deSerializedRunnable: "
>>>>       >      > +deSerializedRunnable);
>>>>       >      >          }
>>>>       >      >      }
>>>>       >      >      public static void main(String[] args) throws
>>> Exception {
>>>>       >      > ClassLoader myCl = new MyClassLoader(
>>>>       >      >
>>> SerializedLambdaTest.class.getClassLoader()
>>>>       >      >          );
>>>>       >      >          Class<?> myCodeClass = Class.forName(
>>>>       >      >
>>>>      SerializedLambdaTest.class.getName()+"$MyCode",
>>>>       >      >                  true,
>>>>       >      >                  myCl
>>>>       >      >          );
>>>>       >      >          Runnable myCode = (Runnable)
>>>>      myCodeClass.newInstance();
>>>>       >      >          myCode.run();
>>>>       >      >      }
>>>>       >      >      static class MyClassLoader extends ClassLoader {
>>>>       >      >          MyClassLoader(ClassLoader parent) {
>>>>       >      >              super(parent);
>>>>       >      >          }
>>>>       >      >          @Override
>>>>       >      >          protected Class<?> loadClass(String 
>>>> name,boolean
>>>>      resolve)
>>>>       >      >                  throws ClassNotFoundException {
>>>>       >      >              if (name.startsWith("test."))
>>>>       >      >                  synchronized 
>>>> (getClassLoadingLock(name)) {
>>>>       >      >                      Class<?> c = 
>>>> findLoadedClass(name);
>>>>       >      >                      if (c==null)
>>>>       >      >                          c = findClass(name);
>>>>       >      >                      if (resolve)
>>>>       >      > resolveClass(c);
>>>>       >      >                      return c;
>>>>       >      >                  }
>>>>       >      >              else
>>>>       >      >                  return super.loadClass(name,resolve);
>>>>       >      >          }
>>>>       >      >          @Override
>>>>       >      >          protected Class<?> findClass(String name) 
>>>> throws
>>>>       >      >                  ClassNotFoundException {
>>>>       >      >              String path =
>>>>      name.replace('.','/').concat(".class");
>>>>       >      >              try ( InputStream is =
>>>>      getResourceAsStream(path)) {
>>>>       >      >                  if (is!=null) {
>>>>       >      >                      byte[] bytes = is.readAllBytes();
>>>>       >      >                      return
>>>>      defineClass(name,bytes,0,bytes.length);
>>>>       >      >                  } else
>>>>       >      >                      throw new
>>> ClassNotFoundException(name);
>>>>       >      >              } catch (IOException e) {
>>>>       >      >                  throw new 
>>>> ClassNotFoundException(name,e);
>>>>       >      >              }
>>>>       >      >          }
>>>>       >      >      }
>>>>       >      > }
>>>>       >      >
>>>>       >
>>>>
>



More information about the core-libs-dev mailing list