web-dev-qa-db-fra.com

champs transitoires finaux et sérialisation

Est-il possible d'avoir des champs final transient Qui sont définis sur une valeur non par défaut après la sérialisation en Java? Mon cas d'utilisation est une variable de cache - c'est pourquoi c'est transient. J'ai également l'habitude de créer des champs Map qui ne seront pas modifiés (c'est-à-dire que le contenu de la carte est modifié, mais l'objet lui-même reste le même) final. Cependant, ces attributs semblent être contradictoires - alors que le compilateur autorise une telle combinaison, je ne peux pas avoir le champ défini sur autre chose que null après la désérialisation.

J'ai essayé ce qui suit, sans succès:

  • initialisation de champ simple (illustrée dans l'exemple): c'est ce que je fais normalement, mais l'initialisation ne semble pas se produire après la désérialisation;
  • initialisation dans le constructeur (je pense que c'est sémantiquement le même que ci-dessus);
  • affecter le champ dans readObject() - ne peut pas être fait car le champ est final.

Dans l'exemple, cache est public uniquement pour les tests.

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

Production:

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

La réponse courte est "non" malheureusement - j'ai souvent voulu cela. mais les transitoires ne peuvent pas être définitifs.

Un champ final doit être initialisé soit par affectation directe d'une valeur initiale soit dans le constructeur. Pendant la désérialisation, aucun de ces éléments n'est appelé, donc les valeurs initiales pour les transitoires doivent être définies dans la méthode privée 'readObject ()' qui est invoquée pendant la désérialisation. Et pour que cela fonctionne, les transitoires doivent être non finaux.

(À proprement parler, les finales ne sont finales que la première fois qu'elles sont lues, il y a donc des hacks possibles qui assignent une valeur avant qu'elle ne soit lue, mais pour moi, cela va un peu trop loin.)

33
mdma

Vous pouvez modifier le contenu d'un champ à l'aide de la réflexion. Fonctionne sur Java 1.5+. Cela fonctionnera, car la sérialisation est effectuée dans un seul thread. Après qu'un autre thread accède au même objet, il ne devrait pas changer le champ final (en raison de l'étrangeté dans le modèle de mémoire et réflexion).

Donc, dans readObject(), vous pouvez faire quelque chose de similaire à cet exemple:

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

}

Rappelez-vous: la finale n'est plus finale!

16
Pindatjuh

Oui, cela est facilement possible en implémentant la méthode (apparemment peu connue!) readResolve(). Il vous permet de remplacer l'objet après sa désérialisation. Vous pouvez l'utiliser pour invoquer un constructeur qui initialisera un objet de remplacement comme vous le souhaitez. Un exemple:

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

Sortie - la chaîne est conservée mais la carte transitoire est réinitialisée sur une carte vide (mais non nulle!):

Before: test$X@172e0cc 'This data will be serialized' {This data=is transient}
After: test$X@490662 'This data will be serialized' {}
14
Boann

La solution générale à des problèmes comme celui-ci consiste à utiliser un "proxy série" (voir Efficace Java 2nd Ed). Si vous avez besoin de l'adapter à une classe sérialisable existante sans rompre la compatibilité série, alors vous devra faire un piratage.

5

Cinq ans plus tard, je trouve ma réponse originale insatisfaisante après que je suis tombé sur ce post via Google. Une autre solution serait de ne pas utiliser de réflexion du tout et d'utiliser la technique suggérée par Boann.

Il utilise également la classe GetField renvoyée par la méthode ObjectInputStream#readFields(), qui selon la spécification de sérialisation doit être appelée dans la méthode privée readObject(...).

La solution rend la désérialisation de champ explicite en stockant les champs récupérés dans un champ transitoire temporaire (appelé FinalExample#fields) D'une "instance" temporaire créée par le processus de désérialisation. Tous les champs d'objet sont ensuite désérialisés et readResolve(...) est appelé: une nouvelle instance est créée mais cette fois en utilisant un constructeur, en supprimant l'instance temporaire avec le champ temporaire. L'instance restaure explicitement chaque champ à l'aide de l'instance GetField; c'est l'endroit pour vérifier tous les paramètres comme n'importe quel autre constructeur. Si une exception est levée par le constructeur, elle est traduite en InvalidObjectException et la désérialisation de cet objet échoue.

Le micro-benchmark inclus garantit que cette solution n'est pas plus lente que la sérialisation/désérialisation par défaut. En effet, c'est sur mon PC:

Problem: 8.598s Solution: 7.818s

Alors voici le code:

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

    }

}

Une mise en garde: chaque fois que la classe fait référence à une autre instance d'objet, il peut être possible de divulguer "l'instance" temporaire créée par le processus de sérialisation: la résolution d'objet se produit uniquement après la lecture de tous les sous-objets, d'où la possibilité pour les sous-objets pour conserver une référence à l'objet temporaire. Les classes peuvent vérifier l'utilisation de telles instances construites illégalement en vérifiant que le champ temporaire GetField est nul. Ce n'est que lorsqu'il est nul qu'il a été créé à l'aide d'un constructeur normal et non via le processus de désérialisation.

Note à soi-même: Peut-être qu'une meilleure solution existe dans cinq ans. À plus tard!

3
Pindatjuh