A transient final field used as a lock is null
Asked Answered
E

3

6

The following code throws a NullPointerException.

import java.io.*;

public class NullFinalTest {
    public static void main(String[] args) throws IOException, ClassNotFoundException {
        Foo foo = new Foo();
        foo.useLock();
        ByteArrayOutputStream buffer = new ByteArrayOutputStream();
        new ObjectOutputStream(buffer).writeObject(foo);
        foo = (Foo) new ObjectInputStream(new ByteArrayInputStream(buffer.toByteArray())).readObject();
        foo.useLock();
    }

    public static class Foo implements Serializable {
        private final String lockUsed = "lock used";
        private transient final Object lock = new Object();
        public void useLock() {
            System.out.println("About to synchronize");
            synchronized (lock) { // <- NullPointerException here on 2nd call
                System.out.println(lockUsed);
            }
        }
    }
}

Here is the output:

About to synchronize
lock used
About to synchronize
Exception in thread "main" java.lang.NullPointerException
    at NullFinalTest$Foo.useLock(NullFinalTest.java:18)
    at NullFinalTest.main(NullFinalTest.java:10)

How can lock possibly be null?

Encyclical answered 7/9, 2012 at 19:47 Comment(1)
@nicholas.hauschild Self-answering questions is not only allowed, but also encouraged.Overrun
S
15

A transient final field used as a lock is null

Here are few facts about the transient variable:

- Transient keyword when used on an instance variable, will prevent that instance variable to be serialized.

- On De-serialization, the transient variable get to their Default values.....

Eg:

  • Object Reference Variable to null
  • int to 0
  • boolean to false, etc.......

So thats the reason you are getting a NullPointerException, when deserializing it...

Submit answered 7/9, 2012 at 20:3 Comment(0)
E
4

Any field that is declared transient is not serialized. Moreover, according to this blog post, field values are not even initialized to the values that would be set by a default constructor. This creates a challenge when a transient field is final.

According to the Serializable javadoc, deserialization can be controlled by implementing the following method:

private void readObject(java.io.ObjectInputStream in)
    throws IOException, ClassNotFoundException;

I came up with the following solution, based on this excellent StackOverflow answer:

import java.io.*;
import java.lang.reflect.*;

public class NullFinalTestFixed {
    public static void main(String[] args) throws IOException, ClassNotFoundException {
        Foo foo = new Foo();
        foo.useLock();
        ByteArrayOutputStream buffer = new ByteArrayOutputStream();
        new ObjectOutputStream(buffer).writeObject(foo);
        foo = (Foo) new ObjectInputStream(new ByteArrayInputStream(buffer.toByteArray())).readObject();
        foo.useLock();
    }

    public static class Foo implements Serializable {
        private final String lockUsed = "lock used";
        private transient final Object lock = new Object();
        public void useLock() {
            System.out.println("About to synchronize");
            synchronized (lock) {
                System.out.println(lockUsed);
            }
        }

        private void readObject(ObjectInputStream in) throws IOException, ClassNotFoundException {
            in.defaultReadObject();
            initLocks(this, "lock");
        }
    }

    public static void initLocks(Object obj, String... lockFields) {
        for (String lockField: lockFields) {
            try {
                Field lock = obj.getClass().getDeclaredField(lockField);
                setFinalFieldValue(obj, lock, new Object());
            } catch (NoSuchFieldException e) {
                throw new RuntimeException(e);
            }
        }
    }

    public static void setFinalFieldValue(Object obj, Field field, Object value) {
        Exception ex;
        try {
            field.setAccessible(true);
            Field modifiers = Field.class.getDeclaredField("modifiers");
            modifiers.setAccessible(true);
            modifiers.setInt(field, field.getModifiers() & ~Modifier.FINAL);
            field.set(obj, value);
            return;
        } catch (IllegalAccessException e) {
            ex = e;
        } catch (NoSuchFieldException e) {
            ex = e;
        }
        throw new RuntimeException(ex);
    }
}

Running it results in the following output (no NullPointerException):

About to synchronize
lock used
About to synchronize
lock used
Encyclical answered 7/9, 2012 at 19:47 Comment(5)
The blog post you cite doesn't say anything at all about default values, let alone what you claim above. It doesn't even make sense: what else would they be initialized to? Your code solution is excessively complex as well: you don't need reflection for this.Handicapper
The blog post says, "an instance member field declared as final could also be transient, but if so, you would face a problem a little bit difficult to solve... when you deserialize the object you would have to initialize the field manually, [but the compiler complains because it is final]... Now, when you deserialize the class your logger will be a null object, since it was transient." This is the problem my answer solves. Without reflection, how do you propose that we set the value of a final field? Your comment is excessively vitriolic as well: you don't need snark for this.Encyclical
The blog didn't say what you said it said. Period. My comment also doesn't say what you claim: the remainder of it can be reduced to the word 'excessive', which is hardly 'vitriolic' or 'snarky'. This baffles me.Handicapper
Sorry, it sounds like we're having a misunderstanding around the overloaded word, "default." In my post above, I was referring to values set in the class definition, i.e., those set before construction.Encyclical
Note that the Android implementation of Serializable lacks the readObject() method, hence this solution will work only on JRE (and fully compatible implementations) but will not compile on Android.Unification
U
2

As pointed out before, the declaration below does not work as one might expect:

transient final Object foo = new Object()

The transient keyword will prevent the member from being serialized. Initialization with a default value is not honored during deserialization, therefore foo will be null after deserialization.

The final keyword will prevent you from modifiying the member once it has been set. This means you're stuck with null forever on a deserialized instance.

In any case you will need to drop the final keyword. This will sacrifice immutability, but should not usually be an issue for private members.

Then you have two options:

Option 1: Override readObject()

transient Object foo = new Object();

@Override
private void readObject(ObjectInputStream in) throws IOException, ClassNotFoundException {
    in.defaultReadObject();
    foo = new Object();
}

When creating a new instance, foo will be initialized to its default value. When deserializing, your custom readObject() method will take care of that.

This will work on JRE but not on Android, as Android's implementation of Serializable lacks the readObject() method.

Option 2: Lazy initialization

Declaration:

transient Object foo;

On access:

if (foo == null)
    foo = new Object();
doStuff(foo);

You would have to do this wherever in your code you access foo, which may be more work and more error-prone than the first option, but it will work on JRE and Android alike.

Unification answered 12/6, 2016 at 15:54 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.