Временное конечное поле, используемое в качестве блокировки, имеет значение null

Следующий код выдает ошибку 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);
            }
        }
    }
}

Вот результат:

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)

Как lock может быть нулевым?


person Brandon    schedule 07.09.2012    source источник
comment
@nicholas.hauschild Вопросы с самостоятельными ответами не только разрешены, но и поощряются.   -  person Adam Lear    schedule 08.09.2012


Ответы (3)


A transient final field used as a lock is null

Вот несколько фактов о переходной переменной:

- Ключевое слово Transient при использовании в переменной экземпляра предотвратит сериализацию этой переменной экземпляра.

- При десериализации переходная переменная получает свои значения по умолчанию.....

Например:

  • Переменная ссылки на объект для null
  • до 0
  • логическое значение для false, и т. д. .......

Вот почему вы получаете NullPointerException при десериализации...

person Kumar Vivek Mitra    schedule 07.09.2012

Любое поле, объявленное как transient, не сериализуется. Более того, согласно этого блога post значения полей даже не инициализируются значениями, которые были бы установлены конструктором по умолчанию. Это создает проблему, когда поле transient равно final.

В соответствии с документом Serializable java, десериализация может можно контролировать, реализуя следующий метод:

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

Я придумал следующее решение, основанное на отличном ответе StackOverflow:

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);
    }
}

Запуск приводит к следующему выводу (без NullPointerException):

About to synchronize
lock used
About to synchronize
lock used
person Brandon    schedule 07.09.2012
comment
Сообщение в блоге, которое вы цитируете, вообще ничего не говорит о значениях по умолчанию, не говоря уже о том, что вы утверждаете выше. Это даже не имеет смысла: чем еще они будут инициализированы? Ваше кодовое решение также чрезмерно сложное: для этого вам не нужно отражение. - person user207421; 08.09.2012
comment
В сообщении блога говорится, что поле члена экземпляра, объявленное как final, также может быть временным, но если это так, вы столкнетесь с проблемой, которую немного сложно решить... когда вы десериализуете объект, вам придется инициализировать поле вручную, [ но компилятор жалуется, потому что он окончательный]... Теперь, когда вы десериализуете класс, ваш регистратор будет нулевым объектом, поскольку он был временным. Это проблема, которую решает мой ответ. Без размышлений, как вы предлагаете установить значение поля final? Ваш комментарий тоже чересчур язвителен: для этого вам не нужен сарказм. - person Brandon; 09.09.2012
comment
В блоге не сказано того, что вы сказали. Период. Мой комментарий также не говорит того, что вы утверждаете: остальную часть его можно свести к слову «чрезмерный», что вряд ли можно назвать «едким» или «язвительным». Это сбивает меня с толку. - person user207421; 10.09.2012
comment
Извините, похоже, у нас возникло недопонимание по поводу перегруженного слова default. В моем посте выше я имел в виду значения, установленные в определении класса, то есть те, которые были установлены до построения. - person Brandon; 10.09.2012
comment
Обратите внимание, что в реализации Serializable для Android отсутствует метод readObject(), поэтому это решение будет работать только на JRE (и полностью совместимых реализациях), но не будет компилироваться на Android. - person user149408; 12.06.2016

Как указывалось ранее, приведенное ниже объявление работает не так, как можно было бы ожидать:

transient final Object foo = new Object()

Ключевое слово transient предотвратит сериализацию члена. Инициализация со значением по умолчанию не учитывается при десериализации, поэтому после десериализации foo будет null.

Ключевое слово final не позволит вам изменить элемент после его установки. Это означает, что вы навсегда застряли с null в десериализованном экземпляре.

В любом случае вам нужно будет убрать ключевое слово final. Это пожертвует неизменностью, но обычно не должно быть проблемой для private участников.

Тогда у вас есть два варианта:

Вариант 1. Переопределить readObject()

transient Object foo = new Object();

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

При создании нового экземпляра foo будет инициализировано значением по умолчанию. При десериализации об этом позаботится ваш собственный метод readObject().

Это будет работать на JRE, но не на Android, поскольку в реализации Android Serializable отсутствует метод readObject().

Вариант 2: ленивая инициализация

Декларация:

transient Object foo;

При доступе:

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

Вам придется делать это везде, где в вашем коде вы получаете доступ к foo, что может быть более трудоемким и более подверженным ошибкам, чем первый вариант, но он будет работать как на JRE, так и на Android.

person user149408    schedule 12.06.2016