A linguagem Java começou recentemente a se desenvolver ativamente. O lançamento de seis meses das versões Java não pode deixar de agradar o desenvolvedor Java com novos recursos.Uma das áreas prioritárias para o desenvolvimento Java é a correspondência de padrões. A correspondência de padrões revela ao desenvolvedor a capacidade de escrever código de maneira mais flexível e bonita, deixando-o claro.Os principais blocos de correspondência de padrões em Java estão planejando gravar e tipos selados.Os registros fornecem uma sintaxe concisa para declarar classes que são portadoras simples de conjuntos de dados constantes e imutáveis.Aparecerá no Java 14 como um recurso de visualização.record Person(String name, int age) { }
Tipos selados são classes ou interfaces que impõem restrições a outras classes ou interfaces que podem estendê-las ou implementá-las. Com um alto grau de probabilidade, eles podem aparecer no Java 15.Eles são do tipo enum em esteróides.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 {}
Um modificador selado é usado para declarar uma classe ou interface selada. A lista de subtipos pode ser listada no momento da declaração da classe ou interface seladaapós a palavra-chave permit. Se os subtipos estiverem no mesmo pacote ou módulo, o compilador poderá exibir uma lista de subtipos e autorizações na declaração daclasse ou interface selada poderão ser omitidas.Se o subtipo for abstrato, ele será marcado implicitamente com um modificador lacrado, a menos que explicitamente marcado com um modificador não lacrado.Como você pode ver, o Java apresenta um novo tipo de palavra-chave, chamado hifenizado .Essas palavras-chave consistirão em duas palavras separadas por um traço.Os subtipos de concreto tornam-se implicitamente finais, a menos que sejam explicitamente marcados com um modificador não selado. Embora me pareça, seria melhor usar não final se você não quiser torná-lo final.Para dar suporte a tipos selados, um novo atributo PermittedSubtypes é adicionado aos arquivos de classe, que armazena uma lista de subtipos.PermittedSubtypes_attribute {
u2 attribute_name_index;
u4 attribute_length;
u2 permitted_subtypes_count;
u2 classes[permitted_subtypes_count];
}
Além disso, para trabalhar com tipos selados por meio de reflexão, dois métodos são adicionados ao java.lang.Class.java.lang.constant.ClassDesc<?>[] getPermittedSubtypes()
boolean isSealed()
O primeiro método retorna uma matriz de objetos java.lang.constant.ClassDesc que representam uma lista de subtipos se a classe estiver marcada com um modificador selado. Se a classe não estiver marcada com um modificador selado, uma matriz vazia será retornada. O segundo método retorna true se a classe ou interface especificada estiver marcada com um modificador selado.Tudo isso permite que o compilador determine se todos os subtipos são enumerados ou não ao iterar sobre a lista de subtipos em uma expressão de opção.var result = switch (color) {
case BiColor bc -> 0x1;
case TriColor tc -> 0x2;
}
A ramificação padrão é opcional, porque o compilador define todos os subtipos válidos.E se um novo subtipo for adicionado, o compilador determinará que nem todos os subtipos são considerados no switch e gerará um erro no tempo de compilação.A idéia de tipos selados não é nova. Por exemplo, no idioma Kotlin, existem classes seladas. Mas não há interfaces seladas.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
As classes seladas Kotlin são implicitamente abstratas e têm um construtor padrão privado. Assim, os subtipos devem ser aninhados na classe. Em Java, ele pode ser representado da seguinte maneira.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 {}
}
Nesse caso, pode-se garantir que todos os subtipos possam ser determinados em tempo de compilação.Tudo está bem, mas e se você quiser experimentar aulas seladas, mas elas ainda não foram publicadas. Felizmente, nada impede a implementação de um simples visitante, assim como a expressão when funciona no Kotlin. matches(color).as(
Color.BiColor.class, bc -> System.out.println("bi color: " + bc),
Color.TriColor.class, tc -> System.out.println("tri color: " + tc)
);
A implementação do visitante é muito simples. Pegamos o tipo do objeto de destino e, dependendo do tipo de ramificação, executamos expressões 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);
}
}
Além disso, precisamos conhecer os subtipos do objeto de destino e comparar com os tipos especificados nas ramificações. E se algum subtipo não estiver marcado, lançaremos uma exceção.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");
}
}
Portanto, para não calcular os subtipos de classe a cada vez, podemos armazenar em cache. Nesse caso, precisamos verificar se a classe de destino é abstrata, tem um construtor privado e todas as classes aninhadas herdadas da classe de destino.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;
}
}
Vamos escrever uma referência simples e medir em média quanto a velocidade de execução do visitante e o código escrito em Java puro diferem. A referência completa da fonte pode ser vista aqui .
@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
Para resumir, podemos dizer que os tipos selados são um recurso muito bom, cuja utilização simplificará a escrita do código e tornará o código mais confiável.O código fonte completo do visitante pode ser visualizado no github .