Blog-Archiv

Sonntag, 15. Juli 2018

Java Inner Class Serialization Gotcha

Inner classes were added to Java in version 1.1. Inner classes always occur inside the curly braces of an outer class, and can occur on unlimited nesting levels. Two different kinds exist:

  1. Static inner classes
  2. Non-static inner classes

This Blog is about the difference between these two, and the sometimes fatal consequences of serializing instances of a non-static inner class. I do not cover anonymous and local classes here.

The Difference

Static inner classes behave like most people would expect from an inner class. They do not require the existence of an outer "parent" object. They are like classes of a sub-package, but referenced through the class, and behave like normal classes, except that you can set access modifiers on them (private, protected, public, default), which is quite useful.

Such an object is constructed like the following:

public class StaticInnerClassExample
{
    private static class Cat
    {
    }
    
    public static void main(String[] args) {
        final Cat cat = new StaticInnerClassExample.Cat();
    }
}

So the construction happens by new OuterClass.InnerClass().

Non-static inner classes on the other hand require the existence of an outer "parent" object. Furthermore they also hold an invisible reference to their outer object, generated by the compiler. You can see this hidden reference in the debugger as "this$0".

The construction of a non-static inner object looks a little unusual, you must call new on the parent instance, new OuterClass().new InnerClass():

public class NonStaticInnerClassExample
{
    private class Cat
    {
    }
    
    public static void main(String[] args) {
        final Cat cat = new NonStaticInnerClassExample().new Cat();
    }
}

This makes the difference clear: the non-static always needs an outer object to come to life.

Java Default Serialization

What can we do with serialization? For example, in case all your classes implement Serializable, you could write the whole object graph of your application to a file before terminating it. On next startup you could load the application from that file. It then would be in exactly the same state as it was when you terminated it!

The following is about Java default serialization. Serialization of an object can be overridden by implementing readObject() and writeObject(), which I will not cover here.

Serialization reads and writes the values of the instance-fields of an object, called "the state of the object". Methods do not get serialized, they belong to the class.

If there is a reference to another object, also this object gets serialized, recursively. In case there is something in the resulting object-graph that doesn't implement the interface Serializable, a NotSerializableException will be thrown.

Serialization ignores static fields, and fields tagged as transient (a Java language keyword).

Mind that serializing instances of inner classes is generally discouraged due to compiler-specific mechanisms around it.

Gotcha

Now that we know about inner classes and serialization we can try out what happens when you serialize an object of a non-static inner class.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
public class CatPack implements Serializable
{
    public class Cat implements Serializable
    {
        private final String individualName;
        
        private Cat(String individualName) {
            this.individualName = individualName;
        }

        @Override
        public String toString() {
            return individualName+" from "+packName+", having: "+mateNames();
        }
    }
    
    private final String packName;
    private final Collection<Cat> pack = new HashSet<>();
    
    public CatPack(String packName) {
        this.packName = packName;
    }

    public Cat add(String individualName)  {
        final Cat individual = new Cat(individualName);
        pack.add(individual);
        return individual;
    }

    private String mateNames()  {
        return pack.stream()
                .map((Cat cat) -> cat.individualName)
                .reduce((String soFar, String next) -> soFar+", "+next)
                .orElse("");
    }

}

The CatPack class encapsulates its Cat members using a non-static inner class. The Cat.toString() implementation shows that non-static inner classes have access to private fields and methods of the outer class, like packName and mateNames(). The implicit pointer to the outer object provides that (static inner classes don't have such).

So far so good, but what happens when serializing a Cat instance? Will it still be able to enumerate the names of pack cats after? Here is the according test code:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
public class InnerClassSerializationGotcha
{
    public static void main(String[] args) throws Exception {
        final CatPack pack = new CatPack("Garfield Clan");
        pack.add("Garfield");
        final CatPack.Cat individual = pack.add("Catbert");
        
        final String expected = individual.toString();
        
        final ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
        final ObjectOutputStream objectOutputStream = new ObjectOutputStream(outputStream);
        objectOutputStream.writeObject(individual);
        objectOutputStream.close();
        
        final InputStream inputStream = new ByteArrayInputStream(outputStream.toByteArray());
        final ObjectInputStream objectInputStream = new ObjectInputStream(inputStream);
        final CatPack.Cat serializedIndividual = (CatPack.Cat) objectInputStream.readObject();
        
        final String result = serializedIndividual.toString();
        if (result.equals(expected) == false)
            throw new Exception("Error: instance of non-static inner class has been serialized without outer object!");
        
        System.err.println(result);
        // yields "Catbert from Garfield Clan, having: Garfield, Catbert"
    }

}

First a CatPack gets constructed. Two members get added, "Garfield" and "Catbert". The add() method returns the created Cat instance, thus we can serialize "Catbert".

We serialize through an ObjectOutputStream based on an in-memory ByteArrayInputStream, which is then the source for the ObjectInputStream that de-serializes "Catbert". Now the question rises: what exactly has been sent over the line? Just "Catbert", or the whole pack?

Answer is: the whole CatPack went through the line! The output proves this:

Catbert from Garfield Clan, having: Garfield, Catbert

→ How could the Cat know the names of its mates after serialization when they did not travel with it?

Conclusion

Even when you know about this implicit pointer to the outer object, you will forget about it. Thus I recommend to always use static inner classes when there is no good reason for not doing it.

Which doesn't mean that I recommend the static keyword generally, inner classes is just a special case. In any other case try to avoid static, it has lots of disadvantages.




Keine Kommentare: