RizTech Academy logo
RizTech Academy
Language BasicsLesson 4 of 630 min

if, switch expressions and loops

Control flow is the part of a language you think you already know. if, loops, switch — every language has them and they mostly look the same.

Java's differ in two ways that matter. Its old switch has a default behaviour that is almost never what you want, and its new one fixes that while also becoming an expression. Knowing both is not optional, because you will write the new form and read the old one.

if, and the brace habit

if (totalPaise >= 50_000) {
    System.out.println("Free delivery");
} else if (distanceMetres <= 3_000) {
    System.out.println("Delivery Rs 20");
} else {
    System.out.println("Delivery Rs 20 plus Rs 8 per extra km");
}

The condition must be a boolean. Java will not accept a number, so C's if (count) meaning "count is non-zero" does not compile — a small mercy, and one reason Java has fewer of these bugs than C.

It does not save you from this, though:

boolean delivered = false;
if (delivered = true) {
    System.out.println("this branch runs, and delivered is now " + delivered);
}
this branch runs, and delivered is now true

One = instead of two. Because the assignment's value is a boolean, it is a legal condition, the branch always runs, and the variable is quietly overwritten. IntelliJ warns about it. Take the warning seriously.

Always use braces, even for one statement. if (x) doThing(); is legal and has caused famous security bugs, because the next person adds a second line, indents it to match, and it is not inside the if at all.

The old switch, and why it bites

switch (plan) {
    case "veg":
        System.out.println(plan + " -> Rs 82.35");
    case "jain":
        System.out.println(plan + " -> Rs 91.00");
        break;
    default:
        System.out.println(plan + " -> unknown");
}

Run it with "veg", "jain" and "eggs":

veg -> Rs 82.35
veg -> Rs 91.00
jain -> Rs 91.00
eggs -> unknown

The first customer was charged twice. There is no break after the veg case, so execution falls through into the next one and keeps going until it meets a break or the end.

This is the default. Every case needs a break that you have to remember, which means a missing one is a silent bug rather than an error. Occasionally fall-through is deliberate — stacking case "sat": case "sun": to share a body — and that legitimate use is why the language cannot simply change the behaviour.

When you write an old-style switch, every case ends with break or return. When you read one, check each case for a missing break before assuming it is correct.

The modern switch

Java 14 made switch an expression with arrow labels, and it removes the whole problem:

String kind = switch (day) {
    case 1, 2, 3, 4, 5 -> "working day";
    case 6, 7 -> "weekend";
    default -> "not a day";
};

Four improvements in one line each:

  • No fall-through. Arrow cases do not run on into the next.
  • Several labels per case, comma separated, instead of stacked empty cases.
  • It produces a value, so the variable can be assigned once and be final. The old form forced you to declare String kind; and assign it in each branch, which is where a forgotten branch left it unassigned.
  • Exhaustiveness is checked when the compiler can know all the cases — on an enum or a sealed type. If you add a value later and forget a case, the build fails instead of the default quietly swallowing it. This becomes properly useful in module 3.

When a branch needs more than one statement, use a block and yield to produce its value:

int discountPercent = switch (tiffins / 10) {
    case 0 -> 0;
    case 1 -> 5;
    default -> {
        int extra = tiffins / 10;
        yield Math.min(5 + extra, 15);
    }
};
26 tiffins -> discount 7%

yield is to a switch expression what return is to a method: it supplies the value of this branch. return inside a switch expression is not allowed, which is a good thing — it would be ambiguous.

switch works on int, char, String, enums, and — from Java 21 — any type at all through pattern matching, which module 3 covers.

One sharp edge: switching on a null String throws NullPointerException, even when there is a default. Check for null first, or use pattern matching's case null in Java 21.

Loops, and which to use

Loop Use it when
for (int i = 0; i < n; i++) You need the index, or a count
for (T item : collection) You need the items and not their positions
while (condition) The number of iterations is unknown in advance
do { } while (condition) The body must run at least once
int[] counts = {4, 0, 12};

for (int i = 0; i < counts.length; i++) {
    System.out.print(counts[i] + " ");
}

for (int c : counts) {
    System.out.print(c + " ");
}

Both print 4 0 12. Prefer the enhanced for — no index means no off-by-one — and reach for the indexed form only when you genuinely need i.

do/while is rare, and menu loops are its one natural home: show the menu, read a choice, repeat until the user quits. The body must run once before there is anything to test.

break, continue, and labels

break leaves the loop. continue skips to the next iteration.

A labelled break leaves an outer loop from inside an inner one:

outer:
for (int[] row : grid) {
    for (int value : row) {
        if (value == 4) {
            break outer;
        }
        System.out.println("checked " + value);
    }
}
checked 1
checked 2
checked 3
found 4, leaving both loops

A plain break would only have left the inner loop. Labels are the one piece of goto-like machinery Java kept, and they are worth using for exactly this — two nested loops with a single exit condition. Three levels deep, extract a method and return instead.

Three loop bugs worth causing on purpose

Off by one. <= where < belongs:

int[] days = {1, 2, 3};
for (int i = 0; i <= days.length; i++) {
    System.out.print(days[i] + " ");
}
1 2 3
java.lang.ArrayIndexOutOfBoundsException: Index 3 out of bounds for length 3

Three elements at indices 0, 1 and 2. length is 3, and index 3 does not exist. A for loop over an array uses <, never <=.

continue skipping the increment. In a for loop, continue still runs the increment. In a while loop it does not:

int i = 0;
while (i < 3) {
    if (i == 1) {
        continue;      // i is never incremented again
    }
    System.out.println("i = " + i);
    i++;
}

That prints i = 0 and then hangs forever. The increment is inside the body, and continue jumps over it. If you must continue in a while, increment before the check, or use a for.

Comparing doubles with ==.

for (double d = 0.0; d != 1.0; d += 0.1) {
    ...
}

After twenty iterations d is 2.0000000000000004 and still going. Ten additions of 0.1 never land exactly on 1.0, for the reason the types lesson gave. Loop counters are integers. If you need decimal steps, count in integers and divide when you use the value.

Check your work

Why did the old-style switch print two prices for "veg"? No break after the veg case, so execution fell through into jain and ran its body too. Fall-through is the default, and a missing break is not an error.

Name three things the arrow form of switch fixes. No fall-through; several labels per case; it produces a value so the result can be assigned once; and exhaustiveness checking on enums and sealed types.

What is yield for? Producing the value of a switch expression branch that needs more than a single expression. return is not allowed there.

What happens when you switch on a null String? NullPointerException, even with a default branch present. Check for null first.

Why does if (delivered = true) compile? The assignment's value is a boolean, so it is a valid condition. It always evaluates to true and overwrites the variable as a side effect.

Why is i <= array.length wrong? Valid indices run from 0 to length - 1. length itself is one past the end, giving ArrayIndexOutOfBoundsException: Index 3 out of bounds for length 3.

Why can continue hang a while loop but not a for loop? A for loop's increment is part of the loop's own machinery and runs on continue. A while loop's increment is a statement in the body, and continue jumps over it.

Practice 3, FizzBuzz for a delivery sheet.

for (int day = 1; day <= 30; day++) {
    String note = switch (day % 7) {
        case 0 -> "rest day";
        default -> (day % 10 == 0) ? "collect payment" : "deliver";
    };
    System.out.println(day + " : " + note);
}

Day 70 would be both, and this version silently picks "rest day". If both matter, a switch is the wrong shape and an if/else if chain that you order deliberately is the right one. That is the real lesson of the exercise.

Practice 5, the grading loop. It does not compile:

error: variable grade might not have been initialized
        System.out.println(marks + " -> " + grade);
                                            ^

There is no else, so when marks is 40 no branch assigns grade, and the compiler refuses to let you read it. That is the local-variable rule from the types lesson doing real work.

Two things worth noticing. Had grade been declared outside the loop and initialised once, it would have compiled and each unmatched student would have silently inherited the previous student's grade — a far worse outcome than a build failure. And the switch expression form makes the whole mistake impossible, because every path must produce a value:

String grade = switch (marks / 10) {
    case 10, 9 -> "A";
    case 8 -> "B";
    case 7 -> "C";
    case 6 -> "D";
    default -> "F";
};

Practice

  1. Reproduce the fall-through. Write the old-style switch from this lesson without the break in the first case, run it with "veg", and see the double charge. Then convert it to the arrow form and confirm the problem cannot be expressed.

  2. Convert an if/else if chain to a switch expression. Take the delivery charge from the operators lesson — free above Rs 500, Rs 20 within 3 km, Rs 20 plus Rs 8 per extra km — and write both. Note which one you can assign to a final variable.

  3. A delivery sheet for 30 days. Print each day numbered 1 to 30. Every seventh day is a rest day. Every tenth day is a payment collection day. Print the right note against each. Decide what day 70 should say, and make your code say it on purpose.

  4. Cause all three loop bugs. The <= off-by-one, the continue that hangs a while, and the double loop counter that never terminates. Read each failure. For the two that hang, stop them with Ctrl+C and note that a hanging program gives you no error message at all — which is why they are harder to find than a crash.

  5. Find the bug in this, then fix it with a switch expression.

    for (int marks : new int[]{95, 82, 40, 71}) {
        String grade;
        if (marks >= 90) grade = "A";
        else if (marks >= 80) grade = "B";
        else if (marks >= 70) grade = "C";
        System.out.println(marks + " -> " + grade);
    }
    

    It does not compile as written. Work out why, then write the version that does — and notice that the compiler caught for you exactly the mistake that the old switch would have let through.

  6. Harder — a menu loop. Use do/while to show a three-option menu (1 add delivery, 2 show total, 3 quit), read a number, and act on it. Handle an unknown option without exiting. You need the next lesson but one to read input properly, so for now drive it from an array of choices and loop over that instead — the shape is what matters.

Next: arrays, their fixed size, and why printing one gives you [I@1b6d3586.

Stuck on this lesson?

Being stuck is part of it — but being stuck alone for three days is not. Our internship programme pairs this curriculum with code review and one-to-one help from working developers, and it is free.

About the internship