Types scellés en Java

Le langage Java a récemment commencé à se développer activement. La version semestrielle des versions Java ne peut que plaire au développeur Java avec de nouvelles fonctionnalités.

L'un des domaines prioritaires pour le développement Java est la correspondance de modèles. La correspondance de modèles révèle au développeur la possibilité d'écrire du code de manière plus flexible et plus belle, tout en le laissant clair.

Les blocs clés pour la correspondance de modèles en Java prévoient d'enregistrer et de sceller les types.

Les enregistrements fournissent une syntaxe concise pour déclarer des classes qui sont de simples porteurs d'ensembles de données constants et immuables.

Apparaîtra dans Java 14 en tant que fonction d'aperçu.

record Person(String name, int age) { }

Les types scellés sont des classes ou des interfaces qui imposent des restrictions sur d'autres classes ou interfaces qui peuvent les étendre ou les implémenter. Avec un degré de probabilité élevé, ils peuvent apparaître dans Java 15.

Ce sont des types énumérés sur les stéroïdes.

sealed interface Color permits BiColor, TriColor { }
	
record BiColor(int r, int g, int b) implements Color {}
record TriColor(int r, int g, int b) implements Color {}

Un modificateur scellé est utilisé pour déclarer une classe ou une interface scellée. La liste des sous-types peut être répertoriée au moment de la déclaration de la classe ou de l'interface scellée
après le mot clé permit. Si les sous-types se trouvent dans le même package ou module, le compilateur peut répertorier les sous-types et les autorisations dans la déclaration de la
classe scellée ou l'interface peuvent être omises.

Si le sous-type est abstrait, il devient alors implicitement marqué avec un modificateur scellé, sauf s'il est explicitement marqué avec un modificateur non scellé.

Comme vous pouvez le voir, Java introduit un nouveau type de mots-clés appelés mots-clés avec trait d'union .
Ces mots clés seront constitués de deux mots séparés par un tiret.

Les sous-types concrets deviennent implicitement définitifs, sauf s'ils sont explicitement marqués par un modificateur non scellé. Bien qu'il me semble, il serait préférable d'utiliser la non-finale si vous ne voulez pas la rendre définitive.

Afin de prendre en charge les types scellés, un nouvel attribut PermittedSubtypes est ajouté aux fichiers de classe, qui stocke une liste de sous-types.

PermittedSubtypes_attribute {
	u2 attribute_name_index;
	u4 attribute_length;
	u2 permitted_subtypes_count;
	u2 classes[permitted_subtypes_count];
}

De plus, afin de travailler avec des types scellés par réflexion, deux méthodes sont ajoutées à java.lang.Class.

java.lang.constant.ClassDesc<?>[] getPermittedSubtypes()
boolean isSealed()

La première méthode renvoie un tableau d'objets java.lang.constant.ClassDesc qui représentent une liste de sous-types si la classe est marquée avec un modificateur scellé. Si la classe n'est pas marquée avec un modificateur scellé, un tableau vide est retourné. La deuxième méthode renvoie true si la classe ou l'interface donnée est marquée avec un modificateur scellé.

Tout cela permet au compilateur de déterminer si tous les sous-types sont énumérés ou non, lors de l'itération sur la liste des sous-types dans une expression de commutateur.

var result = switch (color) {
	case BiColor bc -> 0x1;
	case TriColor tc -> 0x2;
}

La branche par défaut est facultative, car le compilateur définit tous les sous-types valides.
Et si un nouveau sous-type est ajouté, le compilateur déterminera que tous les sous-types ne sont pas pris en compte dans le commutateur, et il générera une erreur dans le temps de compilation.

L'idée des types scellés n'est pas nouvelle. Par exemple, dans la langue Kotlin, il existe des classes scellées. Mais il n'y a pas d'interfaces scellées.

sealed class Color
	
data class BiColor(val r: Int, val g: Int, val b: Int) : Color 
data class TriColor(val r: Int, val g: Int, val b: Int) : Color

Les classes scellées Kotlin sont implicitement abstraites et ont un constructeur par défaut privé. Par conséquent, les sous-types doivent être imbriqués dans la classe. En Java, il peut être représenté comme suit.

public abstract class Color {
	private Color() {}
	
	public static class BiColor(int r, int g, int b) extends Color {}
	public static class TriColor(int r, int g, int b) extends Color {}
}

Dans ce cas, il peut être garanti que tous les sous-types peuvent être déterminés au moment de la compilation.

C'est bien beau, mais si vous voulez essayer des cours scellés, mais ils ne sont pas encore sortis. Heureusement, rien n'empêche l'implémentation d'un simple visiteur, tout comme l'expression quand fonctionne dans Kotlin.

 matches(color).as(
       Color.BiColor.class,  bc -> System.out.println("bi color:  " + bc),
       Color.TriColor.class, tc -> System.out.println("tri color:  " + tc)
 );

La mise en œuvre des visiteurs est très simple. Nous prenons le type de l'objet cible et, selon le type de branche, nous effectuons des expressions lambda.

 public static <V, T1, T2>
 void matches(V value,
                Class<T1> firstClazz,  Consumer<T1> firstBranch,
                Class<T2> secondClazz, Consumer<T2> secondBranch) {
        verifyExhaustiveness(value, new Class<?>[]{ firstClazz, secondClazz });
        Class<?> valueClass = value.getClass();

        if (firstClazz == valueClass) {
            firstBranch.accept((T1) value);
        } else if (secondClazz == valueClass) {
            secondBranch.accept((T2) value);
        }
}

Mais en plus, nous devons connaître les sous-types de l'objet cible et comparer avec les types spécifiés dans les branches. Et si un sous-type n'est pas vérifié, nous lançons une exception.

public static <V> void verifyExhaustiveness(V value, Class<?>[] inputClasses) {
        SealedAttribute data = cacheSubclasses.computeIfAbsent(value.getClass(), SealedAttribute::new);
        Class<?>[] subClasses = data.getSubClasses();
        StringBuilder builder = new StringBuilder();
        boolean flag = false;

        if (subClasses.length != inputClasses.length) {
            throw new PatternException("Require " + inputClasses.length + " subclasses. " +
                                       "But checked class has " + subClasses.length + " subclasses.");
        }

        for (Class<?> subClass : subClasses) {
            for (Class<?> inputClass : inputClasses) {
                if (subClass == inputClass) {
                    flag = true;
                    break;
                }
            }

            if (!flag) {
                builder.append(subClass).append(",");
            }

            flag = false;
        }

        if (builder.length() >= 1) {
            throw new PatternException("Must to be exhaustive, add necessary " + builder.toString() +
                                       " branches or else branch instead");
        }
}

Par conséquent, afin de ne pas calculer à chaque fois les sous-types de classe, nous pouvons mettre en cache. Dans ce cas, nous devons vérifier que la classe cible est abstraite, possède un constructeur privé et toutes les classes imbriquées héritées de la classe cible.

public final class SealedAttribute {
    private Class<?>[] subClasses;

    public SealedAttribute(Class<?> clazz) {
        Class<?> sealedClass = clazz.getSuperclass();

        if (!sealedClass.isAnnotationPresent(Sealed.class)) {
            throw new PatternException("Checked class must to be mark as sealed");
        }

        if (!Modifier.isAbstract(sealedClass.getModifiers())) {
            throw new PatternException("Checked class must to be abstract");
        }

        try {
            final Constructor<?> constructor = sealedClass.getDeclaredConstructor();

            if (!Modifier.isPrivate(constructor.getModifiers())) {
                throw new PatternException("Default constructor must to be private");
            }

            this.subClasses = sealedClass.getClasses();

            if (subClasses.length == 0) {
                throw new PatternException("Checked class must to has one or more visible subClasses");
            }

            for (Class<?> subclass : subClasses) {
                if (!Modifier.isStatic(subclass.getModifiers())) {
                    throw new PatternException("Subclass must to be static");
                }

                if (subclass.getSuperclass() != sealedClass) {
                    throw new PatternException("Subclass must to inherit from checked class");
                }
            }
        } catch (NoSuchMethodException e) {
            throw new PatternException("Checked class must to has default constructor " + e.getMessage());
        }
    }

    public Class<?>[] getSubClasses() {
        return subClasses;
    }
}

Écrivons un benchmark simple et mesurons en moyenne la différence entre la vitesse d'exécution du visiteur et le code écrit en Java pur. Le benchmark source complet peut être consulté ici .


@Warmup(iterations = 3, time = 1)
@Measurement(iterations = 3, time = 1)
@Fork(3)
@State(Scope.Thread)
@BenchmarkMode(Mode.AverageTime)
@OutputTimeUnit(TimeUnit.NANOSECONDS)
public class UnionPatternBenchmark {
     private BiColor biColor;
	
    @Setup
    public void setup() {
        biColor = new BiColor.Red();
    }

    @Benchmark
    public int matchesSealedBiExpressionPlain() {
        if (biColor instanceof BiColor.Red) {
               return 0x1;
        } else if (biColor instanceof BiColor.Blue) {
               return 0x2;
        }

        return 0x0;
    }

    @Benchmark
    public int matchesSealedBiExpressionReflective() {
        return matches(biColor).as(
                BiColor.Red.class,  r -> 0x1,
                BiColor.Blue.class, b -> 0x2
        );
    }
}	

UnionPatternBenchmark.matchesSealedBiExpressionPlain           avgt    9   5,992 ±  0,332  ns/op
UnionPatternBenchmark.matchesSealedTriExpressionPlain          avgt    9   7,199 ±  0,356  ns/op

UnionPatternBenchmark.matchesSealedBiExpressionReflective      avgt    9  45,192 ± 11,951  ns/op
UnionPatternBenchmark.matchesSealedTriExpressionReflective     avgt    9  43,413 ±  0,702  ns/op

Pour résumer, nous pouvons dire que les types scellés sont une très bonne fonctionnalité, dont l'utilisation simplifiera l'écriture de code et rendra le code plus fiable.

Le code source complet du visiteur peut être consulté sur github .

All Articles