Switch Expressions Vs. Switch Statement

החל מגרסה Java 14 ישנה מהפכה במבנה ה switch המסורתי. מעבר משימוש בו כ Statement (פקודה שמבצעת פעולות) לשימוש בו כ Expression (ביטוי שמחזיר ערך ישירות).

יתרונות Switch Expressions (חדש - Java 14 ומעלה) לעומת Switch Statement (ישן - Java 13 ומטה).

1. אין Fall-through (אין זליגה אוטומטית)

ב Switch Statement (ישן): אם שכחת לכתוב break בסוף case מסוים, הקוד המשיך לרוץ אוטומטית לתוך ה-case הבא (תופעה שנקראת Fall-through). זו הייתה אחת הסיבות הנפוצות לבאגים ב Java.

לדוגמה קטע קוד עם הגישה הישנה:

String dayType = "SATURDAY";
String message;

switch (dayType) {
    case "MONDAY":
    case "TUESDAY":
    case "WEDNESDAY":
    case "THURSDAY":
    case "FRIDAY":
        // This runs for weekdays
        System.out.println("Processing a standard workday...");
        message = "Weekday";
        break; // MANDATORY: Without this break, code would leak into the next case!

    case "SATURDAY":
    case "SUNDAY":
        // This runs for weekends
        message = "Weekend";
        break; // MANDATORY: Without this break, code would leak into default!

    default:
        // This runs if no other case matches
        System.out.println("Invalid day provided.");
        message = "Unknown";
        break;
}

System.out.println("Result: " + message);

Switch Expressions (חדש):

לדוגמה קטע קוד עם הגישה החדשה:

String dayType = "SATURDAY";

String message = switch (dayType) {
    case "MONDAY", "TUESDAY", "WEDNESDAY", "THURSDAY", "FRIDAY" -> {
        // This block runs ONLY for weekdays and exits immediately afterwards
        System.out.println("Processing a standard workday...");
        yield "Weekday";
    }
    // This runs for weekends
    case "SATURDAY", "SUNDAY" -> "Weekend";

    default -> {
        // This only executes if none of the above match
        System.out.println("Invalid day provided.");
        yield "Unknown";
    }
};

System.out.println("Result: " + message);

2. קיבוץ ערכים בשורה אחת (Multiple Constants)

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

case "MONDAY", "FRIDAY", "SUNDAY" -> 6;

ולא:

case "MONDAY":
case "TUESDAY":
case "WEDNESDAY":
case "THURSDAY":
case "FRIDY":

3. חובה לכסות את כל האפשרויות (Exhaustiveness)

כאשר משתמשים ב-Switch Expressions (חדש), הקומפיילר דורש שהקוד יכסה את כל הערכים האפשריים מבחינה לוגית, אחרת הקוד לא יתקמפל ותתקבל שגיאת קומפילציה.
אם נכתוב switch על Enum, הקומפיילר מצפה שנכסה את כל הערכים האפשריים של אותו Enum.
אם מדובר במשתנה רגיל (כמו String או int), חובה לכלול תמיד סעיף default שיסגור את כל המקרים הבלתי צפויים.

4. ערך החזרה אחיד מכל ה Cases

מכיוון שמשתנה מקבל את הערך מתוצאת ה-switch, כל ה-caseים כולל ה-default חייבים להחזיר את אותו סוג טיפוס (Type).
אי אפשר ש-case אחד יחזיר String ואחר יחזיר int. הקומפיילר ידרוש עקביות במשתנים המוחזרים.

5. שילוב עם שגיאות (Exceptions)

בתוך case בין אם בחץ או בתוך בלוק עם yield, מותר לזרוק שגיאה Exception אם משהו לא תקין. הקומפיילר חכם מספיק כדי להבין ששגיאה שנזרקת עוצרת את הריצה, ולכן היא לא פוגעת בדרישה להחזיר ערך תקין.

int val = switch (input) {
    case 1 -> 10;
    default -> throw new IllegalArgumentException("Invalid input: " + input);
};
⚠️ שימו לב!

כאשר עוברים ל Java 14 ומעלה, יש נטייה להתבלבל בין הגישה הישנה לחדשה. ולכן אם נכתוב בטעות return באמצע הswitch נעצור את כל המטודה ולא נמשיך הלאה במקרים האפשריים.

⚠️ דוגמה לשימוש לא נכון!
// Buggy implementation using return
public static String evaluateStatusBuggy(String status) {
    String message = switch (status) {
        case "ACTIVE" -> {
            System.out.println("Executing logic for ACTIVE status...");
            // BUG: 'return' exits the ENTIRE evaluateStatusBuggy method immediately!
            // It does NOT just return a value to the switch expression.
            return "Accidental early exit!";
        }
        default -> {
            yield "Unknown status.";
        }
    };
    // This line is dead code for "ACTIVE" because the method already returned above
    return message;
}

ההבדל בארכיטקטורה (Scope & Context):

כאשר מפתחים חדשים/ישנים משדרגים גרסת Java, ורואים סוגריים מסולסלים {} בתוך ה-case, ההרגל לפרש זאת אוטומטית כמו מתודה קטנה, ולכן האינסטינקט הראשוני הוא לכתוב return כדי להחזיר את הערך.

מה קורה בפועל כשמשתמשים ב-return בטעות?

מילת המפתח return אומרת ל-Java: "תעצרי הכל, צאי מיד מהמתודה הנוכחית וחזרי למקום שבו קראת לה".
כתוצאה מכך, הקוד שאמור לקבל את תוצאת ה-switch (במקרה שלנו המשתנה message) מעולם לא מקבל כלום, והשורה שאחרי ה-switch במתודה הופכת ל-Dead Code – קוד מת שלאעולם לא יגיעו אליו.

התפקיד הייחודי של yield:

לעומת זאת, yield הוא פקודה ייעודית שנוצרה אך ורק עבור switch expressions. היא אומרת ל-Java: אל תצאי מהמתודה! רק תעבירי את הערך הספציפי הזה החוצה אל תוך המשתנה שמחכה בתוצאת ה-switch, ותני למתודה להמשיך לרוץ רגיל.


Java 21

החל מגרסה Java 21 הושק באופן רשמי טיפול ב-null בתוך switch! (ניתן למצוא אותו החל מ-Java 17 בחלק מהגרסאות כ-Preview Feature)

בגרסאות עבר, אם היינו שולחים ערך שהוא null לתוך switch היינו מקבלים NullPointerException. עם case null, ה switch יזהה שהאובייקט null ולא יעוף בשגיאה!

String result = switch (input) {
    case null -> "It was null!";
    case String s -> "String: " + s;
    default -> "Something else";
};
⚠️ שימו לב!

בהרבה מקרים, מבחינה לוגית, אין הבדל מבחינת התנהגות בין קבלת אובייקט null לבין קבלת אובייקט מסוג לא ידוע, מכיוון ששני המקרים מהווים קלט לא תקין. במקום לכתוב שתי שורות נפרדות שעושות בדיוק את אותו הדבר, התחביר המודרני מאפשר לאחד אותם עם פסיק:
case null, default ->
ולכן, לא חייבים לבדוק אם האובייקט null בשורה הראשונה ב switch.

לדוגמה:

public class NullDefaultExample {
    public static String evaluateInput(Object input) {
        return switch (input) {
            // Handles valid string inputs
            case String s -> "Valid string: " + s;

            // Combines null and any other unexpected types into a single branch
            case null, default -> "Error: Input is either null or an unsupported type!";
        };
    }
}

החל מגרסה Java 21 הושק באופן רשמי Pattern Matching for Switch (ניתן למצוא אותו החל מ Java 17 בחלק מהגרסאות כ Preview Feature).

בעבר, עד גרסת Java 21 ידענו לתת ל switch רק ערכים בודדים, מספרים, אותיות, Enum וכו'. פה הגיעה הבשורה החדשה: switch יכול לבדוק אובייקטים ולזהות דינמית מהו הסוג שנשלח.

static String format(Object obj) {
    return switch (obj) {
        case Integer i -> "It's an integer: " + i;
        case String s -> "It's a string of length " + s.length();
        case Double d -> "It's a double: " + d;
        default -> "Unknown type";
    };
}

לא רק שהוא יודע לזהות את האובייקט, יש לו את האופציה של Guard Patterns תנאי המשך באמצעות when. אנחנו יכולים להוסיף לו תנאים, ממש כמו מיני if בתוך ה switch:

case String s when s.length() > 10 -> "A very long string";
case String s -> "A short string";

וככה ניתן לשלב את שניהם ולחסוך המון if-else ולעשות את כל ה"מיון" ב-switch. לדוגמה, שילוב של שתי התכונות האלו ב switch אחד (שימו לב לשילוב של Pattern Matching יחד עם null case):

public class SimplePatternExample {
    public static String evaluate(Object obj) {
        return switch (obj) {
            // Guard pattern: handles positive numbers only
            case Integer i when i > 0 -> "Positive integer: " + i;

            // Regular pattern: handles zero or negative numbers
            case Integer i -> "Zero or negative integer: " + i;

            // Guard pattern: handles long strings (> 5 chars)
            case String s when s.length() > 5 -> "Long string: " + s;

            // Regular pattern: handles short strings
            case String s -> "Short string: " + s;

            // Safe handling for null inputs
            case null -> "Value is null";

            // Fallback for any other types
            default -> "Unknown type";
        };
    }
}

עבודה עם Sealed Class

אם אתם לא מכירים את sealed class ניתן לקרוא עוד על הנושא כאן .

Switch Expressions יכול להשתמש ב sealed class, ומייתר לחלוטין את הצורך בכתיבת default!

בזכות העובדה ש sealed class מקובעת בדיוק מיהם היורשים שלה, הקומפיילר של Java חכם מספיק כדי להבטיח את הדברים הבאים:

דוגמה בקוד:

public static String getAreaDescription(Shape shape) {
    return switch (shape) {
        // The compiler automatically matches the Circle type and unwraps it
        case Circle c -> "Circle area: " + c.area();

        // The compiler automatically matches the Square type
        case Square s -> "Square area: " + s.area();

        // No 'default' needed! The compiler is fully satisfied (exhaustiveness)
        // because we covered all possible classes allowed by the sealed 'Shape' permits list.
    };
}

זה שילוב מדהים! ככה אין אופציות שנשכחות גם אם נעדכן את ה sealed class בעתיד.

🏠 Back to Orly's Code Corner