Подтвердить что ты не робот

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

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

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

  • простая инициализация поля (показана в примере): это то, что я обычно делаю, но инициализация, похоже, не происходит после unserialization;
  • инициализация в конструкторе (я считаю, что это семантически то же самое, что и выше);
  • присвоение поля в 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> ();
    }
}

Вывод:

[email protected] {}
[email protected] null
4b9b3361

Ответ 1

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

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

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

Ответ 2

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

}

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

Ответ 3

Да, это легко осуществить, реализуя (по-видимому, малоизвестный!) метод 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);
        }
    }
}

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

Before: [email protected] 'This data will be serialized' {This data=is transient}
After: [email protected] 'This data will be serialized' {}

Ответ 4

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

Ответ 5

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

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

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