At the end of the previous lesson you wrote a sketch of a menu for BiblioTech and dispatched it with an if-else ladder: if (option.equals("1")) … else if (option.equals("2")) …. It works, but it repeats the same variable three times, the same equals call three times, and it does not communicate the intent well. When every branch compares the same variable against specific values, Java offers a dedicated construct: the switch. This lesson covers it completely, including its two faces: the classic switch inherited from C, with the fall-through that is the number one source of bugs in this area, and the modern arrow switch introduced in Java 14, which eliminates the problem at the root and also lets you use switch as an expression that returns a value. By the end you will have written the option dispatcher for the BiblioTech menu, the centrepiece of the module's project.

Contents

  1. What problem switch solves
  2. The classic switch: case, break, default
  3. Fall-through: why it exists, when it is useful and why it bites
  4. Types allowed in a switch
  5. The arrow switch from Java 14+
  6. switch as an expression and the yield keyword
  7. Comparison table: classic, modern and the if-else ladder
  8. Pattern matching for switch (a preview)
  9. BiblioTech: the menu dispatcher
  10. BiblioTech: textual classification of a loan's status
  11. Common Mistakes and Tips
  12. Exercises

  1. What problem switch solves

Compare these two ways of doing the same thing:

// Ladder: the 'option' variable and the equals method appear three times
if (option.equals("1")) {
    System.out.println("Browse catalog");
} else if (option.equals("2")) {
    System.out.println("Register loan");
} else if (option.equals("3")) {
    System.out.println("Register return");
} else {
    System.out.println("Option not recognised");
}
// switch: the variable appears ONCE; below it there are only the values
switch (option) {
    case "1" -> System.out.println("Browse catalog");
    case "2" -> System.out.println("Register loan");
    case "3" -> System.out.println("Register return");
    default  -> System.out.println("Option not recognised");
}

The second version is not just shorter: it expresses exactly what is happening, which is selecting between discrete values of a single variable. That is switch's domain, and also its limit:

Situation Right tool
One variable compared against specific values (== or equals) switch
Ranges (daysLate > 7) if-else ladder
Compound conditions over several variables if-else ladder
Null checks, startsWith, contains if-else ladder

A switch cannot express "if the days are between 1 and 7". If your logic is about ranges, stick with the ladder from lesson 02-01.

  1. The classic switch: case, break, default

The traditional form, available since the first version of Java:

switch (expression) {
    case value1:
        // instructions
        break;
    case value2:
        // instructions
        break;
    default:
        // instructions if nothing matches
}

Each word has an exact role:

  • switch (expression) — the expression is evaluated only once; its value is compared against each case.
  • case value: — a label. The value must be a compile-time constant: a literal, a final constant or an enum constant. It cannot be a variable or a computed expression.
  • break; — leaves the switch. Without it, execution continues into the next case.
  • default: — the fallback branch. It is optional, but leaving it out is usually a design mistake: it means you have not thought about what to do with unexpected values.

A complete example with the status of a library copy:

package com.nexussoftware.bibliotech;

public class BiblioTechApp {
    public static void main(String[] args) {

        char statusCode = 'L';   // A=available, L=on loan, R=repair, W=withdrawn

        switch (statusCode) {
            case 'A':
                System.out.println("AVAILABLE - can be lent");
                break;
            case 'L':
                System.out.println("ON LOAN - check the return date");
                break;
            case 'R':
                System.out.println("IN REPAIR - temporarily unavailable");
                break;
            case 'W':
                System.out.println("WITHDRAWN - removed from the catalog");
                break;
            default:
                System.out.println("UNKNOWN CODE: " + statusCode);
        }
    }
}

The flow, seen as a diagram:

flowchart TD
    S["Evaluate statusCode"] --> D1{"'A'"}
    D1 -- "matches" --> A1["AVAILABLE"] --> F["End of the switch"]
    D1 -- "no" --> D2{"'L'"}
    D2 -- "matches" --> A2["ON LOAN"] --> F
    D2 -- "no" --> D3{"'R'"}
    D3 -- "matches" --> A3["IN REPAIR"] --> F
    D3 -- "no" --> D4{"'W'"}
    D4 -- "matches" --> A4["WITHDRAWN"] --> F
    D4 -- "no" --> DEF["default: unknown"] --> F

A practical detail: the default does not have to go last (it can sit in the middle and works just the same), but putting it at the end is the universal convention and there is no reason to deviate. The last case or the default do not need a break, because the switch ends there anyway; even so, many teams write it in case somebody adds a branch afterwards.

  1. Fall-through: why it exists, when it is useful and why it bites

Fall-through means that, when the instructions of a case finish, execution continues into the next case instead of leaving the switch. It is not an accident of the language: it is its designed behaviour, inherited from C.

Look at what happens when a break is forgotten:

char statusCode = 'A';

switch (statusCode) {
    case 'A':
        System.out.println("AVAILABLE");
        // the break is missing!
    case 'L':
        System.out.println("ON LOAN");
        break;
    case 'R':
        System.out.println("IN REPAIR");
        break;
}

Output:

AVAILABLE
ON LOAN

The book is declared simultaneously available and on loan. The compiler gives no warning, because fall-through is legal. This bug is hard to spot by reading, because your eyes jump from case to case and the missing break is invisible. That is precisely why it is the most reported defect in code reviews of classic switch.

The legitimate use: grouping cases. When several values share the same treatment, the labels are stacked with no instructions between them:

char statusCode = 'R';

switch (statusCode) {
    case 'A':
        System.out.println("The copy CAN be lent.");
        break;

    // Three different codes, one single treatment: intentional fall-through
    case 'L':
    case 'R':
    case 'W':
        System.out.println("The copy cannot be lent right now.");
        break;

    default:
        System.out.println("Unknown code.");
}

Here case 'L': and case 'R': have no body of their own: they deliberately fall through to the case 'W': block. It is idiomatic and perfectly acceptable.

The professional rule, adopted by every serious style guide: if a case with instructions falls into the next one on purpose, document it with an explicit comment:

case 'L':
    System.out.println("Recording the previous return...");
    // intentional fall-through: continues with the common treatment
case 'R':
    System.out.println("Copy not lendable.");
    break;

Without that comment, whoever reads the code cannot tell your intent from an oversight. And whoever maintains it in six months' time will probably be you.

  1. Types allowed in a switch

Not everything can go inside a switch's parentheses. The permitted types are:

Type Allowed? Since Note
byte, short, char, int Yes Java 1.0 The small integers; long is not allowed
Byte, Short, Character, Integer Yes Java 5 Wrappers, with autoboxing
enum Yes Java 5 The most natural use of switch; you will see it in lesson 04-07
String Yes Java 7 Compares by content, like equals
long, float, double, boolean No — Use if-else
Any other object Only with pattern matching Java 21 See section 8

Two important points about String:

  1. The comparison is by content, equivalent to equals. You do not fall into the == trap you saw in lesson 02-01.
  2. It is case-sensitive: case "yes" does not catch "YES". The usual solution is to normalise beforehand: switch (answer.toLowerCase()).
  3. If the variable is null, a classic switch over a String stops the program with a NullPointerException. Protect yourself with an if beforehand, or normalise with trim() on a value you know is not null.

About long: the exclusion is surprising, but it has a historical explanation tied to the bytecode instructions that implement switch. In practice it is not a problem: if you need to switch on a long, either convert with (int) when it is safe, or use if-else.

  1. The arrow switch from Java 14+

Java 14 stabilised an alternative syntax that solves fall-through by removing it entirely:

switch (statusCode) {
    case 'A' -> System.out.println("AVAILABLE - can be lent");
    case 'L' -> System.out.println("ON LOAN - check the return date");
    case 'R' -> System.out.println("IN REPAIR");
    case 'W' -> System.out.println("WITHDRAWN");
    default  -> System.out.println("UNKNOWN CODE: " + statusCode);
}

Differences from the classic form, one by one:

  • It uses -> instead of :.
  • There is no break, and there cannot be one: each branch is independent and leaves the switch when it ends. Fall-through simply does not exist.
  • To group values you separate them with commas in the same label, instead of stacking labels:
switch (statusCode) {
    case 'A'           -> System.out.println("It can be lent.");
    case 'L', 'R', 'W' -> System.out.println("It cannot be lent right now.");
    default            -> System.out.println("Unknown code.");
}
  • If a branch needs several instructions, it is wrapped in braces:
switch (option) {
    case "1" -> {
        System.out.println("=== BIBLIOTECH CATALOG ===");
        System.out.println("Effective Java (978-0000000001)");
        System.out.println("Design Patterns (978-0000000002)");
        System.out.println("Refactoring (978-0000000003)");
    }
    case "0" -> System.out.println("Exiting...");
    default  -> System.out.println("Invalid option.");
}

Java 17, this course's reference version, accepts both syntaxes. For new code, always use the arrow form: it is shorter, safer and it wipes out this construct's most frequent class of bug in one stroke. The classic syntax remains essential for reading existing code, which is half of a developer's job.

  1. switch as an expression and the yield keyword

Java 14's most powerful contribution is not the arrow, but the fact that a switch can be an expression: something that produces a value and can be assigned to a variable, exactly like the ternary operator, but with as many branches as you like.

int daysLate = 12;
final int MINOR_THRESHOLD = 7;

// switch as a STATEMENT: it does things
switch (statusCode) {
    case 'A' -> System.out.println("Available");
    default  -> System.out.println("Not available");
}

// switch as an EXPRESSION: it produces a value (note the '=' and the trailing ';')
String description = switch (statusCode) {
    case 'A' -> "AVAILABLE";
    case 'L' -> "ON LOAN";
    case 'R' -> "IN REPAIR";
    case 'W' -> "WITHDRAWN";
    default  -> "UNKNOWN";
};
System.out.println("Copy status: " + description);

Three rules govern switch as an expression:

  1. It must be exhaustive: for any possible value there has to be a branch. With char or String that forces you to include default. If it is missing, it does not compile (the switch expression does not cover all possible input values). That compiler check is pure gold: it makes forgetting a case impossible.
  2. It ends with a semicolon after the closing brace, because it is part of an assignment statement.
  3. Every branch must produce a value of the same type (or a compatible one).

yield: returning a value from a block. When a branch needs several lines before producing the value, braces are used and the last instruction is yield:

final double DAILY_RATE = 0.25;
final double MAX_FINE = 20.0;
String userType = "EXTERNAL";
int daysLate = 30;

double fine = switch (userType) {
    case "EMPLOYEE" -> daysLate * DAILY_RATE;

    case "INTERN" -> {
        // Interns pay half: several instructions are needed
        double base = daysLate * DAILY_RATE;
        double discounted = base / 2;
        yield discounted;                    // 'yield' delivers the branch's value
    }

    case "EXTERNAL" -> {
        double base = daysLate * DAILY_RATE * 2;         // 100 % surcharge
        yield Math.min(base, MAX_FINE);
    }

    default -> 0.0;
};

System.out.printf("Fine for %s: %.2f EUR%n", userType, fine);
// Fine for EXTERNAL: 15.00 EUR

yield is to a switch block what return is to a method: it delivers the result. Do not confuse it with break, which in the classic switch only exited without returning anything.

There is also a combination of the classic syntax with an expression (case 'A': yield "AVAILABLE";), but mixing : and -> in the same switch is a compilation error, and in practice nobody uses that variant. Stick with the arrows.

  1. Comparison table: classic, modern and the if-else ladder

Criterion Classic switch Arrow switch if-else ladder
Branch syntax case v: + break case v -> if (…) { }
Fall-through risk High None Not applicable
Grouping values Stacked labels case a, b, c -> || in the condition
Can return a value No Yes (expression) No (needs a ternary)
Exhaustiveness checked No Yes, in expressions No
Ranges and compound conditions No No Yes
Several variables in the decision No No Yes
Readability with 5+ branches Medium High Low
Available since Java 1.0 Java 14 Java 1.0

The choice criterion, in one sentence: if you are comparing a single variable against specific values, arrow switch; if there are ranges, nulls or several variables, an if-else ladder.

  1. Pattern matching for switch (a preview)

Java 21 took switch a step further with pattern matching: case labels can check an object's type and extract its data in the same gesture.

// Java 21+. Shown as a sample: do not use it yet.
String description = switch (object) {
    case Integer i when i > 100 -> "Large number: " + i;
    case Integer i              -> "Number: " + i;
    case String s               -> "Text of " + s.length() + " characters";
    case null                   -> "No value";
    default                     -> "Type not covered";
};

Notice two novelties: the case null (at last switch can handle nulls without blowing up) and the when clause, which adds an extra condition to the match.

This code requires understanding classes, reference types and hierarchies, which arrive in modules 3 and 4. It is studied in lesson 10-06 (Java 9 and Beyond). It is enough that you know it exists and that you recognise the syntax if you come across it.

  1. BiblioTech: the menu dispatcher

With all of the above you can now write the piece that will structure the module's project: the menu loop from lesson 02-02 with its dispatch resolved by a switch.

package com.nexussoftware.bibliotech;

import java.util.Scanner;

public class BiblioTechApp {
    public static void main(String[] args) {

        final String COMPANY_NAME = "Nexus Software";
        final int LOAN_DAYS = 15;
        final double DAILY_RATE = 0.25;
        final double MAX_FINE = 20.0;

        Scanner scanner = new Scanner(System.in);
        boolean exit = false;

        while (!exit) {

            System.out.println();
            System.out.println("=== BIBLIOTECH - " + COMPANY_NAME + " ===");
            System.out.println("  1. Browse catalog");
            System.out.println("  2. Register a loan");
            System.out.println("  3. Register a return");
            System.out.println("  0. Exit");
            System.out.print("Option: ");

            String option = scanner.nextLine().trim();

            switch (option) {

                case "1" -> {
                    System.out.println("--- CATALOG ---");
                    System.out.println("  Effective Java       978-0000000001");
                    System.out.println("  Design Patterns      978-0000000002");
                    System.out.println("  Refactoring          978-0000000003");
                }

                case "2" -> {
                    System.out.print("  Employee: ");
                    String employee = scanner.nextLine().trim();
                    System.out.print("  Title: ");
                    String title = scanner.nextLine().trim();
                    System.out.printf("  LOAN registered: %s -> %s (%d days)%n",
                                      title, employee, LOAN_DAYS);
                }

                case "3" -> {
                    System.out.print("  Elapsed days: ");
                    int elapsedDays = Integer.parseInt(scanner.nextLine().trim());

                    int daysLate = elapsedDays - LOAN_DAYS;
                    if (daysLate < 0) {
                        daysLate = 0;
                    }

                    double fine = daysLate * DAILY_RATE;
                    if (fine > MAX_FINE) {
                        fine = MAX_FINE;
                    }

                    System.out.printf("  RETURN: %d days late, fine %.2f EUR%n",
                                      daysLate, fine);
                }

                case "0" -> {
                    System.out.println("Closing BiblioTech. See you soon.");
                    exit = true;
                }

                default -> System.out.println("  Option not recognised. Use 0, 1, 2 or 3.");
            }
        }

        scanner.close();
    }
}

Three observations about the design:

  1. The option is read as a String and not as an int. That way, if the user types hello, it falls into default instead of stopping the program with a NumberFormatException. It is a cheap and effective defensive decision while you do not have exceptions available (module 6).
  2. The exit is done with the exit flag, not with break. And there is a strong reason, which is exactly the topic of the next lesson: inside a loop, a break placed in a switch would leave the switch, not the loop, and that happens with the arrow syntax too (case "0" -> { ...; break; } compiles and does not close the menu). The flag avoids the problem at the root.
  3. Each switch branch is wrapped in braces because it contains several instructions, and the variables declared inside (employee, title, daysLate) only exist in their block. That prevents name clashes between branches.

A sample session:

=== BIBLIOTECH - Nexus Software ===
  1. Browse catalog
  2. Register a loan
  3. Register a return
  0. Exit
Option: 1
--- CATALOG ---
  Effective Java       978-0000000001
  Design Patterns      978-0000000002
  Refactoring          978-0000000003

=== BIBLIOTECH - Nexus Software ===
...
Option: 3
  Elapsed days: 27
  RETURN: 12 days late, fine 3.00 EUR

=== BIBLIOTECH - Nexus Software ===
...
Option: 9
  Option not recognised. Use 0, 1, 2 or 3.

=== BIBLIOTECH - Nexus Software ===
...
Option: 0
Closing BiblioTech. See you soon.

  1. BiblioTech: textual classification of a loan's status

The second natural use of switch in BiblioTech is translating internal codes into readable text. This is where switch as an expression really shines:

package com.nexussoftware.bibliotech;

public class BiblioTechApp {
    public static void main(String[] args) {

        // Severity code computed by the module 1 business rules:
        // 0 = on time, 1 = minor, 2 = severe, 3 = capped fine
        int severity = 2;
        String title = "Design Patterns";

        // A single variable, four discrete values: a textbook case for switch.
        String status = switch (severity) {
            case 0 -> "ON TIME";
            case 1, 2, 3 -> "OVERDUE";         // three codes, one same status
            default -> "UNKNOWN";
        };

        String message = switch (severity) {
            case 0 -> "Correct return. Thank you.";
            case 1 -> "Minor delay. The daily rate applies.";
            case 2 -> "Severe delay. Review the return habit.";
            case 3 -> {
                String notice = "Maximum fine reached.";
                yield notice + " Human Resources will be notified.";
            }
            default -> "Severity code not covered: " + severity;
        };

        // The icon is a char: another switch expression, one line per branch.
        char icon = switch (severity) {
            case 0 -> '+';
            case 1 -> '!';
            case 2, 3 -> 'X';
            default -> '?';
        };

        System.out.printf("[%c] %-22s %-12s %s%n", icon, title, status, message);
    }
}

Output:

[X] Design Patterns        OVERDUE      Severe delay. Review the return habit.

Compare this with the module 1 alternative: three chains of nested ternaries, unreadable. switch as an expression lets you assign a variable with several branches while keeping clarity, and the compiler forces you to cover every case.

Common Mistakes and Tips

1. Forgetting the break in a classic switch. The star bug. Branches you did not want get executed and the compiler says nothing. Definitive fix: use the arrow syntax.

2. Putting a break in an arrow switch. It is never needed to separate branches, and depending on where you write it three different things happen:

Where Result
case "0" -> break; Does not compile: unexpected statement in case, expected is an expression, a block or a throw statement
case "0" -> { … break; } inside a statement switch It does compile, and it leaves the switch, not the surrounding loop: the same trap as the classic switch
break inside an expression switch Does not compile: attempt to break out of a switch expression. There you use yield

The practical conclusion is the same in all three cases: with the arrow syntax, do not write break.

3. Mixing : and -> in the same switch. Compilation error. Pick one form and keep it throughout the block.

4. Using a variable as a case label.

int limit = 7;
switch (days) {
    case limit:       // ERROR: constant expression required
        ...
}

Only literals and final constants initialised with literals are allowed. If the label has to be computed, your problem is not a switch problem but an if-else one.

5. Repeating a value in two cases. case "1": ... case "1": is a compilation error (duplicate case label). That is good news: switch detects duplicates that would slip through unnoticed in an if-else ladder.

6. Forgetting the default in an expression switch. It does not compile if the type is not fully covered. In a statement switch it does compile, but it leaves a silent hole: if an unexpected value arrives, nothing at all happens and nobody finds out.

7. switch over a String that may be null. It stops the program. Validate first:

if (option == null) {
    option = "";
}
switch (option) { ... }

8. Forgetting the semicolon of an expression switch.

String status = switch (severity) {
    case 0 -> "ON TIME";
    default -> "OVERDUE";
}                       // <-- the assignment's ';' is missing

9. Tip: normalise the input before switching. switch (answer.trim().toLowerCase()) saves you from writing case "YES", "Yes", "yes", "yEs".

10. Tip: when the branches grow, something else is being asked of you. A switch with twenty branches of fifteen lines each is a sign that you need to break it into methods (lesson 03-03) or, later on, use polymorphism (lesson 03-06), which is the object-oriented way of eliminating giant switches.

Exercises

Exercise 1: hunt the forgotten fall-through

The following code is meant to classify the quarter in which a book from the BiblioTech catalog was acquired, but it has two forgotten breaks. Run it in your head with acquisitionMonth = 2 and with acquisitionMonth = 5, note the actual output, identify the two faults and fix them. Then rewrite the corrected version using an arrow switch.

int acquisitionMonth = 2;
String quarter;

switch (acquisitionMonth) {
    case 1:
    case 2:
    case 3:
        quarter = "Q1";
    case 4:
    case 5:
    case 6:
        quarter = "Q2";
        break;
    case 7:
    case 8:
    case 9:
        quarter = "Q3";
        break;
    case 10:
    case 11:
    case 12:
        quarter = "Q4";
    default:
        quarter = "INVALID MONTH";
}

System.out.println("Acquired in quarter: " + quarter);

Exercise 2: reports menu with an expression switch

Write a program that shows this menu in a loop and uses a switch as an expression to obtain the text of the selected report, plus a separate statement switch to run the exit action:

1. Fines report
2. Active loans report
3. Most overdue books report
0. Exit

Requirements:

  • Options 1, 2 and 3 must produce a descriptive string via an expression switch assigned to a String report variable.
  • Option 3 must use a block with yield, because it builds its text in two steps.
  • Any other input must fall into default and show a warning without breaking the loop.
  • Count how many reports were generated during the session and show it on exit.

Exercise 3: combined classification

Write a program that, given an int daysLate, first computes a severity code (0 = no delay, 1 = minor if <= MINOR_THRESHOLD, 2 = severe, 3 = capped fine because the amount reaches MAX_FINE) using an if-else ladder, and then uses an expression switch over that code to obtain, all at once, the textual label, the effective fine amount and the message for the employee.

The key of the exercise is for you to see where the if ends and the switch begins: ranges are solved by the if; the resulting discrete values are solved by the switch. Test with daysLate equal to 0, 5, 30 and 200.

Solutions

Solution 1

Behaviour of the broken code.

With acquisitionMonth = 2: it matches case 2, falls through to case 3 (no instructions), assigns quarter = "Q1", and since the break is missing, it keeps falling through case 4 and case 5 until it runs quarter = "Q2"; and there it does find a break. Output: Acquired in quarter: Q2. Incorrect: February is Q1.

With acquisitionMonth = 5: it matches case 5, assigns "Q2", finds the break. Output: Q2. Correct by chance.

And there is a third revealing case, acquisitionMonth = 11: it assigns "Q4", falls into default and overwrites it with "INVALID MONTH". Wrong output as well.

The two forgotten breaks are at the end of the "Q1" block and at the end of the "Q4" block.

Corrected classic version:

switch (acquisitionMonth) {
    case 1:
    case 2:
    case 3:
        quarter = "Q1";
        break;              // <-- ADDED
    case 4:
    case 5:
    case 6:
        quarter = "Q2";
        break;
    case 7:
    case 8:
    case 9:
        quarter = "Q3";
        break;
    case 10:
    case 11:
    case 12:
        quarter = "Q4";
        break;              // <-- ADDED
    default:
        quarter = "INVALID MONTH";
}

Modern version, where the bug is impossible:

package com.nexussoftware.bibliotech;

public class BiblioTechApp {
    public static void main(String[] args) {

        int acquisitionMonth = 2;

        // Expression switch: exhaustive, no break and no possible fall-through.
        String quarter = switch (acquisitionMonth) {
            case 1, 2, 3    -> "Q1";
            case 4, 5, 6    -> "Q2";
            case 7, 8, 9    -> "Q3";
            case 10, 11, 12 -> "Q4";
            default         -> "INVALID MONTH";
        };

        System.out.println("Acquired in quarter: " + quarter);            // Q1
    }
}

From twenty-five lines to seven, and with a whole class of bugs eliminated. This is why the course recommends the arrow syntax for all new code.

Solution 2

package com.nexussoftware.bibliotech;

import java.util.Scanner;

public class BiblioTechApp {
    public static void main(String[] args) {

        final String COMPANY_NAME = "Nexus Software";
        final double DAILY_RATE = 0.25;

        Scanner scanner = new Scanner(System.in);
        boolean exit = false;
        int reportsGenerated = 0;        // session counter

        while (!exit) {

            System.out.println();
            System.out.println("=== BIBLIOTECH REPORTS - " + COMPANY_NAME + " ===");
            System.out.println("  1. Fines report");
            System.out.println("  2. Active loans report");
            System.out.println("  3. Most overdue books report");
            System.out.println("  0. Exit");
            System.out.print("Option: ");

            String option = scanner.nextLine().trim();

            // 'report' is null when the option generates no report (0 or invalid).
            String report = switch (option) {

                case "1" -> "FINES: 3 returns with a fine. "
                          + "Rate applied: " + DAILY_RATE + " EUR/day.";

                case "2" -> "ACTIVE LOANS: Marta Ruiz (Effective Java), "
                          + "Diego Alonso (Design Patterns).";

                case "3" -> {
                    // A branch with several instructions: it needs braces and yield.
                    String worst = "Refactoring";
                    int daysLate = 105;
                    yield "MOST OVERDUE: " + worst + " with " + daysLate + " days.";
                }

                default -> null;    // 0 and any other input
            };

            if (report != null) {
                System.out.println("  " + report);
                reportsGenerated++;
            }

            // Second switch, now as a STATEMENT: it runs actions.
            switch (option) {
                case "1", "2", "3" -> { /* already printed above */ }
                case "0" -> {
                    System.out.printf("Session closed. Reports generated: %d%n",
                                      reportsGenerated);
                    exit = true;
                }
                default -> System.out.println("  Option not recognised.");
            }
        }

        scanner.close();
    }
}

A sample session:

Option: 1
  FINES: 3 returns with a fine. Rate applied: 0.25 EUR/day.
Option: 3
  MOST OVERDUE: Refactoring with 105 days.
Option: 7
  Option not recognised.
Option: 0
Session closed. Reports generated: 2

A note about the design: using null as "there is no report" works, but it is exactly the kind of decision that produces NullPointerException in large programs. In module 10 you will meet Optional, the modern way of representing "there may be no value". For now, the if (report != null) guard does the job.

Solution 3

package com.nexussoftware.bibliotech;

public class BiblioTechApp {
    public static void main(String[] args) {

        final double DAILY_RATE = 0.25;
        final double MAX_FINE = 20.0;
        final int MINOR_THRESHOLD = 7;

        int daysLate = 30;
        String title = "Refactoring";
        String employee = "Nuria Vidal";

        double theoreticalFine = daysLate * DAILY_RATE;

        // ---- PHASE 1: RANGES are solved by an if-else ----
        int severity;
        if (daysLate == 0) {
            severity = 0;                          // no delay
        } else if (theoreticalFine >= MAX_FINE) {
            severity = 3;                          // capped fine
        } else if (daysLate <= MINOR_THRESHOLD) {
            severity = 1;                          // minor
        } else {
            severity = 2;                          // severe
        }

        // ---- PHASE 2: DISCRETE VALUES are solved by a switch ----
        String label = switch (severity) {
            case 0 -> "ON TIME";
            case 1 -> "MINOR DELAY";
            case 2 -> "SEVERE DELAY";
            case 3 -> "MAXIMUM FINE";
            default -> "UNKNOWN";
        };

        double effectiveFine = switch (severity) {
            case 0 -> 0.0;
            case 1, 2 -> theoreticalFine;
            case 3 -> MAX_FINE;
            default -> 0.0;
        };

        String message = switch (severity) {
            case 0 -> "Thank you for returning on time.";
            case 1 -> "A small delay. Try to keep within the term.";
            case 2 -> "A significant delay. Review your active loans.";
            case 3 -> {
                String base = "The cap of " + MAX_FINE + " EUR has been reached.";
                yield base + " A notification has been sent to Human Resources.";
            }
            default -> "No message.";
        };

        System.out.println("=== RETURN ASSESSMENT ===");
        System.out.printf("%-14s %s%n",       "Employee:", employee);
        System.out.printf("%-14s %s%n",       "Book:", title);
        System.out.printf("%-14s %d%n",       "Days late:", daysLate);
        System.out.printf("%-14s %s%n",       "Category:", label);
        System.out.printf("%-14s %.2f EUR%n", "Fine:", effectiveFine);
        System.out.printf("%-14s %s%n",       "Message:", message);
    }
}

Results with the four requested values:

daysLate Theoretical fine severity Label Effective fine
0 €0.00 0 ON TIME €0.00
5 €1.25 1 MINOR DELAY €1.25
30 €7.50 2 SEVERE DELAY €7.50
200 €50.00 3 MAXIMUM FINE €20.00

The lesson of the exercise is the division of responsibilities: if-else turns continuous ranges into a discrete code, and from there the switch can do its job. Notice too that the order of the if-else matters: the cap check comes before the MINOR_THRESHOLD one, because a capped fine always takes priority over the classification by days. And observe that three different switches share the same severity code; when you reach lesson 04-07 you will see that enumerations are the right way to represent that code, and the compiler will even be able to verify that no branch is missing without needing a default.

Conclusion

You now know how to dispatch decisions over discrete values. You have seen the classic switch with case, break and default; fall-through, its reason for being, its legitimate use for grouping cases and why a forgotten break is this construct's most frequent bug; the allowed types (int and its smaller siblings, char, String, enum) and the forbidden ones (long, double, boolean); the arrow switch from Java 14+, where fall-through is impossible and grouping values is a matter of commas; and switch as an expression, with yield for multi-line branches and with exhaustiveness verified by the compiler. With the comparison table you have the criterion: switch for specific values of one variable, if-else for ranges and compound conditions.

BiblioTechApp now has a looping menu with its dispatcher written with arrows, four working options and a default branch that absorbs erroneous input without breaking anything. You have also seen that the exit is controlled by a boolean flag, and the reason has been hinted at: a break inside a switch does not do what a beginner expects.

That is precisely what the next lesson, Break and Continue, clarifies: how to abandon a loop as soon as you find what you were looking for, how to skip an iteration you are not interested in, why a break inside a switch nested in a loop is a classic trap, and what alternatives —labels, flags— exist when you need to break several levels at once.

Java Programming Course

Module 1: Introduction to Java

Module 2: Control Flow

Module 3: Object-Oriented Programming

Module 4: Advanced Object-Oriented Programming

Module 5: Data Structures and Collections

Module 6: Exception Handling

Module 7: File Input/Output

Module 8: Multithreading and Concurrency

Module 9: Networking

Module 10: Advanced Topics

Module 11: Java Frameworks and Libraries

Module 12: Building Real-World Applications

© Copyright 2026. All rights reserved