Java中的密封类型

Java语言最近开始积极开发。 Java版本的六个月发行版不能不向Java开发人员提供新功能。

Java开发的优先领域之一是模式匹配。模式匹配向开发人员展示了更灵活,更漂亮地编写代码的能力,同时使代码保持清晰。

Java中用于模式匹配的关键块正计划记录和密封类型。

记录提供了简洁的语法来声明类,这些类是常量,不变数据集的简单载体。

将在Java 14中显示为预览功能。

record Person(String name, int age) { }

密封类型是对可以扩展或实现它们的其他类或接口施加限制的类或接口。

它们很有可能出现在Java 15中。它们是类固醇上的枚举类型。

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

密封的修饰符用于声明密封的类或接口。子类型列表可以
在allows关键字之后的密封类或接口的声明时列出。如果子类型位于相同的程序包或模块中,则编译器可以显示子类型列表,并且可以在密封
类或接口的声明中省略允许

如果子类型是抽象的,则除非使用非密封的修饰符显式标记,否则它将隐式地用密封的修饰符标记。

如您所见,Java引入了一种新型的关键字,称为hyphenated keyword
此类关键字将包含两个单词,并用破折号分隔。

除非明确用非密封修饰符标记,否则具体的子类型将隐式地变为最终类型。虽然在我看来,但如果您不想将其定为决赛,最好使用非决赛。

为了支持密封类型,将新的PermittedSubtypes属性添加到类文件中,该属性存储子类型列表。

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

另外,为了通过反射处理密封类型,向java.lang.Class添加了两种方法。

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

第一个方法返回java.lang.constant.ClassDesc对象的数组,如果该类用密封修饰符标记,则该数组代表子类型的列表。如果该类未使用密封的修饰符标记,则返回一个空数组。如果给定的类或接口用密封的修饰符标记,则第二种方法返回true。

所有这些使编译器可以在遍历switch表达式中的子类型列表时确定是否枚举了所有子类型。

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

默认分支是可选的,因为编译器定义了所有有效的子类型。
并且,如果添加了新的子类型,则编译器将确定并非所有子类型都在switch中被考虑,并且它将在编译时抛出错误。

密封类型的想法并不新鲜。例如,在Kotlin语言中,有密封的类。但是没有密封的接口。

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

Kotlin密封类是隐式抽象的,并具有私有的默认构造函数。因此,子类型必须嵌套在类中。在Java中,它可以表示如下。

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

在这种情况下,可以保证可以在编译时确定所有子类型。

这一切都很好,但是如果您想尝试密封类,但还没有出来怎么办。幸运的是,没有什么可以阻止简单访问者的实现,就像when-expression在Kotlin中起作用一样。

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

访客实现非常简单。我们采用目标对象的类型,并根据分支的类型执行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);
        }
}

但是此外,我们需要了解目标对象的子类型,并将其与分支中指定的类型进行比较。如果未检查某些子类型,我们将抛出异常。

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

因此,为了不每次都计算类的子类型,我们可以进行缓存。在这种情况下,我们需要检查目标类是否为抽象类,是否具有私有构造函数以及所有从目标类继承的嵌套类。

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

让我们写一个简单的基准,并平均衡量访问者的执行速度和用纯Java编写的代码有多少不同。完整的基准测试可以在这里查看


@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

总而言之,我们可以说密封类型是一个非常好的功能,使用它可以简化代码的编写并使代码更可靠。

访问者的完整源代码可以在github上查看

All Articles