конечные переходные поля и сериализация - PullRequest
57 голосов
/ 03 июня 2010

Возможно ли иметь поля final transient, для которых задано любое значение не по умолчанию после сериализации в Java? Мой вариант использования - это переменная кеша, поэтому она равна transient. У меня также есть привычка создавать Map поля, которые не будут изменены (т.е. содержимое карты изменяется, но сам объект остается прежним) final. Тем не менее, эти атрибуты кажутся противоречивыми - хотя компилятор допускает такую ​​комбинацию, я не могу установить в поле ничего, кроме null после десериализации.

Я безуспешно пробовал следующее:

  • простая инициализация поля (показано в примере): это то, что я обычно делаю, но инициализация после десериализации, кажется, не происходит;
  • инициализация в конструкторе (я думаю, что это семантически то же самое, что и выше);
  • назначение поля в readObject() - не может быть выполнено, поскольку поле final.

В примере cache означает public только для тестирования.

import java.io.*;
import java.util.*;

public class test
{
    public static void main (String[] args) throws Exception
    {
        X  x = new X ();
        System.out.println (x + " " + x.cache);

        ByteArrayOutputStream  buffer = new ByteArrayOutputStream ();
        new ObjectOutputStream (buffer).writeObject (x);
        x = (X) new ObjectInputStream (new ByteArrayInputStream (buffer.toByteArray ())).readObject ();
        System.out.println (x + " " + x.cache);
    }

    public static class X implements Serializable
    {
        public final transient Map <Object, Object>  cache = new HashMap <Object, Object> ();
    }
}

Вывод:

test$X@1a46e30 {}
test$X@190d11 null

Ответы [ 5 ]

33 голосов
/ 03 июня 2010

К сожалению, короткий ответ «нет» - я часто этого хотел. но переходные процессы не могут быть окончательными.

Последнее поле должно быть инициализировано либо прямым присваиванием начального значения, либо в конструкторе. Во время десериализации ни один из них не вызывается, поэтому начальные значения для переходных процессов должны быть установлены в закрытом методе readObject (), который вызывается во время десериализации. И чтобы это работало, переходные процессы должны быть не окончательными.

(Строго говоря, финал является окончательным только при первом прочтении, поэтому возможны хаки, которые присваивают значение до его прочтения, но для меня это слишком далеко).

16 голосов
/ 03 июня 2010

Вы можете изменить содержимое поля, используя Reflection. Работает на Java 1.5+. Это будет работать, потому что сериализация выполняется в одном потоке. После того, как другой поток обращается к тому же объекту, он не должен изменять конечное поле (из-за странностей в модели памяти и рефлексии).

Итак, в readObject() вы можете сделать что-то похожее на этот пример:

import java.lang.reflect.Field;

public class FinalTransient {

    private final transient Object a = null;

    public static void main(String... args) throws Exception {
        FinalTransient b = new FinalTransient();

        System.out.println("First: " + b.a); // e.g. after serialization

        Field f = b.getClass().getDeclaredField("a");
        f.setAccessible(true);
        f.set(b, 6); // e.g. putting back your cache

        System.out.println("Second: " + b.a); // wow: it has a value!
    }

}

Помните: Финал больше не является финалом!

14 голосов
/ 06 ноября 2014

Да, это легко сделать, реализовав (очевидно малоизвестный!) readResolve() метод. Позволяет заменить объект после его десериализации. Вы можете использовать это для вызова конструктора, который будет инициализировать замещающий объект, как вам угодно. Пример:

import java.io.*;
import java.util.*;

public class test {
    public static void main(String[] args) throws Exception {
        X x = new X();
        x.name = "This data will be serialized";
        x.cache.put("This data", "is transient");
        System.out.println("Before: " + x + " '" + x.name + "' " + x.cache);

        ByteArrayOutputStream buffer = new ByteArrayOutputStream();
        new ObjectOutputStream(buffer).writeObject(x);
        x = (X)new ObjectInputStream(new ByteArrayInputStream(buffer.toByteArray())).readObject();
        System.out.println("After: " + x + " '" + x.name + "' " + x.cache);
    }

    public static class X implements Serializable {
        public final transient Map<Object,Object> cache = new HashMap<>();
        public String name;

        public X() {} // normal constructor

        private X(X x) { // constructor for deserialization
            // copy the non-transient fields
            this.name = x.name;
        }

        private Object readResolve() {
            // create a new object from the deserialized one
            return new X(this);
        }
    }
}

Вывод - строка сохраняется, но карта переходных процессов сбрасывается на пустую (но не нулевую!) Карту:

Before: test$X@172e0cc 'This data will be serialized' {This data=is transient}
After: test$X@490662 'This data will be serialized' {}
5 голосов
/ 04 июня 2010

Общее решение таких проблем - использование «последовательного прокси» (см. Effective Java 2nd Ed). Если вам нужно дооснастить это до существующего сериализуемого класса, не нарушая последовательную совместимость, то вам придется взломать.

3 голосов
/ 12 июля 2015

Пять лет спустя я нахожу свой оригинальный ответ неудовлетворительным после того, как наткнулся на этот пост через Google. Другим решением было бы вообще не использовать отражение и использовать технику, предложенную Boann.

Также используется класс GetField , возвращаемый методом ObjectInputStream#readFields(), который в соответствии со спецификацией сериализации должен вызываться в закрытом методе readObject(...).

Решение делает явную десериализацию поля путем сохранения найденных полей во временном переходном поле (называемом FinalExample#fields) временного «экземпляра», созданного процессом десериализации. Затем все поля объекта десериализуются и вызывается readResolve(...): создается новый экземпляр, но на этот раз с использованием конструктора, отбрасывающего временный экземпляр с помощью временного поля. Экземпляр явно восстанавливает каждое поле, используя экземпляр GetField; это место для проверки любых параметров, как и любой другой конструктор. Если конструктор выдает исключение, оно преобразуется в InvalidObjectException, и десериализация этого объекта завершается неудачей.

Микропроцессор, поставляемый в комплекте, гарантирует, что это решение не медленнее, чем сериализация / десериализация по умолчанию. Действительно, это на моем ПК:

Problem: 8.598s Solution: 7.818s

Тогда вот код:

import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.InvalidObjectException;
import java.io.ObjectInputStream;
import java.io.ObjectInputStream.GetField;
import java.io.ObjectOutputStream;
import java.io.ObjectStreamException;
import java.io.Serializable;

import org.junit.Test;

import static org.junit.Assert.*;

public class FinalSerialization {

    /**
     * Using default serialization, there are problems with transient final
     * fields. This is because internally, ObjectInputStream uses the Unsafe
     * class to create an "instance", without calling a constructor.
     */
    @Test
    public void problem() throws Exception {
        ByteArrayOutputStream baos = new ByteArrayOutputStream();
        ObjectOutputStream oos = new ObjectOutputStream(baos);
        WrongExample x = new WrongExample(1234);
        oos.writeObject(x);
        oos.close();
        ByteArrayInputStream bais = new ByteArrayInputStream(baos.toByteArray());
        ObjectInputStream ois = new ObjectInputStream(bais);
        WrongExample y = (WrongExample) ois.readObject();
        assertTrue(y.value == 1234);
        // Problem:
        assertFalse(y.ref != null);
        ois.close();
        baos.close();
        bais.close();
    }

    /**
     * Use the readResolve method to construct a new object with the correct
     * finals initialized. Because we now call the constructor explicitly, all
     * finals are properly set up.
     */
    @Test
    public void solution() throws Exception {
        ByteArrayOutputStream baos = new ByteArrayOutputStream();
        ObjectOutputStream oos = new ObjectOutputStream(baos);
        FinalExample x = new FinalExample(1234);
        oos.writeObject(x);
        oos.close();
        ByteArrayInputStream bais = new ByteArrayInputStream(baos.toByteArray());
        ObjectInputStream ois = new ObjectInputStream(bais);
        FinalExample y = (FinalExample) ois.readObject();
        assertTrue(y.ref != null);
        assertTrue(y.value == 1234);
        ois.close();
        baos.close();
        bais.close();
    }

    /**
     * The solution <em>should not</em> have worse execution time than built-in
     * deserialization.
     */
    @Test
    public void benchmark() throws Exception {
        int TRIALS = 500_000;

        long a = System.currentTimeMillis();
        for (int i = 0; i < TRIALS; i++) {
            problem();
        }
        a = System.currentTimeMillis() - a;

        long b = System.currentTimeMillis();
        for (int i = 0; i < TRIALS; i++) {
            solution();
        }
        b = System.currentTimeMillis() - b;

        System.out.println("Problem: " + a / 1000f + "s Solution: " + b / 1000f + "s");
        assertTrue(b <= a);
    }

    public static class FinalExample implements Serializable {

        private static final long serialVersionUID = 4772085863429354018L;

        public final transient Object ref = new Object();

        public final int value;

        private transient GetField fields;

        public FinalExample(int value) {
            this.value = value;
        }

        private FinalExample(GetField fields) throws IOException {
            // assign fields
            value = fields.get("value", 0);
        }

        private void readObject(ObjectInputStream stream) throws IOException,
                ClassNotFoundException {
            fields = stream.readFields();
        }

        private Object readResolve() throws ObjectStreamException {
            try {
                return new FinalExample(fields);
            } catch (IOException ex) {
                throw new InvalidObjectException(ex.getMessage());
            }
        }

    }

    public static class WrongExample implements Serializable {

        private static final long serialVersionUID = 4772085863429354018L;

        public final transient Object ref = new Object();

        public final int value;

        public WrongExample(int value) {
            this.value = value;
        }

    }

}

Предупреждение: всякий раз, когда класс ссылается на другой экземпляр объекта, может быть возможно утечка временного «экземпляра», созданного процессом сериализации: разрешение объекта происходит только после того, как все подобъекты прочитаны, следовательно, это возможно, чтобы подобъекты сохраняли ссылку на временный объект. Классы могут проверить использование таких незаконно созданных экземпляров, проверив, что временное поле GetField является нулевым. Только когда он нулевой, он был создан с использованием обычного конструктора, а не в процессе десериализации.

Примечание для себя: Возможно, лучшее решение существует через пять лет. Увидимся тогда!

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...