Tipos selados em Java

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 selada
apó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 da
classe 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 .

All Articles