When you closed module 1 you left BiblioTechApp working, but with an uncomfortable diagnosis: it does not repeat, it does not decide, it does not validate and it remembers nothing. This module attacks the first three problems, and this lesson deals with the most urgent one: deciding. Until now your program was a straight line —read, calculate, print— and any "decision" it made was really an arithmetic trick or a ternary operator buried inside a formula. If an employee typed -5 elapsed days, the program accepted it without blinking. If the delay was 200 days, it printed the same sentence as if it were 1. Conditional statements are the mechanism a program uses to fork its path: it runs some instructions or others depending on the state of the data. Without them there are no business rules, no validation and no software; with them, BiblioTechApp starts behaving like a real application.

Contents

  1. From a linear program to a program that decides
  2. The simple if
  3. The {} block and why omitting it is dangerous
  4. if-else: the two branches
  5. The else if ladder
  6. Compound conditions and short-circuiting in practice
  7. Nesting, guard clauses and early exit
  8. if-else versus the ternary operator
  9. Comparing Strings in conditions and the null trap
  10. The BiblioTech business rules as a decision table
  11. Validating user input with if
  12. Common Mistakes and Tips
  13. Exercises

  1. From a linear program to a program that decides

A program without conditionals runs all its instructions, always, in the same order. That is what BiblioTechApp does today:

flowchart TD
    A["Read data"] --> B["Calculate delay"]
    B --> C["Calculate fine"]
    C --> D["Print receipt"]

A program with conditionals has alternative paths. The same journey, but with detours:

flowchart TD
    A["Read data"] --> V{"Data valid?"}
    V -- "no" --> E["Report the error"]
    V -- "yes" --> B["Calculate delay"]
    B --> R{"Is it late?"}
    R -- "no" --> P["Receipt with no fine"]
    R -- "yes" --> M["Calculate fine"]
    M --> P

The difference is enormous: the number of possible paths is no longer one. And with it comes a new responsibility that will stay with you for the rest of the course: every branch you write is a branch that has to be tested.

  1. The simple if

The most basic form runs a block only if a boolean condition is true.

if (condition) {
    // runs only if condition is true
}

Points to internalise from day one:

  • The condition always goes in parentheses.
  • The condition must be of type boolean. There is no implicit conversion: in Java, if (1) does not compile (unlike C or JavaScript). This eliminates a whole family of bugs.
  • After the ) you do not put a semicolon. if (x > 0); compiles, but the if ends up empty and the following block always runs.

Applied to BiblioTech:

package com.nexussoftware.bibliotech;

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

        final int LOAN_DAYS = 15;
        final double DAILY_RATE = 0.25;

        String title = "Effective Java";
        int elapsedDays = 27;

        int daysLate = elapsedDays - LOAN_DAYS;                // 12

        System.out.println("Book: " + title);

        if (daysLate > 0) {
            double fine = daysLate * DAILY_RATE;
            System.out.printf("Returned after the deadline. Fine: %.2f EUR%n", fine);
        }

        System.out.println("Record completed.");
    }
}

Notice three things:

  1. daysLate > 0 produces a boolean; that is the only requirement.
  2. The fine variable is declared inside the {} block, so it only exists in there. Outside the if you cannot use it: the compiler will say cannot find symbol. A variable's scope is the block in which it is declared.
  3. "Record completed." is always printed, fine or no fine: it is outside the if.

  1. The {} block and why omitting it is dangerous

Java lets an if govern a single statement without braces:

if (daysLate > 0)
    System.out.println("Overdue");

This compiles and works. And it is such a well-known source of bugs that most professional style guides (Google's, Oracle's, almost any company's) forbid it. The reason:

if (daysLate > 0)
    System.out.println("Overdue");
    System.out.println("Applying fine...");     // ALWAYS runs!

The indentation suggests both lines depend on the if, but Java ignores indentation. Only the first statement belongs to the if; the second one always runs, even with daysLate equal to 0. This mistake was the origin of Apple's "goto fail" security bug in 2014, which affected millions of devices.

Course rule: always use braces, even for a single line. The cost is two characters; the benefit is that adding one more line will never change the meaning of the code.

  1. if-else: the two branches

When there is an explicit alternative, else picks up every case in which the condition is false:

if (daysLate > 0) {
    System.out.println("Return AFTER THE DEADLINE");
} else {
    System.out.println("Return ON TIME");
}

if and else are mutually exclusive and exhaustive: exactly one of the two branches runs, never neither and never both. That guarantee is very valuable: if you need a variable to end up initialised whatever happens, an if-else assures it and the compiler knows it.

String status;                 // declared with no value
if (daysLate > 0) {
    status = "OVERDUE";
} else {
    status = "ON TIME";
}
System.out.println(status);    // compiles: the compiler knows it is always assigned

If you removed the else, the compiler would give the error variable status might not have been initialized, because there would be a path where the variable arrived without a value. The Java compiler analyses the possible paths; it is worth having it as an ally.

  1. The else if ladder

When there are more than two cases, conditions are chained. Technically there is no elseif keyword: what exists is an if inside the previous else, written compactly.

if (condition1) {
    // case 1
} else if (condition2) {
    // case 2
} else if (condition3) {
    // case 3
} else {
    // none of the above
}

Behaviour rules you must memorise:

  • The conditions are evaluated top to bottom and it stops at the first true one.
  • Only one block runs, even if several conditions are true.
  • The final else is optional; without it, it is possible that no branch runs at all.

That is why the order matters. This ladder is wrong:

// INCORRECT: the first case swallows all the others
if (daysLate > 0) {
    System.out.println("Minor delay");
} else if (daysLate > 7) {
    System.out.println("Severe delay");      // unreachable in practice
}

With daysLate = 20, the first condition is already true and "Minor delay" is printed. The second branch is never reached with values greater than 7. The practical rule: order from the most restrictive condition to the most general one.

// CORRECT
if (daysLate > 7) {
    System.out.println("Severe delay");
} else if (daysLate > 0) {
    System.out.println("Minor delay");
} else {
    System.out.println("No delay");
}

  1. Compound conditions and short-circuiting in practice

In module 1 you saw &&, ||, ! and short-circuiting. Here you use them for real. A one-line reminder: && evaluates the right operand only if the left one is true; || evaluates it only if the left one is false.

That turns the order of the operands into a protection tool:

String reference = null;

// The order saves the program: if reference is null, isEmpty() is never called.
if (reference != null && !reference.isEmpty()) {
    System.out.println("Reference: " + reference);
}

If you wrote the conditions the other way round (!reference.isEmpty() && reference != null), the program would try to call a method on null and blow up. That protection has its own name: a null guard.

The same principle applied to BiblioTech, where a loan is only considered "recoverable with a notice" if it is late but the cap has not yet been reached:

double fine = daysLate * DAILY_RATE;

if (daysLate > 0 && fine < MAX_FINE) {
    System.out.println("Partial fine: the cap has not been reached yet.");
}

if (daysLate == 0 || elapsedDays <= LOAN_DAYS) {
    System.out.println("Loan closed with no incidents.");
}

And ! to negate, with one piece of advice: one negation is readable, two are a riddle. Instead of if (!(daysLate <= 0)), write if (daysLate > 0).

Written like this Better like this Reason
if (!(a > b)) if (a <= b) The negation of a comparison is another comparison
if (!(a && b)) if (!a || !b) De Morgan's law; often clearer still to reframe the domain
if (isValid == true) if (isValid) Comparing a boolean with true is redundant
if (isValid == false) if (!isValid) Likewise

  1. Nesting, guard clauses and early exit

Nesting conditionals means putting an if inside another. It works, but it quickly degenerates into so-called "arrow code", in which the logic drifts to the right and becomes unreadable:

// Hard to follow: you have to hold three conditions in your head at once
if (employee != null) {
    if (!employee.isEmpty()) {
        if (elapsedDays >= 0) {
            if (elapsedDays > LOAN_DAYS) {
                System.out.println("Process fine");
            }
        }
    }
}

The technique that fixes it is called a guard clause: instead of nesting the good case, you discard the bad cases first and exit. Since in this module all the code lives in main and you have not seen methods yet (they arrive in lesson 03-03), the exit is done with return;, which in main ends the program.

if (employee == null || employee.isEmpty()) {
    System.err.println("ERROR: the employee name is mandatory.");
    return;                       // ends main
}
if (elapsedDays < 0) {
    System.err.println("ERROR: the elapsed days cannot be negative.");
    return;
}

// From here on, everything that follows is the "happy path", with no extra indentation.
if (elapsedDays > LOAN_DAYS) {
    System.out.println("Process fine");
}

Compare both styles:

Aspect Deep nesting Guard clauses
Indentation Grows with each condition Stays flat
Mental load You have to hold every condition in your head Each guard is forgotten as soon as you pass it
Error messages Hard to tie to their cause One per guard, specific
Main path Buried at the bottom Visible at the end, with no noise

When in module 3 you learn to write methods, this pattern will be even more natural: each guard will return from the method without aborting the whole program. The idea, however, is the same and it is worth adopting now.

  1. if-else versus the ternary operator

You already used the ternary condition ? valueIfTrue : valueIfFalse in module 1 to clamp the delay to zero. It is worth pinning down when to use each tool, because they are interchangeable only in appearance.

The ternary is an expression: it produces a value. The if is a statement: it runs actions.

// Ternary: choosing a VALUE. Correct and readable.
int daysLate = (elapsedDays > LOAN_DAYS) ? elapsedDays - LOAN_DAYS : 0;
String status = (daysLate > 0) ? "OVERDUE" : "ON TIME";

// if-else: running ACTIONS. The ternary would be no use here.
if (daysLate > 0) {
    System.out.println("Notifying the employee...");
    System.out.printf("Amount: %.2f EUR%n", fine);
} else {
    System.out.println("Nothing to charge.");
}
Criterion Use a ternary Use if-else
Goal Obtaining a value Running instructions
Number of branches Exactly 2 2 or more
Length Fits comfortably on one line Several statements per branch
Nesting Never more than one No problem
Initialising a final variable Ideal Possible, but more verbose

The practical limit is nesting. This is valid but indefensible in a code review:

// UNREADABLE: do not do this
String severity = daysLate == 0 ? "NONE" : daysLate <= MINOR_THRESHOLD ? "MINOR" : "SEVERE";

The same logic with a ladder reads effortlessly, and it is what you will use in BiblioTech:

String severity;
if (daysLate == 0) {
    severity = "NONE";
} else if (daysLate <= MINOR_THRESHOLD) {
    severity = "MINOR";
} else {
    severity = "SEVERE";
}

  1. Comparing Strings in conditions and the null trap

Here is the number one mistake for anyone coming from other languages. In Java, == on objects compares references (whether they are the same object in memory), not content. With String it works sometimes —because of the literal pool you saw in module 1— and fails exactly when the text comes from the user, which is always.

String option = scanner.nextLine();     // the user types: exit

if (option == "exit") {                 // WRONG: almost always false
    System.out.println("Goodbye");
}

if (option.equals("exit")) {            // RIGHT: compares content
    System.out.println("Goodbye");
}

Useful methods in conditions:

Method What it checks true example
a.equals(b) Identical content, case-sensitive "exit".equals("exit")
a.equalsIgnoreCase(b) Identical content ignoring case "Exit".equalsIgnoreCase("EXIT")
a.isEmpty() Zero length "".isEmpty()
a.isBlank() Empty or only spaces (Java 11+) " ".isBlank()
a.startsWith(b) Starts with that text "978-0000000001".startsWith("978")
a.contains(b) Contains that text "Effective Java".contains("Java")

For user input, equalsIgnoreCase combined with trim() avoids absurd complaints:

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

if (answer.equalsIgnoreCase("yes")) {
    System.out.println("Confirmed.");
}

The null trap. If a String variable is null, calling any method on it stops the program with a NullPointerException (you will see how to handle it in module 6; for now, avoid it). The standard technique has two forms:

String reference = null;

// Option A: explicit guard with short-circuiting
if (reference != null && reference.equals("REF-001")) { ... }

// Option B (more elegant): the literal on the left, which is never null
if ("REF-001".equals(reference)) { ... }          // false, without blowing up

Option B is known as a Yoda comparison. "REF-001" is a literal, it is never null, and equals cleanly returns false when it receives null as an argument. Use it when the value may be null and you do not want to write the guard.

  1. The BiblioTech business rules as a decision table

Before writing conditionals it is worth writing the decision table: which cases exist, how they are told apart and what each one does. If the table is right, the code writes itself.

Rules of the Nexus Software library (constants already fixed in module 1: LOAN_DAYS = 15, DAILY_RATE = 0.25, MAX_FINE = 20.0, MINOR_THRESHOLD = 7):

Case Condition status severity Fine
1. On time daysLate == 0 ON TIME NONE €0.00
2. Minor delay daysLate <= MINOR_THRESHOLD OVERDUE MINOR daysLate * 0.25
3. Severe delay daysLate > MINOR_THRESHOLD and fine < cap OVERDUE SEVERE daysLate * 0.25
4. Capped fine calculated fine >= MAX_FINE OVERDUE MAXIMUM €20.00

Notice that case 4 cuts across cases 2 and 3: it does not depend on the days but on the amount. That is why it is checked after calculating the fine, not before. The complete flow:

flowchart TD
    A["daysLate = max(0, elapsedDays - LOAN_DAYS)"] --> B{"daysLate == 0"}
    B -- "yes" --> C["status = ON TIME<br/>severity = NONE<br/>fine = 0"]
    B -- "no" --> D["fine = daysLate * DAILY_RATE"]
    D --> E{"fine >= MAX_FINE"}
    E -- "yes" --> F["fine = MAX_FINE<br/>severity = MAXIMUM"]
    E -- "no" --> G{"daysLate <= MINOR_THRESHOLD"}
    G -- "yes" --> H["severity = MINOR"]
    G -- "no" --> I["severity = SEVERE"]
    C --> Z["Print receipt"]
    F --> Z
    H --> Z
    I --> Z

And the direct translation into code, replacing module 1's nested ternaries:

package com.nexussoftware.bibliotech;

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

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

        String employee = "Diego Alonso";
        String title = "Design Patterns";
        String isbn = "978-0000000002";
        int elapsedDays = 120;

        // Delay clamped to zero: never negative.
        int daysLate = elapsedDays - LOAN_DAYS;
        if (daysLate < 0) {
            daysLate = 0;
        }

        String status;
        String severity;
        double fine;

        if (daysLate == 0) {
            status = "ON TIME";
            severity = "NONE";
            fine = 0.0;
        } else {
            status = "OVERDUE";
            fine = daysLate * DAILY_RATE;

            if (fine >= MAX_FINE) {
                fine = MAX_FINE;                // the cap is applied
                severity = "MAXIMUM";
            } else if (daysLate <= MINOR_THRESHOLD) {
                severity = "MINOR";
            } else {
                severity = "SEVERE";
            }
        }

        System.out.println("=== BiblioTech - Nexus Software ===");
        System.out.printf("%-14s %s%n", "Employee:", employee);
        System.out.printf("%-14s %s (%s)%n", "Book:", title, isbn);
        System.out.printf("%-14s %d%n", "Days late:", daysLate);
        System.out.printf("%-14s %s / %s%n", "Status:", status, severity);
        System.out.printf("%-14s %.2f EUR%n", "Fine:", fine);

        // Extra notice: only when the fine has reached the cap.
        if (severity.equals("MAXIMUM")) {
            System.out.println();
            System.out.println("WARNING: fine capped. Notify Human Resources.");
        }
    }
}

Output with elapsedDays = 120 (105 days late, theoretical fine €26.25):

=== BiblioTech - Nexus Software ===
Employee:      Diego Alonso
Book:          Design Patterns (978-0000000002)
Days late:     105
Status:        OVERDUE / MAXIMUM
Fine:          20.00 EUR

WARNING: fine capped. Notify Human Resources.

One design detail worth pointing out: the three variables status, severity and fine are declared before the if and assigned inside it. Since every branch ends up assigning them, the compiler accepts using them afterwards. If you removed the final else, it would stop compiling. That is the compiler verifying your decision table.

  1. Validating user input with if

The module 1 program accepted anything. Now you can reject what makes no sense. The strategy, with guard clauses, is to read and check immediately:

package com.nexussoftware.bibliotech;

import java.util.Scanner;

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

        final int LOAN_DAYS = 15;
        Scanner scanner = new Scanner(System.in);

        System.out.print("Employee: ");
        String employee = scanner.nextLine().trim();

        if (employee.isBlank()) {
            System.err.println("ERROR: the employee name cannot be empty.");
            scanner.close();
            return;
        }

        System.out.print("Book title: ");
        String title = scanner.nextLine().trim();

        if (title.isBlank()) {
            System.err.println("ERROR: the title cannot be empty.");
            scanner.close();
            return;
        }

        System.out.print("ISBN: ");
        String isbn = scanner.nextLine().trim();

        if (!isbn.startsWith("978")) {
            System.err.println("ERROR: the library ISBN must start with 978.");
            scanner.close();
            return;
        }

        System.out.print("Elapsed days: ");
        String daysInput = scanner.nextLine().trim();
        int elapsedDays = Integer.parseInt(daysInput);

        if (elapsedDays < 0) {
            System.err.println("ERROR: the elapsed days cannot be negative.");
            scanner.close();
            return;
        }
        if (elapsedDays > 3650) {
            System.err.println("ERROR: implausible value (more than 10 years).");
            scanner.close();
            return;
        }

        System.out.println("Input validated. Processing the return for " + employee + "...");
        int daysLate = elapsedDays - LOAN_DAYS;
        if (daysLate < 0) {
            daysLate = 0;
        }
        System.out.println("Days late: " + daysLate);

        scanner.close();
    }
}

Look at what you have just achieved: the program no longer accepts negative days, nor empty names, nor ISBNs from another library. And pause on the obvious limitation: when it detects an error, the program gives up and ends. The sensible thing would be to ask for the value again. That requires repeating, and repeating is exactly what the loops in the next lesson do.

There is one hole you cannot plug yet: if the user types twenty instead of 20, Integer.parseInt does not fail through an if, it stops the program with a NumberFormatException. No condition can prevent it, because the error happens inside the conversion. That gap is closed in module 6, with exceptions. For now, assume the user types digits.

Common Mistakes and Tips

1. The phantom semicolon.

if (daysLate > 0);               // <-- that ';' closes the if
{
    System.out.println("Overdue");       // ALWAYS runs
}

It compiles with no warnings and is fiendishly hard to spot. No modern IDE lets it pass without underlining it: pay attention.

2. Confusing = with ==.

boolean available = false;
if (available = true) { ... }    // ASSIGNS true and then evaluates true: always enters

With booleans it compiles and produces a silent bug. With other types (if (x = 5)) it does not compile, because int is not boolean. Write plain if (available) and the problem disappears.

3. Comparing Strings with ==. You have already seen it: use equals or equalsIgnoreCase. Always.

4. Comparing doubles with ==. Because of the binary precision you saw in module 1, 0.1 + 0.2 == 0.3 is false. For amounts, compare with a tolerance or —better— compare the integers they derive from:

if (Math.abs(fine - MAX_FINE) < 0.001) { ... }        // "practically equal"

5. Ladders with overlapping, badly ordered conditions. Remember: from the most restrictive to the most general. And check that every branch is reachable with some real data.

6. Forgetting the final else. If your branches assign a variable, without an else it may end up unassigned. The compiler will warn you; do not silence it by initialising to a fake value like "" or -1 without thinking, because then the bug moves to runtime.

7. Style tip: extract complex conditions into named boolean variables.

// Before: you have to decipher it
if (daysLate > MINOR_THRESHOLD && fine < MAX_FINE && !employee.isBlank()) { ... }

// After: it reads like a sentence
boolean isSevereDelay = daysLate > MINOR_THRESHOLD;
boolean fineBelowCap  = fine < MAX_FINE;
boolean validEmployee = !employee.isBlank();

if (isSevereDelay && fineBelowCap && validEmployee) { ... }

There is no appreciable performance cost and the code documents itself. Besides, when debugging (lesson 02-05) you will see each condition separately.

8. Tip: test the boundaries. If your condition is daysLate <= MINOR_THRESHOLD, test with 6, 7 and 8. Conditional bugs almost always live on the exact edge.

Exercises

Exercise 1: availability classifier

Write a program that, given the variables title, available (boolean) and daysRemaining (int, days left before the current loan expires; it is 0 if the book is on the shelf), prints one of these messages:

  • If available is true: "Effective Java: AVAILABLE for immediate loan".
  • If it is not available and daysRemaining is greater than 0: "Effective Java: ON LOAN, back in N days".
  • If it is not available and daysRemaining is 0 or negative: "Effective Java: ON LOAN AND OVERDUE, chase the employee".

Use an else if ladder and braces everywhere. Test the three cases.

Exercise 2: validating a new loan

An employee wants to take a book. The Nexus Software library only authorises the loan if all of the following hold:

  • The employee's name is not blank.
  • The book is available (available == true).
  • The employee has no outstanding fines (outstandingFine == 0.0).
  • The employee's number of active loans (activeLoans) is fewer than 3.

Write the program with guard clauses: each failure prints a specific message on System.err and ends with return. If everything holds, print the authorisation with the return date expressed as "Return in 15 days" using the LOAN_DAYS constant.

Test it with Nuria Vidal, available true, fine 0.0 and 1 active loan (it should authorise), and with Diego Alonso, available true, fine 4.50 and 0 loans (it should reject).

Exercise 3: safe reference comparison

Given two variables String reference and String searchedReference, either of which may be null, write the logic that prints:

  • "MATCH" if both have the same content ignoring case.
  • "NO REFERENCE" if either of the two is null or blank.
  • "NO MATCH" in any other case.

The program cannot stop with a NullPointerException in any possible combination. Test at least these four: ("REF-001", "ref-001"), ("REF-001", "REF-002"), (null, "REF-001"), ("REF-001", null).

Solutions

Solution 1

package com.nexussoftware.bibliotech;

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

        String title = "Effective Java";
        boolean available = false;
        int daysRemaining = 4;

        // Ordered ladder: first the most specific case (available),
        // then the two subcases of "not available".
        if (available) {
            System.out.println(title + ": AVAILABLE for immediate loan");
        } else if (daysRemaining > 0) {
            System.out.println(title + ": ON LOAN, back in " + daysRemaining + " days");
        } else {
            System.out.println(title + ": ON LOAN AND OVERDUE, chase the employee");
        }
    }
}

Output with the example values: Effective Java: ON LOAN, back in 4 days.

Test the other two cases by changing available to true and then leaving available = false with daysRemaining = 0. Notice that you do not need to write else if (!available && daysRemaining > 0): being in the else, we already know available is false. Repeating the condition would just be noise.

Solution 2

package com.nexussoftware.bibliotech;

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

        final int LOAN_DAYS = 15;
        final int MAX_LOANS = 3;

        String employee = "Nuria Vidal";
        String title = "Refactoring";
        String isbn = "978-0000000003";
        boolean available = true;
        double outstandingFine = 0.0;
        int activeLoans = 1;

        // --- Guard clauses: the invalid cases are discarded first ---

        if (employee == null || employee.isBlank()) {
            System.err.println("REJECTED: the employee name is mandatory.");
            return;
        }

        if (!available) {
            System.err.println("REJECTED: '" + title + "' is not available.");
            return;
        }

        // Comparing a double with a tolerance instead of != 0.0
        if (outstandingFine > 0.001) {
            System.err.printf("REJECTED: %s has %.2f EUR in outstanding fines.%n",
                              employee, outstandingFine);
            return;
        }

        if (activeLoans >= MAX_LOANS) {
            System.err.printf("REJECTED: %s already has %d active loans (maximum %d).%n",
                              employee, activeLoans, MAX_LOANS);
            return;
        }

        // --- Happy path: no extra indentation, no pending conditions ---

        System.out.println("=== LOAN AUTHORISED ===");
        System.out.printf("%-12s %s%n", "Employee:", employee);
        System.out.printf("%-12s %s (%s)%n", "Book:", title, isbn);
        System.out.printf("%-12s Return in %d days%n", "Term:", LOAN_DAYS);
        System.out.printf("%-12s %d of %d%n", "Loans:", activeLoans + 1, MAX_LOANS);
    }
}

Output with Nuria Vidal:

=== LOAN AUTHORISED ===
Employee:    Nuria Vidal
Book:        Refactoring (978-0000000003)
Term:        Return in 15 days
Loans:       2 of 3

With employee = "Diego Alonso" and outstandingFine = 4.50, the third guard fires and the program prints on System.err:

REJECTED: Diego Alonso has 4.50 EUR in outstanding fines.

The important thing about this exercise: four conditions and zero nesting. Each guard is independent, has its own message and can be added or removed without touching the others.

Solution 3

package com.nexussoftware.bibliotech;

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

        String reference = "REF-001";
        String searchedReference = null;

        // Null guard with short-circuiting: if the first part of each
        // '||' is true, isBlank() is never called on null.
        boolean anyMissing = reference == null || reference.isBlank()
                          || searchedReference == null || searchedReference.isBlank();

        if (anyMissing) {
            System.out.println("NO REFERENCE");
        } else if (reference.equalsIgnoreCase(searchedReference)) {
            System.out.println("MATCH");
        } else {
            System.out.println("NO MATCH");
        }
    }
}

Results for the four requested combinations:

reference searchedReference Output
"REF-001" "ref-001" MATCH
"REF-001" "REF-002" NO MATCH
null "REF-001" NO REFERENCE
"REF-001" null NO REFERENCE

Keys to the solution:

  1. The compound condition is extracted into anyMissing, a named boolean. It reads better and, when debugging, you can inspect its value at a glance.
  2. The order inside each || is critical: reference == null comes before reference.isBlank(). If you swapped them, the program would blow up with null.
  3. By the time the else if is reached, both variables are guaranteed non-null, so equalsIgnoreCase is safe. If the ladder were the other way round, it would not be.

Conclusion

Your program now decides. You have seen the simple if and why braces must never be omitted; if-else and the exhaustiveness guarantee it gives the compiler; the else if ladder, in which the order of the conditions is the logic; compound conditions with short-circuiting used as null guards; guard clauses as the antidote to nesting; the division of labour between the ternary (choosing a value) and if-else (running actions); the correct comparison of Strings with equals and equalsIgnoreCase, including the Yoda comparison for surviving null; and, above all, the habit of writing the decision table first and the code afterwards, which is what turned BiblioTech's four business rules into a clean, verifiable ladder.

BiblioTechApp now classifies the four fine cases correctly, warns when the cap is reached and rejects absurd input. But it gives up at the first hurdle: when it detects invalid data, it ends instead of asking again, and it still processes one single return per run.

In the next lesson, Loops, you will break that barrier. With while, do-while and for your program will be able to repeat a block as many times as needed: retrying the reading until the value is valid, going through the whole library catalog and accumulating the fines of an entire batch of returns without duplicating a single line of code.

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