Java flow control practice — a sign-up form, the student office desk and a three-attempt guessing game (and the passwords that didn’t match even though they were the same)

The first time I wrote a sign-up form in Java I was sure everything was right. I typed the password, repeated it letter by letter, pressed Enter… and the program calmly answered: “they don’t match”. I tried five times. I copied and pasted the password so I couldn’t get it wrong. Nothing. The bug wasn’t in what I typed but in a single line: I had compared the two texts with ==, as I did with numbers. In Java that compiles, runs and almost always gives false. This practice starts right there, with that program fixed and the bug triggered on purpose so you can see it with your own eyes.

If you come from the Java flow control theory, you already know the pieces: Scanner and the buffer trap, if/else if/else, the &&, || and ! operators, and switch in its three forms. Today we put them together in three medium-level programs with Laura, the FP2 student who has been with us since the syntax topic: her sign-up for the virtual campus (checking each piece of data and comparing texts properly), the student office desk (one procedure per run, with the arrow switch, the switch that returns a value and a fall-through done on purpose) and a guess-the-number game with three attempts and no loops. The data looks like real data: ages out of range, emails without a dot, grades with a decimal comma and options that don’t exist.

One warning before we start: there isn’t a single loop or array here. That’s not an oversight: they come in the next topic. And the third program is written this way on purpose, so you can see for yourself why they are needed.

Before you start

Make sure you are clear on:

  • How to read from the keyboard with Scanner: nextLine() for texts and nextInt()/nextDouble() for numbers, and why an empty nextLine() is needed after a nextInt() (the buffer trap).
  • That on a computer set to Spanish, nextDouble() expects a comma: you type 7,4, not 7.4.
  • How if, else if and else chain together: the first branch whose condition is true runs, and no other.
  • The difference between && (both things at once) and || (one is enough).

If any of these sounds shaky, go back to the theory before carrying on.

Create a flow_control_practice folder with one .java file per program (the file is named after the class: UserSignup.java). Compile and run with javac UserSignup.java and java UserSignup. The outputs in this article are real, from a single run with Java 21 on a computer set to Spanish: that’s why decimals show a comma (6,50 €) and the decimals typed at the keyboard carry one too. What you see after each question is what was typed. On a computer set to English, printf would print a point and nextDouble() would expect one. You can also try them in the site’s Java playground: what you would type goes in the Input tab, one line per value.

Program 1: User data validator

Laura signs up for the virtual campus. The program asks for her name, age, email, the password twice and whether she accepts the rules, and checks each piece of data separately. What makes it tricky: it has to report everything that is wrong at once (not stop at the first failure), compare texts without falling into ==, and not cut the email before knowing it has an @.

The idea

The program has three clearly separate phases:

  1. Read everything first, with the buffer trap solved right after nextInt().
  2. Check each piece of data with its own if/else. They are not branches of one chain: they are six independent questions, so they are six if statements one after another. An allGood variable, which starts as true, becomes false as soon as something fails.
  3. Decide at the end with a single if (allGood).

Before coding, think about each piece of data: what condition must it meet to be valid? Write it in positive form (“age between 16 and 99”) and then translate it: “between” is two conditions at once, so &&.

The code

import java.util.Scanner;

/**
 * Virtual campus sign-up: each piece of data is checked with if/else and texts are compared with equals().
 * Laura's data is made up.
 */
public class UserSignup {
    public static void main(String[] args) {
        Scanner sc = new Scanner(System.in);
        System.out.println("=== VIRTUAL CAMPUS SIGN-UP ===");

        System.out.print("Name and surname: ");
        String name = sc.nextLine().trim();      // trim() removes the spaces at the edges

        System.out.print("Age: ");
        int age = sc.nextInt();
        sc.nextLine();                           // takes away the Enter left by nextInt()

        System.out.print("Email: ");
        String email = sc.nextLine().trim();

        System.out.print("Password: ");
        String password = sc.nextLine();         // no trim(): a space is part of the password too

        System.out.print("Repeat the password: ");
        String password2 = sc.nextLine();

        System.out.print("Do you accept the campus rules? (y/n): ");
        String accepts = sc.nextLine().trim();

        System.out.println();
        System.out.println("--- Check ---");
        boolean allGood = true;                  // starts as true: any failure sets it to false

        // 1. The name cannot be empty
        if (name.isEmpty()) {
            System.out.println("Name: empty");
            allGood = false;
        } else {
            System.out.println("Name: correct");
        }

        // 2. Age between 16 and 99: BOTH things must hold, so &&
        if (age >= 16 && age <= 99) {
            System.out.println("Age: correct");
        } else {
            System.out.println("Age: out of range (16-99)");
            allGood = false;
        }

        // 3. Email: an @ that is not at the start and a dot after the @ (and not at the end)
        int at = email.indexOf('@');             // -1 if there is none
        int dot = email.lastIndexOf('.');        // the last dot: the one in .es, .com...
        if (at > 0 && dot > at + 1 && dot < email.length() - 1) {
            System.out.println("Email: correct");
        } else {
            System.out.println("Email: not valid");
            allGood = false;
        }

        // 4. Password: length first; if it is long, it must not be the name itself
        if (password.length() < 8) {
            System.out.println("Password: too short (" + password.length() + " of 8)");
            allGood = false;
        } else if (password.equalsIgnoreCase(name)) {
            System.out.println("Password: cannot be your name");
            allGood = false;
        } else {
            System.out.println("Password: correct");
        }

        // 5. The two passwords: equals() compares the letters, one by one and case-sensitive
        if (password.equals(password2)) {
            System.out.println("Repeat: matches");
        } else {
            System.out.println("Repeat: does not match");
            allGood = false;
        }

        // 6. Both "y" and "Y" are fine: equalsIgnoreCase ignores upper and lower case
        if (accepts.equalsIgnoreCase("y")) {
            System.out.println("Rules: accepted");
        } else {
            System.out.println("Rules: not accepted");
            allGood = false;
        }

        System.out.println();
        if (allGood) {
            // Only here is it safe to cut the email: we know the @ exists and is not at position 0
            String username = email.substring(0, at);
            System.out.println("Signed up. Your username is " + username + ".");
        } else {
            System.out.println("Cannot sign you up: check what is wrong.");
        }
        sc.close();
    }
}

Output

First, Laura with all the data right:

=== VIRTUAL CAMPUS SIGN-UP ===
Name and surname: Laura Santana
Age: 19
Email: laura.santana@ulpgc.es
Password: fp2-Laura26
Repeat the password: fp2-Laura26
Do you accept the campus rules? (y/n): y

--- Check ---
Name: correct
Age: correct
Email: correct
Password: correct
Repeat: matches
Rules: accepted

Signed up. Your username is laura.santana.

All six checks pass and the username comes from the email: everything before the @, that is, laura.santana. Now the same Laura, in a hurry and with everything wrong:

=== VIRTUAL CAMPUS SIGN-UP ===
Name and surname: Laura Santana
Age: 15
Email: laura@ulpgc
Password: laura
Repeat the password: Laura
Do you accept the campus rules? (y/n): n

--- Check ---
Name: correct
Age: out of range (16-99)
Email: not valid
Password: too short (5 of 8)
Repeat: does not match
Rules: not accepted

Cannot sign you up: check what is wrong.

Notice that the program doesn’t stop at the age: it keeps checking and shows the five failures at once. That’s what six independent if statements give you instead of an else if chain. Let’s check each line by hand:

DataValueConditionResult
Age1515 >= 16 && 15 <= 99 → false && …out of range
Emaillaura@ulpgcdot = lastIndexOf('.') = -1, and -1 > 5 + 1 is falsenot valid
Passwordlauralength() = 5, and 5 < 8too short (5 of 8)
Repeatlaura / Lauraequals is case-sensitivedoes not match
Rulesn"n".equalsIgnoreCase("y") → falsenot accepted

And a third run for the missing branch: the password is long, but it’s the name itself (with different capitals).

=== VIRTUAL CAMPUS SIGN-UP ===
Name and surname: Laura Santana
Age: 19
Email: laura.santana@ulpgc.es
Password: laura santana
Repeat the password: laura santana
Do you accept the campus rules? (y/n): Y

--- Check ---
Name: correct
Age: correct
Email: correct
Password: cannot be your name
Repeat: matches
Rules: accepted

Cannot sign you up: check what is wrong.

"laura santana".equalsIgnoreCase("Laura Santana") gives true, so it goes into the password’s else if. Notice the capital Y for the rules too: equalsIgnoreCase accepts it just like y.

Questions you might ask

Why six separate if statements and not an else if chain? Because a chain keeps one branch and skips the rest. If the age failed in an if and the email were in its else if, the email wouldn’t even be checked. Here we want all six answers, so each question goes in its own if. The else if chain does appear inside the password check: there the questions depend on each other (if it’s short, there’s no point checking whether it matches the name).

Why does allGood start as true? It’s the “innocent until proven guilty” pattern: it starts as true and any failing check drops it to false. None ever raises it again. If it started as false, you’d have to write allGood = true somewhere, and you’d end up accepting the data as soon as a single check passed.

What exactly does the email condition do? Let’s zoom in on that piece:

// 3. Email: an @ that is not at the start and a dot after the @ (and not at the end)
int at = email.indexOf('@');            // -1 if there is none
int dot = email.lastIndexOf('.');       // the last dot: the one in .es, .com...
if (at > 0 && dot > at + 1 && dot < email.length() - 1) {
    System.out.println("Email: correct");
} else {
    System.out.println("Email: not valid");
    allGood = false;
}

indexOf('@') returns the position of the first @ (or -1 if there is none). at > 0 requires it to exist and not be the first character. dot > at + 1 requires at least one letter between the @ and the dot (laura@.es is not valid). dot < email.length() - 1 prevents the dot from being the last character (laura@ulpgc. isn’t valid either). With laura.santana@ulpgc.es: at = 13, dot = 19 and length() = 22, so 13 > 0, 19 > 14 and 19 < 21: all three hold. It’s not a perfect validation (that’s what regular expressions are for, and they have their own topic), but it catches the real mistakes.

Why is the substring inside the if (allGood)? Because email.substring(0, at) with at = -1 throws StringIndexOutOfBoundsException. Inside if (allGood) we know the email passed the check, so the @ exists. It’s the idea of validating before using: first you make sure the data makes sense, then you work with it.

And what happens if I compare the passwords with ==? The same thing that happened to me. This short program reads both passwords and compares them both ways:

import java.util.Scanner;

/** The same repeat check, with == and with equals(). */
public class RepeatWrong {
    public static void main(String[] args) {
        Scanner sc = new Scanner(System.in);
        System.out.print("Password: ");
        String password = sc.nextLine();
        System.out.print("Repeat the password: ");
        String password2 = sc.nextLine();

        // == asks: "are they the SAME object in memory?". Each nextLine() creates a new String.
        if (password == password2) {
            System.out.println("With ==: they match");
        } else {
            System.out.println("With ==: they don't match");
        }
        // equals() asks: "do they have the same letters?"
        if (password.equals(password2)) {
            System.out.println("With equals(): they match");
        } else {
            System.out.println("With equals(): they don't match");
        }
        sc.close();
    }
}

Output, typing exactly the same thing twice:

Password: fp2-Laura26
Repeat the password: fp2-Laura26
With ==: they don't match
With equals(): they match

The diagram explains why:

Why == says the passwords do not matchEach nextLine() creates a new String object. password and password2 point to two different objects with the same letters fp2-Laura26: == compares the arrows and gives false; equals() compares the letters and gives true.VariablesHeap: two String objectspasswordfp2–Laura26object 1password2fp2–Laura26object 2password == password2the same arrow? → falsepassword.equals(password2)the same letters? → true
Two objects with the same letters: == checks whether the arrows end in the same place; equals(), whether they say the same thing.

Each nextLine() creates a new String object. The two variables point to two different objects that, by chance, have the same letters. == asks whether both arrows reach the same object (no), and equals() asks whether both objects say the same thing (yes).

Comparing texts with ==. It compiles without warnings and with literals written in the code it sometimes “works” (Java reuses identical literals), which makes it even more treacherous: as soon as the text comes from the keyboard, it gives false. With String, always equals() (or equalsIgnoreCase() if case doesn’t matter).

password.equals(password2) would throw NullPointerException if password were null. With Scanner that can’t happen (nextLine() returns at worst an empty string), but when you compare with a fixed text you can turn it around: "y".equalsIgnoreCase(accepts) never fails, even if accepts is null.

Try it yourself

  • Add a seventh check: the email must end in @ulpgc.es (hint: endsWith). Does it go as a separate if or as an else if of the email?
  • Change the equals of the repeat check to ==, run it with the right data and see what happens to the sign-up. Then put it back.

Program 2: The student office desk with switch

Laura goes to the student office desk. A menu offers three procedures: request a certificate (free in digital form, 6.50 € on paper, 12.75 € urgent), change group (granted if she has a job or a 7 average) and see how much tuition is left to pay (4 instalments of 180.40 €). Prices and rules are made up. Each run does one procedure: without a loop you can’t go back to the menu (that will come with do-while).

The idea

A numbered menu is the natural home of switch: each number is a case. The three forms you saw in the theory appear here, each where it fits best:

  1. The arrow switch for the menu: each procedure does its own thing and doesn’t fall into the next.
  2. The switch as an expression for the certificate price: each case returns a value and the result is stored in a variable.
  3. The classic switch without break, on purpose, to add up the missing instalments: it’s the only place in the whole practice where fall-through helps us.

Before coding, ask yourself for each switch: do I want to do something different in each case (arrows), calculate a value depending on the case (expression), or have the cases pile up one on top of another (classic without break)?

The code

import java.util.Scanner;

/**
 * Student office desk: one procedure per run, with the three forms of switch.
 * Prices and instalments made up.
 */
public class OfficeDesk {
    public static void main(String[] args) {
        Scanner sc = new Scanner(System.in);
        final int INSTALMENT = 18040;                // each tuition instalment, in cents (180.40 €)

        System.out.println("=== STUDENT OFFICE DESK ===");
        System.out.println("1. Request a certificate");
        System.out.println("2. Change group");
        System.out.println("3. See what is left to pay");
        System.out.println("0. Exit");
        System.out.print("Procedure: ");
        int procedure = sc.nextInt();
        sc.nextLine();                               // Enter out: lines are read afterwards

        // switch with arrows (Java 14+): each case does its own thing and does not fall into the next
        switch (procedure) {
            case 1 -> {
                System.out.print("Format (digital/paper/urgent): ");
                String format = sc.nextLine().trim().toLowerCase();
                // switch as an EXPRESSION: each case returns a price and the result is stored
                double price = switch (format) {
                    case "digital" -> 0.0;
                    case "paper" -> 6.50;
                    case "urgent" -> 12.75;
                    default -> -1;                   // marker for "unknown format"
                };
                if (price < 0) {
                    System.out.println("Unknown format: " + format);
                } else if (price == 0) {
                    System.out.println("Digital certificate: free, it arrives by email.");
                } else {
                    System.out.printf("Certificate on %s: %.2f €%n", format, price);
                }
            }
            case 2 -> {
                System.out.print("Current group (morning/afternoon): ");
                String current = sc.nextLine().trim().toLowerCase();
                System.out.print("Do you work? (y/n): ");
                boolean works = sc.nextLine().trim().equalsIgnoreCase("y");
                System.out.print("Average grade: ");
                double average = sc.nextDouble();    // on a computer set to Spanish, with a comma: 7,4
                // First rule out the impossible; then the rule: having a job OR a 7 average
                if (!current.equals("morning") && !current.equals("afternoon")) {
                    System.out.println("Invalid group: " + current);
                } else if (works || average >= 7) {
                    String newGroup = current.equals("morning") ? "afternoon" : "morning";
                    System.out.println("Change granted: you move to the " + newGroup + " group.");
                } else {
                    System.out.println("Change refused: you need a job or a 7 average.");
                }
            }
            case 3 -> {
                System.out.print("Instalments paid (0-4): ");
                int paid = sc.nextInt();
                int pending = 0;
                // classic switch WITHOUT break on purpose: from the entry case, each instalment adds the next one
                switch (paid) {
                    case 0:
                        pending += INSTALMENT;       // instalment 1 is missing...
                    case 1:
                        pending += INSTALMENT;       // ...number 2...
                    case 2:
                        pending += INSTALMENT;       // ...number 3...
                    case 3:
                        pending += INSTALMENT;       // ...and number 4
                        break;                       // here, yes: don't carry on to default
                    case 4:
                        break;                       // all paid: pending stays at 0
                    default:
                        pending = -1;                // impossible data
                }
                if (pending < 0) {
                    System.out.println("Invalid data: tuition has 4 instalments.");
                } else if (pending == 0) {
                    System.out.println("Tuition paid. Nothing pending.");
                } else {
                    System.out.printf("You have %d instalments left: %.2f €%n", 4 - paid, pending / 100.0);
                }
            }
            case 0 -> System.out.println("Goodbye.");
            default -> System.out.println("Invalid procedure: " + procedure);
        }
        sc.close();
    }
}

Output

Five runs, one per path. A paper certificate:

=== STUDENT OFFICE DESK ===
1. Request a certificate
2. Change group
3. See what is left to pay
0. Exit
Procedure: 1
Format (digital/paper/urgent): paper
Certificate on paper: 6,50 €

"paper" goes into case "paper" -> 6.50, and since 6.50 is neither below 0 nor equal to 0, it goes to the else and is printed with %.2f, which on a computer set to Spanish uses a comma. Group change with a 7.4 average (no job):

=== STUDENT OFFICE DESK ===
1. Request a certificate
2. Change group
3. See what is left to pay
0. Exit
Procedure: 2
Current group (morning/afternoon): morning
Do you work? (y/n): n
Average grade: 7,4
Change granted: you move to the afternoon group.

works || average >= 7 is false || true, that is, true. And with a 6.9 from the afternoon group:

=== STUDENT OFFICE DESK ===
1. Request a certificate
2. Change group
3. See what is left to pay
0. Exit
Procedure: 2
Current group (morning/afternoon): afternoon
Do you work? (y/n): n
Average grade: 6,9
Change refused: you need a job or a 7 average.

false || false gives false: refused. Now the instalments, with one already paid:

=== STUDENT OFFICE DESK ===
1. Request a certificate
2. Change group
3. See what is left to pay
0. Exit
Procedure: 3
Instalments paid (0-4): 1
You have 3 instalments left: 541,20 €

This is the switch that deserves a diagram:

The fall-through that adds up the missing instalmentsWith paid = 1 the switch enters case 1 and, as there is no break, carries on through case 2 and case 3: pending goes through 18040, 36080 and 54120 cents. The break in case 3 takes it out before case 4 and default. 54120 / 100.0 is 541.20 euros.switch (paid) with paid = 1pendingcase 0:pending += INSTALMENT;case 1:pending += INSTALMENT;18040case 2:pending += INSTALMENT;36080case 3:pending += INSTALMENT; break;54120case 4:break;default:pending = -1;enters hereexits54120 / 100.0 → 541,20 €Without break, each case falls into the next one:here that is exactly what we want.
You enter at the case of the instalments already paid and go down adding until the break.

With paid = 1, Java jumps straight to case 1: and starts going down: it adds instalment 2 (18040), falls into case 2: and adds 3 (36080), falls into case 3: and adds 4 (54120), and there it finds the break. Check: 3 × 18040 = 54120 cents, and 54120 / 100.0 = 541.20 €. Finally, an option that doesn’t exist:

=== STUDENT OFFICE DESK ===
1. Request a certificate
2. Change group
3. See what is left to pay
0. Exit
Procedure: 7
Invalid procedure: 7

Questions you might ask

Why are the instalments in cents (int) and the certificate in double? Because the instalments are added up: three sums of 180.40 in double can leave remainders like 541.1999999…, which printf would hide but would still be there. In cents everything is exact and you only divide by 100.0 when printing. The certificate price isn’t added to anything: it’s printed as it is, so double causes no trouble.

Why default -> -1 in the price switch? Because a switch that returns a value must return something in every case. If you remove the default, javac complains: with a String, any text can arrive. The -1 is an impossible marker for a price, and the if (price < 0) afterwards detects it.

Why toLowerCase() when reading the format? Because a switch on a String compares with equals(), which is case-sensitive. Without toLowerCase(), Paper wouldn’t enter case "paper" and would end in the default. Lowercasing everything when reading is shorter than writing case "paper", "Paper", "PAPER".

Why is the invalid group ruled out before the rule? Because of the order of the chain: first the impossible, then the rule. If the rule came first, someone typing “night” with an 8 average would get a “Change granted” to a group that doesn’t exist. Also look at !current.equals("morning") && !current.equals("afternoon"): “it is not morning and it is not afternoon”. With || it would always be true (nothing can be morning and afternoon at once), a mistake you’ll see in the exercises.

And the empty case 4: break;? Without it, paid = 4 would fall into default and set pending = -1, as if the data were impossible. That break says “there’s nothing to add here, but the data is valid”.

Forgetting a break in a classic switch when you don’t want fall-through. It compiles without warnings and the cases overwrite each other: you’ll see one in the exercises with the library loan days. If you don’t need the cases to pile up, use arrows: with -> there is no fall-through.

javac -Xlint:fallthrough OfficeDesk.java warns about every case that falls into the next one. Here we do it on purpose, so the warning is noise, but in your own program it can save you an afternoon.

Try it yourself

  • Add the "apostille" format at 21.00 € and check that Apostille with a capital also works.
  • Change the instalments switch so that instead of adding, it counts how many instalments are missing, with the same idea of falling from one case to the next.

Program 3: Guess the number with three attempts (no loops)

The classic: the program thinks of a number between 1 and 100 and you have three attempts. After each miss it tells you whether it’s higher or lower; if you go out of range, you lose the attempt. What makes it tricky: we can’t use loops, so the same block of code is copied three times. By the end you’ll hate that copy and paste, and that’s exactly what we want.

The idea

  • The secret number comes from Random with a fixed seed (42): with the same seed, nextInt(100) + 1 always gives the same number. That way it can be tested and the outputs in this article are repeatable. In a real game you remove the seed and it changes every game.
  • Each attempt is an if/else if/else chain with four paths: out of range, correct, higher or lower.
  • If you get it right, return; ends main at that very moment. That way you don’t need to put attempt 2 inside the else of attempt 1 (and attempt 3 inside attempt 2): the code stays flat.

Before coding, order the conditions: what should be checked first, the range or the guess? If the range came later, a 150 would land in “Lower” and the player wouldn’t know they had gone out of range.

The code

import java.util.Random;
import java.util.Scanner;

/**
 * Guess the number in 3 attempts, WITHOUT loops: the same block copied three times.
 * When you finish, count the repeated lines: that is why loops exist.
 */
public class Guess {
    public static void main(String[] args) {
        Scanner sc = new Scanner(System.in);
        // Fixed seed (42): the secret number is always the same, so it can be tested. Without a seed, it changes every time.
        Random random = new Random(42);
        int secret = random.nextInt(100) + 1;       // nextInt(100) gives 0 to 99; with +1, 1 to 100

        System.out.println("=== GUESS THE NUMBER (1-100) ===");
        System.out.println("You have 3 attempts.");

        // ---- Attempt 1
        System.out.print("Attempt 1: ");
        int n = sc.nextInt();
        if (n < 1 || n > 100) {
            System.out.println("That is not between 1 and 100: you lose the attempt.");
        } else if (n == secret) {
            System.out.println("First try! It was " + secret + ".");
            return;                                 // return in main: the program ends here
        } else if (n < secret) {
            System.out.println("Higher.");
        } else {
            System.out.println("Lower.");
        }

        // ---- Attempt 2: the same block, again
        System.out.print("Attempt 2: ");
        n = sc.nextInt();
        if (n < 1 || n > 100) {
            System.out.println("That is not between 1 and 100: you lose the attempt.");
        } else if (n == secret) {
            System.out.println("Second try! It was " + secret + ".");
            return;
        } else if (n < secret) {
            System.out.println("Higher.");
        } else {
            System.out.println("Lower.");
        }

        // ---- Attempt 3: the last one gives no hints
        System.out.print("Attempt 3: ");
        n = sc.nextInt();
        if (n == secret) {
            System.out.println("Just in time! It was " + secret + ".");
        } else {
            System.out.println("No attempts left. The number was " + secret + ".");
        }
        sc.close();
    }
}

Output

The secret with seed 42 turns out to be 31. A game played by “halving”:

=== GUESS THE NUMBER (1-100) ===
You have 3 attempts.
Attempt 1: 50
Lower.
Attempt 2: 25
Higher.
Attempt 3: 31
Just in time! It was 31.

And a disastrous game, with an out-of-range number in the middle:

=== GUESS THE NUMBER (1-100) ===
You have 3 attempts.
Attempt 1: 50
Lower.
Attempt 2: 150
That is not between 1 and 100: you lose the attempt.
Attempt 3: 20
No attempts left. The number was 31.

The diagram shows which condition wins in each attempt of the first game:

Three attempts without a loop: three copies of the same ifWith secret 31: attempt 1 (50) falls into the else and says Lower; attempt 2 (25) goes into n < secret and says Higher; attempt 3 (31) gets it. Each attempt repeats the same block of conditions.Secret: 31. The same block, copied three timesAttempt 1: 50n < 1 || n > 100n == secretn < secretelseLower.Attempt 2: 25n < 1 || n > 100n == secretn < secretelseHigher.Attempt 3: 31n == secretelseGot it≈ 12 lines per attempt: with 10 attempts that would be 120. In Loops, a while does it in 10.
In coral, the condition that wins in each attempt. The code is the same three times: that is a loop waiting to be born.

Attempt 1: 50 is in range, isn’t 31 and isn’t below 31, so it falls into the else: “Lower”. Attempt 2: 25 < 31, “Higher”. Attempt 3: 31 == 31.

Questions you might ask

Why nextInt(100) + 1? nextInt(100) returns a number from 0 to 99 (100 is left out, just like the end of range() in Python). To go from 1 to 100 you add 1 to everything: 0 becomes 1 and 99 becomes 100.

What does return; do inside main? It ends the method at that point. Since main is the whole program, the program ends: attempt 2 never runs. Without that return, after a first-try hit the program would still ask for attempt 2.

Why doesn’t attempt 3 check the range? Because there’s no hint left to give: if it isn’t right, the game is over whether it’s in range or not. Fewer branches, less to read.

Why is n declared once and then just reused? Because int n in attempt 2 would be an error: there’s already a variable n in the same block. The second time it’s just given a new value: n = sc.nextInt();.

And if I want ten attempts? Count: about 12 lines per attempt, so 120 almost identical lines. And if one day you change the “Higher.” message, you change it ten times. That’s exactly what Java loops solve: the same game with a while fits on one screen.

Putting the range check after the hints: if (n == secret) … else if (n < secret) … else if (n > secret) … else if (n < 1 || n > 100) …. That last branch never runs: any number other than the secret is either lower or higher than it, so a 150 stays in “Lower” and a -3 in “Higher”. The first true condition wins: the most restrictive goes first.

Random without a seed (new Random()) uses a different seed on every run. With a seed, the sequence is always the same on any computer with Java: that’s why your 42 will also give 31.

Try it yourself

  • Change the seed to 7 and play without looking at the code: what’s the secret? Check afterwards by printing it at the start.
  • Add a “hot!” hint when you’re within 5 of the secret (hint: Math.abs(n - secret) <= 5). In how many places do you have to add it?

Common mistakes in this practice

  • Comparing String with ==. It compiles and gives false with texts read from the keyboard even when they’re equal. Always equals() or equalsIgnoreCase().
  • Using an else if chain when the checks are independent. Only one branch runs, so you stop checking the rest. To validate several pieces of data, one if per piece.
  • Ordering conditions badly. The first true one wins: the impossible (out of range, unknown format) goes before the rules.
  • Forgetting the break in a classic switch. The cases pile up without warning. Unless you want it on purpose, use arrows.
  • Using data before validating it. substring(0, at) with at = -1 blows up: check first, operate afterwards.
  • Typing 7.4 on a computer set to Spanish. nextDouble() throws InputMismatchException: you saw it in the theory, and here it was typed with a comma.

Watch it step by step in Python Tutor

Python Tutor (pythontutor.com) runs code line by line and draws variables and objects with their arrows. Choose Java in the drop-down and paste this program, which combines the instalments fall-through and the two passwords (no Scanner: Python Tutor can’t type, so the data is fixed and the passwords are created with new String so that they’re two objects, as with nextLine()).

public class TutorInstalments {
    public static void main(String[] args) {
        int paid = 1;
        int pending = 0;
        switch (paid) {
            case 0:
                pending += 18040;
            case 1:
                pending += 18040;
            case 2:
                pending += 18040;
            case 3:
                pending += 18040;
                break;
            default:
                pending = -1;
        }
        String password = new String("fp2-Laura26");
        String repeated = new String("fp2-Laura26");
        boolean sameObject = password == repeated;
        boolean sameLetters = password.equals(repeated);
        System.out.println(pending + " " + sameObject + " " + sameLetters);
    }
}

Output:

54120 false true

What to watch step by step:

  1. When it reaches the switch, the red arrow jumps straight to case 1:: it doesn’t go through case 0: or evaluate it.
  2. After pending += 18040 it doesn’t leave the switch: the next line is case 2:. Watch pending go through 18040, 36080 and 54120.
  3. At the break it jumps out of the switch, without touching default.
  4. When password and repeated are created, Python Tutor draws two String boxes on the heap, each with its own arrow: it’s the diagram from this practice.
  5. Look at the values: sameObject is false and sameLetters is true.

Summary and next step

  • To validate several pieces of data, one if per piece and an allGood variable that starts as true: that way you report every failure at once.
  • Texts are compared with equals() (or equalsIgnoreCase()); == checks whether they are the same object.
  • In an else if chain, the first true condition wins: the impossible and the most restrictive go first.
  • The three forms of switch: arrows to do different things, expression to calculate a value (with a mandatory default) and classic without break when the cases pile up on purpose.
  • return; in main ends the program and saves you from nesting one block inside another.
  • Copying the same block three times works, but it doesn’t scale: that’s what loops are for.

Checklist: have you practised it?

  • Have you run the three programs and checked the outputs?
  • Can you explain why == says that two identical passwords don’t match?
  • Could you say, without running it, what the office desk prints with 0 instalments paid?
  • Do you know when to use six separate if statements and when an else if chain?
  • Have you tried the “Try it yourself” sections without looking at the theory?

If you answered yes to almost everything, you’re ready for the Java flow control exercises, where I no longer give you the code. See you there!

Next step: exercisesJava flow control exercises — the entry grade, the library and a three-attempt ATM (and the 9.5 that came out as “Pass”)

Did this article help you? Here is how you can support SergioLearns →