أنواع مختومة في جافا

بدأت لغة جافا في التطور بنشاط في الآونة الأخيرة. لا يمكن لإصدار 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 {}

يتم استخدام معدّل مختوم للإعلان عن فئة أو واجهة مختومة. يمكن سرد قائمة الأنواع الفرعية في وقت التصريح للفئة أو الواجهة المختومة
بعد الكلمة الأساسية للتصاريح. إذا كانت الأنواع الفرعية في نفس الحزمة أو الوحدة النمطية ، فيمكن للمترجم سرد الأنواع الفرعية والتصاريح في إعلان
الفئة أو الواجهة المختومة يمكن حذفها.

إذا كان النوع الفرعي مجرّدًا ، فسيتم وضع علامة ضمنيًا على معدّل مختوم ، ما لم يتم وضع علامة صريحة على معدّل غير مختوم.

كما ترى ، تقدم Java نوعًا جديدًا من الكلمات الرئيسية يسمى الكلمات الأساسية الواصلة .
ستتألف هذه الكلمات الرئيسية من كلمتين مفصولتين بشرطة.

تصبح الأنواع الفرعية الملموسة نهائية بشكل ضمني ما لم يتم وضع علامة صريحة عليها بمعدّل غير مغلق. على الرغم من أنه يبدو لي ، سيكون من الأفضل استخدام غير نهائي إذا كنت لا تريد جعله نهائيًا.

من أجل دعم الأنواع المختومة ، تتم إضافة سمة 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 إذا تم تمييز الفئة أو الواجهة المحددة بمُعدِّل مختوم.

كل هذا يسمح للمترجم بتحديد ما إذا كان يتم سرد جميع الأنواع الفرعية أم لا ، عند التكرار على قائمة الأنواع الفرعية في تعبير التبديل.

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

الفرع الافتراضي اختياري ، لأن المترجم يعرف جميع الأنواع الفرعية الصالحة.
وإذا تمت إضافة نوع فرعي جديد ، فسيحدد المترجم أنه لا يتم اعتبار جميع الأنواع الفرعية في التبديل ، وسوف يلقي خطأ في وقت الترجمة.

فكرة الأنواع المختومة ليست جديدة. على سبيل المثال ، في لغة 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 {}
}

في هذه الحالة ، يمكن ضمان أن جميع الأنواع الفرعية يمكن تحديدها في وقت الترجمة.

كل هذا جيد وجيد ، ولكن ماذا لو كنت تريد تجربة فصول مختومة ، لكنهم لم يخرجوا بعد. لحسن الحظ ، لا يوجد شيء يمنع تنفيذ زائر بسيط ، تمامًا مثل عندما يعمل التعبير في Kotlin.

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

تنفيذ الزائر بسيط للغاية. نأخذ نوع الكائن المستهدف ، ونعتمد على نوع الفرع ، نقوم بتعابير لامدا.

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

دعنا نكتب مقياسًا بسيطًا ونقيس في المتوسط ​​مدى اختلاف سرعة تنفيذ الزائر والرمز المكتوب بلغة جافا الخالصة. يمكن الاطلاع على معيار المصدر الكامل هنا .


@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

للتلخيص ، يمكننا القول أن الأنواع المختومة هي ميزة جيدة جدًا ، حيث يبسط استخدامها كتابة التعليمات البرمجية ويجعلها أكثر موثوقية.

يمكن عرض كود المصدر الكامل للزائر على جيثب .

All Articles