[JAVA] Élément 89: Pour le contrôle d'instance, préférez les types enum à readResolve

89. Pour le contrôle d'instance, sélectionnez le type enum dans readResolve

L'élément 3 a introduit le modèle singleton avec l'exemple suivant.

public class Elvis {
    public static final Elvis INSTANCE = new Elvis();

    private Elvis() {
    ...
    }

    public void leaveTheBuilding() {
    ...
    }
}

Cette classe n'est plus singleton lors de l'implémentation de Serializable. Ce n'est plus un singleton, qu'il s'agisse d'un formulaire sérialisé par défaut ou d'un formulaire sérialisé personnalisé, et qu'il fournisse une méthode readObject (Item87). La méthode readObject, qu'elle soit par défaut ou non, renvoie une instance nouvellement créée qui est différente de celle créée lors de l'initialisation de la classe.

readResolve En utilisant la méthode readResolve, il est possible de renvoyer une instance différente de l'objet créé par readObject.

Si la classe désérialisée définit correctement une méthode readResolve, cette méthode sera appelée après la désérialisation pour l'objet nouvellement créé. La référence renvoyée par cette méthode remplace l'objet nouvellement créé. Avec cette fonctionnalité, dans la plupart des cas, il n'y aura aucune référence aux objets nouvellement créés et les objets nouvellement créés seront bientôt traités par le garbage collection.

Si la classe Elvis ci-dessus implémente Serializable, il peut être garanti qu'elle est singleton en ajoutant la méthode readResolve comme indiqué ci-dessous.

public class Elvis {
    public static final Elvis INSTANCE = new Elvis();

    private Elvis() {
    ...
    }

    public void leaveTheBuilding() {
    ...
    }
    private Object readResolve() {
        return INSTANCE;
    }
}

Cette méthode readResolve ignore l'objet désérialisé et renvoie l'instance créée lors de l'initialisation de la classe. Par conséquent, les instances Elvis sérialisées ne doivent pas contenir de données. Autrement dit, tous les champs doivent être déclarés comme transitoires.

** Si vous souhaitez contrôler une instance avec readResolve, vous devez ajouter transitoire à tous les champs qui ont des références **. Sinon, une attaque comme celle illustrée dans Item88 pourrait être effectuée avant l'exécution de readResolve.

L'attaque est un peu compliquée, mais l'idée est simple.

Tout d'abord, écrivez la classe de voleur. La classe voleur a une méthode readResolve et un champ singleton masqué par le voleur. Remplacez un champ non transitoire singleton dans le flux de sérialisation par une instance de stealer. À ce stade, l'instance de singleton contient un voleur, qui a une référence de singleton.

Singleton contient un voleur, donc lorsque singleton est désérialisé, la méthode readResolve du voleur est lue en premier. Par conséquent, lorsque la méthode readResolve du voleur circule, le champ d'instance fait référence à un singleton partiellement désérialisé.

La méthode readResolve de stealer copie la référence du champ d'instance vers le champ statique, et la référence est accessible même après le flux de la méthode readResolve. La méthode readResolve renvoie ensuite le type correct de champ singleton masqué. Sinon, la machine virtuelle lèvera une exception CastException lorsque le système de sérialisation stocke la référence de voleur dans un champ singleton.

Un exemple spécifique est présenté. Considérez la classe singleton cassée suivante.

package tryAny.effectiveJava;

import java.io.Serializable;
import java.util.Arrays;

//Implémentation singleton incomplète avec des propriétés non transitoires
public class Elvis implements Serializable {
    public static final Elvis INSTANCE = new Elvis();

    private Elvis() {
    }

    //Propriétés non transitoires
    private String[] favoriteSongs = { "Hound Dog", "Heartbreak Hotel" };

    public void printFavorites() {
        System.out.println(Arrays.toString(favoriteSongs));
    }

    private Object readResolve() {
        return INSTANCE;
    }
}

La classe de voleur est la suivante.

package tryAny.effectiveJava;

import java.io.Serializable;

//Classe attaquant la classe Elvis
public class ElvisStealer implements Serializable {
    //Champ statique pour contenir des instances non singleton
    static Elvis impersonator;

    //Une autre instance d'Elvis
    private Elvis payload;

    private Object readResolve() {
        //Conserver l'instance Elvis dans la charge utile dans l'imitateur
        impersonator = payload;

        //Renvoie un tableau de chaînes car il déguise cette propriété en tableau de chaînes
        return new String[] { "A Fool Such as I" };
    }

    private static final long serialVersionUID = 0;
}

Vous trouverez ci-dessous une classe qui désérialise un flux maison qui crée deux instances d'un singleton défectueux. (Cela ne fonctionne pas avec Java 10?)

package tryAny.effectiveJava;

import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.io.ObjectInputStream;

public class ElvisImpersonator {
    //Flux Elvis modifié
    private static final byte[] serializedForm = new byte[] { (byte) 0xac, (byte) 0xed, 0x00, 0x05, 0x73, 0x72, 0x00,
            0x05, 0x45, 0x6c, 0x76, 0x69, 0x73, (byte) 0x84, (byte) 0xe6, (byte) 0x93, 0x33, (byte) 0xc3, (byte) 0xf4,
            (byte) 0x8b, 0x32, 0x02, 0x00, 0x01, 0x4c, 0x00, 0x0d, 0x66, 0x61, 0x76, 0x6f, 0x72, 0x69, 0x74, 0x65, 0x53,
            0x6f, 0x6e, 0x67, 0x73, 0x74, 0x00, 0x12, 0x4c, 0x6a, 0x61, 0x76, 0x61, 0x2f, 0x6c, 0x61, 0x6e, 0x67, 0x2f,
            0x4f, 0x62, 0x6a, 0x65, 0x63, 0x74, 0x3b, 0x78, 0x70, 0x73, 0x72, 0x00, 0x0c, 0x45, 0x6c, 0x76, 0x69, 0x73,
            0x53, 0x74, 0x65, 0x61, 0x6c, 0x65, 0x72, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0x00, 0x01,
            0x4c, 0x00, 0x07, 0x70, 0x61, 0x79, 0x6c, 0x6f, 0x61, 0x64, 0x74, 0x00, 0x07, 0x4c, 0x45, 0x6c, 0x76, 0x69,
            0x73, 0x3b, 0x78, 0x70, 0x71, 0x00, 0x7e, 0x00, 0x02 };

    public static void main(String[] args) {
        Elvis elvis = (Elvis) deserialize(serializedForm);
        Elvis impersonator = ElvisStealer.impersonator;
        elvis.printFavorites();
        impersonator.printFavorites();
    }

    static Object deserialize(byte[] sf) {
        try {
            return new ObjectInputStream(new ByteArrayInputStream(sf)).readObject();
        } catch (IOException | ClassNotFoundException e) {
            throw new IllegalArgumentException(e);
        }

    }
}

Lorsque vous exécutez ce programme

[Hound Dog,Heartbreak Hotel]
[A Fool Such as I]

Il semble qu'il s'affiche sous la forme. (Quand je l'ai exécuté sur Java 10, cela n'a pas fonctionné. Cela ressemble à ce qui suit)

Exception in thread "main" java.lang.IllegalArgumentException: java.lang.ClassNotFoundException: Elvis
	at tryAny.effectiveJava.ElvisImpersonator.deserialize(ElvisImpersonator.java:29)
	at tryAny.effectiveJava.ElvisImpersonator.main(ElvisImpersonator.java:19)
Caused by: java.lang.ClassNotFoundException: Elvis
	at java.base/jdk.internal.loader.BuiltinClassLoader.loadClass(BuiltinClassLoader.java:582)
	at java.base/jdk.internal.loader.ClassLoaders$AppClassLoader.loadClass(ClassLoaders.java:190)
	at java.base/java.lang.ClassLoader.loadClass(ClassLoader.java:499)
	at java.base/java.lang.Class.forName0(Native Method)
	at java.base/java.lang.Class.forName(Class.java:374)
	at java.base/java.io.ObjectInputStream.resolveClass(ObjectInputStream.java:685)
	at java.base/java.io.ObjectInputStream.readNonProxyDesc(ObjectInputStream.java:1877)
	at java.base/java.io.ObjectInputStream.readClassDesc(ObjectInputStream.java:1763)
	at java.base/java.io.ObjectInputStream.readOrdinaryObject(ObjectInputStream.java:2051)
	at java.base/java.io.ObjectInputStream.readObject0(ObjectInputStream.java:1585)
	at java.base/java.io.ObjectInputStream.readObject(ObjectInputStream.java:422)
	at tryAny.effectiveJava.ElvisImpersonator.deserialize(ElvisImpersonator.java:27)
	... 1 more

Si le résultat est comme prévu, c'est un problème car il y a deux instances différentes même s'il s'agit d'une seule tonne.

Ce problème peut être résolu en rendant le champ favoriteSongs transitoire.

single-element enum Cependant, il est plus logique de créer la classe Elvis en tant qu'énumération avec un élément (Item3).

Si vous écrivez une classe qui est sérialisable et nécessite un contrôle d'instance dans enum, Java garantit qu'il ne peut y avoir aucune instance autre que celles déclarées. Dans l'exemple d'Elvis, c'est comme suit.

public enum Elvis {
    INSTANCE;
    private String[] favoriteSongs =
            {"Hound Dog", "Heartbreak Hotel"};

    public void printFavorites() {
        System.out.println(Arrays.toString(favoriteSongs));
    }
}

Enum ne rend pas le contrôle d'instance readResolve obsolète. Enum ne peut pas gérer les classes sérialisables et nécessitant un contrôle d'instance, qui ne sont pas corrigées lors de la compilation.

readResolve accessibilité

Si vous mettez readResolve dans la classe finale, il doit être privé.

Si vous le mettez dans une classe qui n'est pas définitive, vous devez le considérer. S'il est privé, il n'est pas accessible depuis les sous-classes. Si package-private, seules les sous-classes du même package sont accessibles. S'il est protégé ou public, il peut être utilisé dans toutes les sous-classes.

Dans le cas de protected ou public, si la sous-classe ne remplace pas readResolve, elle retournera une instance de la superclasse lors de la désérialisation de la sous-classe, ce qui est susceptible de provoquer une exception ClassCastException.

Recommended Posts

Élément 89: Pour le contrôle d'instance, préférez les types enum à readResolve
Point 58: Préférez les boucles for-each aux boucles for traditionnelles
Point 61: Préférez les types primitifs aux primitives encadrées
Élément 28: Préférer les listes aux tableaux
Rubrique 65: Préférez les interfaces à la réflexion
Rubrique 43: Préférez les références de méthode aux lambdas
Point 42: Préférez les lambdas aux classes anonymes
Élément 39: Préférez les annotations aux modèles de dénomination
Point 85: Préférez les alternatives à la sérialisation Java
Point 41: Utiliser les interfaces de marqueurs pour définir les types
Élément 23: Préférez les hiérarchies de classes aux classes balisées
Élément 81: Préférez les utilitaires de concurrence pour attendre et notifier
Élément 80: Préférez les exécuteurs, les tâches et les flux aux threads