Versiegelte Typen in Java

Die Java-Sprache hat vor kurzem begonnen, sich aktiv zu entwickeln. Die sechsmonatige Veröffentlichung von Java-Versionen kann dem Java-Entwickler mit neuen Funktionen nur gefallen.

Einer der vorrangigen Bereiche für die Java-Entwicklung ist der Pattern Matching. Der Mustervergleich zeigt dem Entwickler die Möglichkeit, Code flexibler und schöner zu schreiben, während er klar bleibt.

Die Schlüsselblöcke für den Mustervergleich in Java planen das Aufzeichnen und Versiegeln von Typen.

Datensätze bieten eine präzise Syntax zum Deklarieren von Klassen, die einfache Träger konstanter, unveränderlicher Datensätze sind.

Wird in Java 14 als Vorschaufunktion angezeigt.

record Person(String name, int age) { }

Versiegelte Typen sind Klassen oder Schnittstellen, die anderen Klassen oder Schnittstellen, die sie erweitern oder implementieren können, Einschränkungen auferlegen. Mit hoher Wahrscheinlichkeit können sie in Java 15 auftreten.

Es handelt sich um Aufzählungstypen für Steroide.

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

Ein versiegelter Modifikator wird verwendet, um eine versiegelte Klasse oder Schnittstelle zu deklarieren. Die Liste der Untertypen kann zum Deklarationszeitpunkt der versiegelten Klasse oder Schnittstelle
nach dem Schlüsselwort allow aufgeführt werden. Befinden sich die Untertypen im selben Paket oder Modul, kann der Compiler eine Liste der Untertypen anzeigen und Zulassungen in der Deklaration der versiegelten
Klasse oder Schnittstelle weglassen.

Wenn der Subtyp abstrakt ist, wird er implizit mit einem versiegelten Modifikator markiert, sofern er nicht explizit mit einem nicht versiegelten Modifikator markiert ist.

Wie Sie sehen können, führt Java einen neuen Typ von Schlüsselwörtern ein, die als getrennte Schlüsselwörter bezeichnet werden .
Solche Schlüsselwörter bestehen aus zwei Wörtern, die durch einen Bindestrich getrennt sind.

Konkrete Subtypen werden implizit endgültig, sofern sie nicht explizit mit einem nicht versiegelten Modifikator gekennzeichnet sind. Obwohl es mir scheint, wäre es besser, nicht endgültig zu verwenden, wenn Sie es nicht endgültig machen möchten.

Um versiegelte Typen zu unterstützen, wird Klassendateien ein neues PermittedSubtypes-Attribut hinzugefügt, in dem eine Liste von Untertypen gespeichert ist.

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

Um mit versiegelten Typen durch Reflexion zu arbeiten, werden java.lang.Class zwei Methoden hinzugefügt.

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

Die erste Methode gibt ein Array von java.lang.constant.ClassDesc-Objekten zurück, die eine Liste von Untertypen darstellen, wenn die Klasse mit einem versiegelten Modifikator markiert ist. Wenn die Klasse nicht mit einem versiegelten Modifikator markiert ist, wird ein leeres Array zurückgegeben. Die zweite Methode gibt true zurück, wenn die angegebene Klasse oder Schnittstelle mit einem versiegelten Modifikator markiert ist.

Auf diese Weise kann der Compiler feststellen, ob alle Untertypen aufgelistet sind oder nicht, wenn er die Liste der Untertypen in einem Schalterausdruck durchläuft.

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

Der Standardzweig ist optional, da der Compiler alle gültigen Untertypen definiert.
Wenn ein neuer Subtyp hinzugefügt wird, stellt der Compiler fest, dass nicht alle Subtypen im Switch berücksichtigt werden, und gibt einen Fehler in der Kompilierungszeit aus.

Die Idee der versiegelten Typen ist nicht neu. In der Kotlin-Sprache gibt es beispielsweise versiegelte Klassen. Es gibt jedoch keine versiegelten Schnittstellen.

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

Versiegelte Kotlin-Klassen sind implizit abstrakt und haben einen privaten Standardkonstruktor. Dementsprechend müssen Untertypen in der Klasse verschachtelt sein. In Java kann es wie folgt dargestellt werden.

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

In diesem Fall kann garantiert werden, dass alle Subtypen zur Kompilierungszeit bestimmt werden können.

Das ist alles schön und gut, aber was ist, wenn Sie versiegelte Klassen ausprobieren möchten, diese aber noch nicht herausgekommen sind? Glücklicherweise verhindert nichts die Implementierung eines einfachen Besuchers, so wie der Wann-Ausdruck in Kotlin funktioniert.

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

Die Implementierung der Besucher ist sehr einfach. Wir nehmen den Typ des Zielobjekts und führen je nach Art der Verzweigung Lambda-Ausdrücke aus.

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

Darüber hinaus müssen wir die Untertypen des Zielobjekts kennen und mit den in den Zweigen angegebenen Typen vergleichen. Und wenn ein Subtyp nicht aktiviert ist, wird eine Ausnahme ausgelöst.

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

Dementsprechend können wir zwischenspeichern, um nicht jedes Mal Klassensubtypen zu berechnen. In diesem Fall müssen wir überprüfen, ob die Zielklasse abstrakt ist, einen privaten Konstruktor hat und alle verschachtelten Klassen von der Zielklasse geerbt werden.

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

Schreiben wir einen einfachen Benchmark und messen im Durchschnitt, wie stark sich die Ausführungsgeschwindigkeit des Besuchers und der in reinem Java geschriebene Code unterscheiden. Der vollständige Quell-Benchmark kann hier eingesehen werden .


@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

Zusammenfassend können wir sagen, dass versiegelte Typen eine sehr gute Funktion sind, deren Verwendung das Schreiben von Code vereinfacht und den Code zuverlässiger macht.

Der vollständige Quellcode des Besuchers kann auf github eingesehen werden .

All Articles