71 votes

champs transitoires finaux et sérialisation

Est-il possible d'avoir final transient les champs qui prennent une valeur autre que la valeur par défaut après la sérialisation en Java ? Mon cas d'utilisation est une variable de cache - c'est pourquoi il s'agit d'une valeur par défaut. transient . J'ai aussi l'habitude de faire Map les champs 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 permet une telle combinaison, je ne peux pas faire en sorte que le champ soit défini sur autre chose que null après la désérialisation.

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

  • initialisation simple des champs (montré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 crois que c'est sémantiquement la même chose que ci-dessus) ;
  • l'attribution du champ dans readObject() - ne peut pas être fait puisque le champ est final .

Dans l'exemple cache es 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> ();
    }
}

Sortie :

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

36voto

mdma Points 33973

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

Un champ final doit être initialisé soit par affectation directe d'une valeur initiale, soit dans le constructeur. Pendant la désérialisation, aucune de ces méthodes n'est invoquée. Les valeurs initiales des transients doivent donc ê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.

(Strictement parlant, 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 c'est aller un peu trop loin).

0 votes

Merci. Je m'en doutais aussi, mais je n'étais pas sûr de ne pas avoir manqué quelque chose.

7 votes

Votre réponse "les transitoires ne peuvent pas être finaux" est incorrecte : veuillez expliquer le code source d'Hibernate avec final transient partout : github.com/hibernate/hibernate-orm/blob/4.3.7.Final/

23 votes

En fait, la réponse est fausse. transient Les champs peuvent être final . Mais pour que cela fonctionne pour autre chose que les valeurs par défaut ( false / 0 / 0.0 / null ), vous voulez mettre en œuvre non seulement readObject() mais aussi readResolve() ou utiliser Réflexion .

17voto

Boann Points 11904

Oui, cela est facilement possible en mettant en œuvre la (apparemment peu connue !) readResolve() méthode. Elle 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 préservée mais la carte transitoire est réinitialisée à 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' {}

17voto

Pindatjuh Points 6929

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

Ainsi, en 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 la finale !

5 votes

Eh bien, ça semble trop désordonné, je suppose que c'est plus facile d'abandonner. final ici ;)

1 votes

Vous pouvez également mettre en œuvre une TransientMap que vous marquez final mais pas transient . Chaque propriété, cependant, dans la carte doit être transient Par conséquent, la carte n'est pas sérialisée, mais existe toujours lors de la désérialisation (et est vide).

0 votes

@doublep : en fait, la désérialisation est la raison pour laquelle cette possibilité existe. C'est aussi la raison pour laquelle cela ne fonctionne pas pour static final champs, static ne sont jamais (dé)sérialisés, cette fonctionnalité n'est donc pas nécessaire.

5voto

La solution générale à ce genre de problème consiste à utiliser un "proxy série" (voir Effective Java 2nd Ed). Si vous avez besoin de l'adapter à une classe sérialisable existante sans rompre la compatibilité sérielle, vous devrez faire quelques modifications.

1 votes

Je suppose que vous ne pouvez pas développer cette réponse, n'est-ce pas ? J'ai bien peur de ne pas avoir le livre en question...

1 votes

@user1803551 Ce n'est pas vraiment utile. Les réponses ici sont censées fournir une description réelle de la façon de résoudre le problème, et pas seulement un pointeur vers une recherche google.

4voto

Pindatjuh Points 6929

Cinq ans plus tard, je trouve ma réponse initiale insatisfaisante après être tombé sur cet article 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 fait également appel au GetField retournée par ObjectInputStream#readFields() qui, conformément à la spécification de sérialisation, doit être appelée dans la méthode privée readObject(...) méthode.

La solution rend la désérialisation des champs explicite en stockant les champs récupérés dans un champ temporaire transitoire (appelé FinalExample#fields ) d'une "instance" temporaire créée par le processus de désérialisation. Tous les champs de l'objet sont alors désérialisés et readResolve(...) est appelé : une nouvelle instance est créée, mais cette fois-ci en utilisant un constructeur, en rejetant l'instance temporaire avec le champ temporaire. L'instance restaure explicitement chaque champ en utilisant la fonction GetField c'est l'endroit où il faut vérifier les paramètres comme pour tout autre constructeur. Si une exception est levée par le constructeur, elle est traduite en un message de type 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, elle l'est sur mon PC :

Problem: 8.598s Solution: 7.818s

Ensuite, 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 note d'avertissement : chaque fois que la classe fait référence à une autre instance d'objet, il est possible de faire fuir l'"instance" temporaire créée par le processus de sérialisation : la résolution de l'objet ne se produit qu'après la lecture de tous les sous-objets, il est donc possible que les sous-objets conservent une référence à l'objet temporaire. Les classes peuvent vérifier l'utilisation de telles instances illégalement construites en vérifiant que la classe GetField le champ temporaire est nul. Ce n'est que lorsqu'il est nul qu'il a été créé à l'aide d'un constructeur normal et non par le processus de désérialisation.

Note à soi-même : Peut-être qu'une meilleure solution existera dans cinq ans. À ce moment-là !

Prograide.com

Prograide est une communauté de développeurs qui cherche à élargir la connaissance de la programmation au-delà de l'anglais.
Pour cela nous avons les plus grands doutes résolus en français et vous pouvez aussi poser vos propres questions ou résoudre celles des autres.

Powered by:

X