Java - Sealed Classes
עד JAVA 17 , כשבנינו class חדש, היו לנו שתי אופציות ליורשה:
- ירושה פתוחה לחלוטין: ניתן לייצור מחלקה חדשה שיורשת ממך (extends).
- חסימה מוחלטת (final): אף אחד בעולם לא יכול לרשת את המחלקה שלך.
ואז הגיע חידוש מטורף בתחום, למה לא לעשות איזה "יצור ביניים", שמאפשר מראש למי ניתן להוריש וכל השאר חסומים?
פה נכנס 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 sealedpublic: גישה ציבורית, כל מחלקה בפרויקט יכולה לראות ולהשתמש בממשק הזה.
sealed: מילת המפתח החשובה ביותר כאן! היא אומרת לקומפיילר: שים לב, מי שיכול לרשת את זה מוגבל, וזאת הרשימה!
Permits: מגדירה באופן יחודי וברור את "המשפחה המצומצמת". רק המחלקות Circle (עיגול) ו Square (ריבוע) רשאיות לממש את הממשק Shape. אם מחלקה אחרת (למשל Triangle) תנסה לכתוב implements Shape, הקומפיילר יזרוק שגיאה מיד.
חוק שלושת ה Modifiers לתתי המחלקות שיורשות מ sealed
ברגע שמחלקה או ממשק מוגדרים כ sealed, כל מחלקה שמופיעה ברשימת ה permits חייבת להגדיר את עצמה באחת מתוך שלוש אפשרויות בלבד, אחרת הקומפיילר יזרוק שגיאה:
- final: אף אחד אחר לא יכול לרשת את תת המחלקה הזו (הנפוץ ביותר, במיוחד בשילוב עם Records).
- sealed: ממשיך את השרשרת החצי סגורה. כלומר, תת המחלקות בעצמן הופכות ל sealed ודורשות רשימת permits משלהן עבור הדור הבא.
- non-sealed: "שובר" את הסגירות ומחזיר את המצב לקדמותו עבור תת המחלקה הזו. כלומר, ממנה והלאה, כל אחד בעולם יכול לרשת אותה בלי הגבלה.
דוגמה המשלבת את כל סוגי תת המחלקות האפשריות :
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 נועדו בדיוק בשביל דבר אחד: לשאת נתונים טהורים (Data Carriers) בלי לוגיקה מסובכת, והם מטבעם final, ניתן לקרוא בהרחבה על הנושא
כאן
.
- Sealed Classes נועדו בדיוק בשביל להגדיר משפחה סגורה וידועה מראש של תתי מחלקות.
ברגע שמחברים אותם, אנחנו מקבלים מבנה שבו הממשק הסגור מייצג מושג כללי (למשל: תשלום, תוצאת פעולה, או שגיאה), וה 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 האלו כדי:
- לזהות אוטומטית, אילו תרחישים קיימים במערכת מבלי להזריק להן רשימות ידניות.
- לבנות סריאליזציה חכמה לJSON, שמזהה מיד איזה Record ספציפי מגיע מהרשת ומנתבת אותו למבנה הנתונים הנכון.