Java - Sealed Classes

עד JAVA 17 , כשבנינו class חדש, היו לנו שתי אופציות ליורשה:

ואז הגיע חידוש מטורף בתחום, למה לא לעשות איזה "יצור ביניים", שמאפשר מראש למי ניתן להוריש וכל השאר חסומים?

פה נכנס Sealed Classes , מחלקה שנותנת לנו להחליט מי יכול לרשת ומי לא. העיקרון של הירושה במחלקה הזאת עובד על Whitelist אנחנו מגדירים מראש מה מותר , וזה ואך ורק זה יכול לרשת מהמחלקקה שלנו.

לדוגמה:

// We define a sealed interface and explicitly state who can implement it
public sealed interface Shape permits Circle, Square {
    double area();
}

// Allowed subclass 1
public final class Circle implements Shape {
    double radius;
    public double area() { return Math.PI * radius * radius; }
}

// Allowed subclass 2
public final class Square implements Shape {
    double side;
    public double area() { return side * side; }
}

יש לשים לב לשינוי בכתיבה, בואו נפרק את מילות המפתח החשובות בקוד :

תתי המחלקות חייבות להיות באותו package או באותו Module, במידה והמערכת שלנו בנויה בצורת מודלים.

public sealed

public: גישה ציבורית, כל מחלקה בפרויקט יכולה לראות ולהשתמש בממשק הזה.

sealed: מילת המפתח החשובה ביותר כאן! היא אומרת לקומפיילר: שים לב, מי שיכול לרשת את זה מוגבל, וזאת הרשימה!

Permits: מגדירה באופן יחודי וברור את "המשפחה המצומצמת". רק המחלקות Circle (עיגול) ו Square (ריבוע) רשאיות לממש את הממשק Shape. אם מחלקה אחרת (למשל Triangle) תנסה לכתוב implements Shape, הקומפיילר יזרוק שגיאה מיד.

חוק שלושת ה Modifiers לתתי המחלקות שיורשות מ sealed

ברגע שמחלקה או ממשק מוגדרים כ sealed, כל מחלקה שמופיעה ברשימת ה permits חייבת להגדיר את עצמה באחת מתוך שלוש אפשרויות בלבד, אחרת הקומפיילר יזרוק שגיאה:

דוגמה המשלבת את כל סוגי תת המחלקות האפשריות :

public class HierarchyExample {

    // 1. The top-level sealed interface. It explicitly permits only two branches.
    public sealed interface Vehicle permits Car, Truck {
        String getType();
    }

    // 2. Branch A: 'final' closes the inheritance chain completely.
    // No one can extend 'Car'.
    public final class Car implements Vehicle {
        @Override
        public String getType() {
            return "Car (Sealed branch closed with final)";
        }
    }

    // 3. Branch B: 'sealed' continues the restriction down the tree.
    // It must explicitly state who can extend it next.
    public sealed interface Truck extends Vehicle permits HeavyTruck, LightTruck {
        int getCapacity();
    }

    // 3.1. Sub-branch 1: 'final' closes this specific truck type.
    public final class HeavyTruck implements Truck {
        @Override
        public String getType() { return "Heavy Truck"; }
        @Override
        public int getCapacity() { return 20000; }
    }

    // 3.2. Sub-branch 2: 'non-sealed' breaks the restriction!
    // It reopens the hierarchy, allowing anyone to extend 'LightTruck' freely.
    public non-sealed class LightTruck implements Truck {
        @Override
        public String getType() { return "Light Truck"; }
        @Override
        public int getCapacity() { return 3000; }
    }

    // 4. Because LightTruck is 'non-sealed', external or custom extensions are now allowed:
    public static class DeliveryVan extends LightTruck {
        // This is completely legal because LightTruck is non-sealed!
    }
}

Sealed Classes ו Records

השילוב של Sealed Interfaces יחד עם Records נחשב לאחד התכונות הטובות ביותר ב Java 21 ומעלה. היא מאפשרת למפתחים לממש ארכיטקטורה שנקראת בשפות תכנות פונקציונליות Algebraic Data Types (ADTs) כלומר, מבני נתונים מתמטיים/לוגיים סגורים.

למה הם מתאימים זה לזה בצורה מושלמת?

ברגע שמחברים אותם, אנחנו מקבלים מבנה שבו הממשק הסגור מייצג מושג כללי (למשל: תשלום, תוצאת פעולה, או שגיאה), וה Records המורשים מייצגים את התרחישים האפשריים הספציפיים שלו.

דוגמה קלאסית: ניהול תוצאת פעולה (Result / Either) במקום להשתמש בחריגות (Exceptions) יקרות או ב null מסוכן, אפשר להגדיר תוצאה שמחזירה או הצלחה או כישלון:

public sealed interface OperationResult permits OperationResult.Success, OperationResult.Failure {

    record Success(Object data) implements OperationResult {}
    record Failure(String errorCode, String errorMessage) implements OperationResult {}

}

היתרון: ה Records יושבים ישירות בתוך הממשק, או באותו package. הם קצרים מאוד לכתיבה (ללא Getter או קונסטרקטורים מיותרים).

תמיכה מלאה ב Reflection

מכיוון ש sealed הוא מנגנון שמשנה את מבנה המחלקות ב Java, מערכת ה Reflection API (שמאפשרת לבדוק מחלקות ואובייקטים בזמן ריצה) עודכנה באופן מלא כדי לאפשר בדיקה של המבנים האלו.

הנה כמה מתודות מרכזיות שנוספו למחלקת java.lang.Class ומאפשרות לשלוט ולחקור מחלקות sealed בזמן ריצה:

isSealed

אם יש לנו אובייקט או מחלקה, ואנחנו רוצים לדעת אם היא מוגדרת כ sealed:

Class classForChack = Shape.class;
boolean isItSealed = classForChack.isSealed(); // return true

שליפת רשימת היורשים getPermittedSubclasses

המתודה הזו מאפשרת לשלוף מערך של כל המחלקות שקיבלו יכולת לרשת את המחלקה, מתוך פקודת ה permits:

Class[] permittedSubclasses = Shape.getPermittedSubclasses();

for (Class sub : permittedSubclasses) {
    System.out.println("Allowed subclass: " + sub.getName());
}

בדיקה האם מחלקה מממשת ישירות מחלקה סגורה

בנוסף, אפשר לבדוק את הקשר ההדדי בין המחלקות בעזרת מתודות קיימות כמו isInstance או לבדוק את ה Superclass / Interfaces כרגיל ( getInterfaces/getSuperclass).

ספריות מתקדמות (כמו ספריות מיפוי, ספריות JSON כמו Jackson, או פריימוורקים של Serialization) משתמשות במתדות ה Reflection האלו כדי:

🏠 Back to Orly's Code Corner