końcowe pola przejściowe i serializacja

Czy możliwe jest posiadanie pól final transient, które po serializacji w Javie są ustawione na dowolną wartość inną niż domyślna? Moja baza usecase jest zmienną cache-dlatego jest transient. Mam też zwyczaj tworzenia Map pól, które nie będą zmieniane (tzn. zmieniana jest Zawartość mapy, ale sam obiekt pozostaje taki sam) final. Atrybuty te wydają się jednak sprzeczne - podczas gdy kompilator dopuszcza taką kombinację, nie mogę ustawić pola na nic innego jak null po unserialization.

Próbowałem następujących, bez powodzenia:

  • prosta inicjalizacja pola( pokazana w przykładzie): to jest to, co zwykle robię, ale inicjalizacja nie wydaje się mieć miejsca po unserializacji;
  • inicjalizacja w konstruktorze (wydaje mi się, że jest to semantycznie to samo co wyżej);
  • przypisanie pola w readObject() - nie można zrobić, ponieważ pole jest final.

W przykładzie cache jest public tylko dla testuję.

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

Wyjście:

test$X@1a46e30 {}
test$X@190d11 null
Author: doublep, 2010-06-03

5 answers

Krótka odpowiedź brzmi "nie" niestety - często tego chciałem. ale przechodnie nie mogą być ostateczne.

Ostateczne pole musi zostać zainicjowane przez bezpośrednie przypisanie wartości początkowej lub w konstruktorze. Podczas deserializacji nie są wywoływane żadne z nich, więc początkowe wartości dla transjentów muszą być ustawione w prywatnej metodzie ' readObject ()', która jest wywoływana podczas deserializacji. A żeby to zadziałało, przechodnie muszą być niekończące się.

(ściśle mówiąc, finały są tylko final za pierwszym razem są czytane, więc są hacki, które są możliwe, że przypisać wartość, zanim zostanie odczytana, ale dla mnie to idzie o krok za daleko.)

 31
Author: mdma,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2010-06-03 19:00:00

Możesz zmienić zawartość pola za pomocą odbicia. Działa na Javie 1.5+. Będzie działać, ponieważ serializacja jest wykonywana w jednym wątku. Po uzyskaniu dostępu do tego samego obiektu przez inny wątek, nie powinien on zmieniać ostatecznego pola(z powodu dziwności w modelu pamięci & refaction).

Więc w readObject() możesz zrobić coś podobnego do tego przykładu:

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!
    }

}

Pamiętaj: finał już nie jest finałowy!

 14
Author: Pindatjuh,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2010-06-03 19:06:10

Tak, jest to łatwo możliwe poprzez wdrożenie (najwyraźniej mało znany!) readResolve() metoda. Umożliwia zastąpienie obiektu po jego deserializacji. Możesz użyć tego do wywołania konstruktora, który zainicjalizuje obiekt zastępczy w dowolny sposób. Przykład:

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

Output -- łańcuch jest zachowany, ale Mapa przejściowa jest resetowana do pustej (ale nie-null!"Mapa": {]}

Before: test$X@172e0cc 'This data will be serialized' {This data=is transient}
After: test$X@490662 'This data will be serialized' {}
 11
Author: Boann,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2014-11-06 17:44:15

Ogólnym rozwiązaniem takich problemów jest użycie "seryjnego serwera proxy" (Zobacz Effective Java 2nd Ed). Jeśli chcesz ją doposażyć w istniejącą klasę serializowalną bez naruszania kompatybilności szeregowej, musisz wykonać kilka czynności hakerskich.

 5
Author: Tom Hawtin - tackline,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2010-06-04 10:42:52

Pięć lat później, moja oryginalna odpowiedź jest niezadowalająca po tym, jak natknąłem się na ten post przez Google. Innym rozwiązaniem byłoby użycie żadnego odbicia i użycie techniki sugerowanej przez Boanna.

Używa również klasy GetField zwracanej metodą ObjectInputStream#readFields(), która zgodnie ze specyfikacją serializacji musi być wywołana w prywatnej metodzie readObject(...).

Rozwiązanie sprawia, że deserializacja pól jest wyraźna, przechowując pobrane pola w tymczasowe pole przejściowe (zwane FinalExample#fields) tymczasowej "instancji" utworzonej w procesie deserializacji. Wszystkie pola obiektu są następnie deserializowane i wywołane readResolve(...): powstaje nowa instancja, ale tym razem przy użyciu konstruktora, odrzucając tymczasową instancję z tymczasowym polem. Instancja jawnie przywraca każde pole za pomocą instancji GetField; jest to miejsce do sprawdzania dowolnych parametrów, tak jak każdy inny konstruktor. Jeśli wyjątek jest wyrzucany przez konstruktor, jest on tłumaczony na an InvalidObjectException i deserializacja tego obiektu nie powiodła się.

Dołączony mikro-benchmark zapewnia, że to rozwiązanie nie jest wolniejsze niż domyślna serializacja/deserializacja. Rzeczywiście, jest na moim PC:

Problem: 8.598s Solution: 7.818s

Oto kod:

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

    }

}

Uwaga: ilekroć Klasa odnosi się do innej instancji obiektu, może być możliwe wyciek tymczasowej "instancji" utworzonej przez proces serializacji: rozdzielczość obiektu występuje dopiero po odczytaniu wszystkich pod-obiektów, stąd podobiekty mogą utrzymywać odniesienie do obiektu tymczasowego. Klasy mogą sprawdzić użycie takich nielegalnie skonstruowanych instancji, sprawdzając, czy tymczasowe pole GetField jest równe null. Tylko wtedy, gdy jest null, został utworzony przy użyciu zwykłego konstruktora, a nie w procesie deserializacji.

Uwaga dla siebie: być może lepsze rozwiązanie istnieje za pięć lat. Do zobaczenia!

 3
Author: Pindatjuh,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2015-07-12 18:29:04