конечные переходные поля и сериализация



можно ли есть final transient поля, которые установлены в любое значение не по умолчанию после сериализации в Java? Мой usecase является переменной кэша - вот почему это 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
610   5  

5 ответов:

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

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

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

Вы можете изменить содержимое поля с помощью отражения. Работает на 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!
    }

}

помните: финал уже не финал!

Да, это легко возможно путем реализации (по-видимому, мало известно!)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' {}

общее решение таких проблем заключается в использовании "последовательного прокси" (см. эффективный Java 2nd Ed). Если вам нужно обновить его до существующего сериализуемого класса без нарушения последовательной совместимости, то вам нужно будет сделать некоторые взломы.

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

он также использует GetField класс, возвращенный ObjectInputStream#readFields() метод, который согласно спецификации сериализации должен вызываться в private 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 временное поле имеет значение null. Только когда он равен null, он был создан с помощью обычного конструктора, а не через процесс десериализации.

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

Comments

    Ничего не найдено.