Jenis tertutup di Jawa

Bahasa Jawa baru-baru ini mulai berkembang secara aktif. Rilis enam bulan versi Java tidak bisa tidak menyenangkan pengembang Java dengan fitur baru.

Salah satu bidang prioritas untuk pengembangan Java adalah pencocokan pola. Pencocokan pola mengungkapkan kepada pengembang kemampuan untuk menulis kode lebih fleksibel dan lebih indah, sambil tetap membuatnya jelas.

Blok kunci untuk pencocokan pola di Jawa berencana untuk merekam dan menyegel jenis.

Catatan menyediakan sintaksis ringkas untuk mendeklarasikan kelas yang merupakan pembawa sederhana dari kumpulan data yang konstan dan tidak dapat diubah.

Akan muncul di Java 14 sebagai fitur pratinjau.

record Person(String name, int age) { }

Jenis tertutup adalah kelas atau antarmuka yang memaksakan pembatasan pada kelas atau antarmuka lain yang dapat memperluas atau mengimplementasikannya. Dengan tingkat probabilitas tinggi mereka mungkin muncul di Jawa 15.

Mereka adalah jenis enum pada steroid.

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

Pengubah yang disegel digunakan untuk mendeklarasikan kelas atau antarmuka yang disegel. Daftar subtipe dapat dicantumkan pada waktu deklarasi kelas yang disegel atau antarmuka
setelah kata kunci izin. Jika subtipe dalam paket atau modul yang sama, kompiler dapat menampilkan daftar subtipe dan izin dalam deklarasi
kelas atau antarmuka yang disegel dapat dihilangkan.

Jika subtipe itu abstrak, maka ia ditandai secara implisit dengan pengubah yang disegel, kecuali ditandai secara eksplisit dengan pengubah yang tidak tersegel.

Seperti yang Anda lihat, Java memperkenalkan jenis kata kunci baru yang disebut kata kunci ditulis dgn tanda penghubung .
Kata kunci tersebut akan terdiri dari dua kata yang dipisahkan oleh tanda hubung.

Subtipe beton secara implisit menjadi final kecuali ditandai secara eksplisit dengan pengubah yang tidak disegel. Meskipun menurut saya, akan lebih baik menggunakan non-final jika Anda tidak ingin menjadikannya final.

Untuk mendukung tipe yang disegel, atribut PermittedSubtypes baru ditambahkan ke file kelas, yang menyimpan daftar subtipe.

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

Juga, untuk bekerja dengan tipe yang disegel melalui refleksi, dua metode ditambahkan ke java.lang.Class.

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

Metode pertama mengembalikan array objek java.lang.constant.ClassDesc yang mewakili daftar subtipe jika kelas ditandai dengan pengubah disegel. Jika kelas tidak ditandai dengan pengubah disegel, array kosong dikembalikan. Metode kedua mengembalikan true jika kelas atau antarmuka yang diberikan ditandai dengan pengubah disegel.

Semua ini memungkinkan kompiler untuk menentukan apakah semua subtipe disebutkan atau tidak, ketika iterasi daftar subtipe dalam ekspresi switch.

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

Cabang default adalah opsional, karena kompilator mendefinisikan semua subtipe yang valid.
Dan jika subtipe baru ditambahkan, maka kompiler akan menentukan bahwa tidak semua subtipe dianggap berganti, dan itu akan menimbulkan kesalahan dalam waktu kompilasi.

Gagasan tipe tersegel bukanlah hal baru. Misalnya, dalam bahasa Kotlin, ada kelas yang disegel. Tetapi tidak ada antarmuka yang disegel.

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

Kelas tertutup Kotlin abstrak secara implisit dan memiliki konstruktor default pribadi. Dengan demikian, subtipe harus bersarang di kelas. Di Jawa, dapat direpresentasikan sebagai berikut.

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

Dalam hal ini, dapat dijamin bahwa semua subtipe dapat ditentukan pada waktu kompilasi.

Ini semua baik dan bagus, tetapi bagaimana jika Anda ingin mencoba kelas tertutup, tetapi mereka belum keluar. Untungnya, tidak ada yang menghalangi implementasi pengunjung sederhana, seperti kapan ekspresi bekerja di Kotlin.

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

Implementasi pengunjung sangat sederhana. Kami mengambil jenis objek target dan, tergantung pada jenis cabang, kami melakukan ekspresi 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);
        }
}

Tetapi di samping itu, kita perlu mengetahui subtipe dari objek target dan membandingkannya dengan tipe-tipe yang ditentukan dalam cabang. Dan jika beberapa subtipe tidak dicentang, kami akan memberikan pengecualian.

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

Dengan demikian, agar tidak menghitung subtipe kelas setiap kali, kita dapat melakukan cache. Dalam hal ini, kita perlu memeriksa bahwa kelas target abstrak, memiliki konstruktor pribadi, dan semua kelas bersarang diwarisi dari kelas target.

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

Mari kita menulis patokan sederhana dan mengukur rata-rata seberapa besar kecepatan eksekusi pengunjung dan kode yang ditulis dalam Java murni berbeda. Tolok ukur sumber lengkap dapat dilihat di sini .


@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

Untuk meringkas, kita dapat mengatakan bahwa tipe tersegel adalah fitur yang sangat baik, penggunaannya akan menyederhanakan penulisan kode dan membuat kode lebih dapat diandalkan.

Kode sumber lengkap dari pengunjung dapat dilihat di github .

All Articles