Tipos sellados en Java

El lenguaje Java ha comenzado recientemente a desarrollarse activamente. La versión de seis meses de las versiones de Java no puede dejar de complacer al desarrollador de Java con nuevas características.

Una de las áreas prioritarias para el desarrollo de Java es la coincidencia de patrones. La coincidencia de patrones revela al desarrollador la capacidad de escribir código de manera más flexible y hermosa, mientras lo deja claro.

Los bloques clave para la coincidencia de patrones en Java planean grabar y sellar tipos.

Los registros proporcionan una sintaxis concisa para declarar clases que son portadores simples de conjuntos de datos constantes e inmutables.

Aparecerá en Java 14 como una función de vista previa.

record Person(String name, int age) { }

Los tipos sellados son clases o interfaces que imponen restricciones a otras clases o interfaces que pueden ampliarlas o implementarlas. Con un alto grado de probabilidad, pueden aparecer en Java 15.

Son tipos de enumeración en los esteroides.

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 modificador sellado se utiliza para declarar una clase o interfaz sellada. La lista de subtipos se puede enumerar en el momento de la declaración de la clase o interfaz sellada
después de la palabra clave de permisos. Si los subtipos están en el mismo paquete o módulo, el compilador puede mostrar una lista de subtipos y
se pueden omitir los permisos en la declaración de la clase o interfaz sellada .

Si el subtipo es abstracto, entonces se marca implícitamente con un modificador sellado, a menos que se marque explícitamente con un modificador no sellado.

Como puede ver, Java introduce un nuevo tipo de palabras clave llamadas palabras clave con guiones .
Dichas palabras clave consistirán en dos palabras separadas por un guión.

Los subtipos concretos se vuelven implícitamente finales a menos que estén marcados explícitamente con un modificador no sellado. Aunque me parece, sería mejor usar el no final si no quieres hacerlo definitivo.

Para admitir tipos sellados, se agrega un nuevo atributo PermittedSubtypes a los archivos de clase, que almacena una lista de subtipos.

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

Además, para trabajar con tipos sellados a través de la reflexión, se agregan dos métodos a java.lang.Class.

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

El primer método devuelve una matriz de objetos java.lang.constant.ClassDesc que representan una lista de subtipos si la clase está marcada con un modificador sellado. Si la clase no está marcada con un modificador sellado, se devuelve una matriz vacía. El segundo método devuelve verdadero si la clase o interfaz dada está marcada con un modificador sellado.

Todo esto permite que el compilador determine si todos los subtipos están enumerados o no, al iterar sobre la lista de subtipos en una expresión de cambio.

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

La rama predeterminada es opcional, porque el compilador define todos los subtipos válidos.
Y si se agrega un nuevo subtipo, el compilador determinará que no todos los subtipos se consideran en el conmutador, y arrojará un error en el tiempo de compilación.

La idea de los tipos sellados no es nueva. Por ejemplo, en el idioma Kotlin, hay clases selladas. Pero no hay interfaces selladas.

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

Las clases selladas de Kotlin son implícitamente abstractas y tienen un constructor privado predeterminado. En consecuencia, los subtipos deben estar anidados en la clase. En Java, se puede representar de la siguiente manera.

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

En este caso, se puede garantizar que todos los subtipos se pueden determinar en tiempo de compilación.

Todo esto está muy bien, pero ¿qué pasa si quieres probar clases selladas, pero aún no han salido? Afortunadamente, nada impide la implementación de un visitante simple, al igual que la expresión cuándo funciona en 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 implementación del visitante es muy simple. Tomamos el tipo del objeto de destino y, dependiendo del tipo de rama, realizamos expresiones 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);
        }
}

Pero además, necesitamos conocer los subtipos del objeto de destino y comparar con los tipos especificados en las ramas. Y si algún subtipo no está marcado, lanzaremos una excepción.

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

En consecuencia, para no calcular los subtipos de clase cada vez, podemos almacenar en caché. En este caso, debemos verificar que la clase de destino sea abstracta, tenga un constructor privado y que todas las clases anidadas se hereden de la clase 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;
    }
}

Escribamos un punto de referencia simple y midamos en promedio cuánto difieren la velocidad de ejecución del visitante y el código escrito en Java puro. El punto de referencia de la fuente completa se puede ver aquí .


@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 decir que los tipos sellados son una muy buena característica, cuyo uso simplificará la escritura del código y hará que el código sea más confiable.

El código fuente completo del visitante se puede ver en github .

All Articles