Just added: Algorithms you can run and practice
Updated Jun 22, 2026 Edit on GitHub

Control Flow & Methods - Decisions, Loops & Reusable Logic

So far your programs have run in a straight line: top to bottom, every statement once. Real programs make decisions ("if logged in, show the dashboard"), repeat work ("for every order, send a receipt"), and bundle logic into named pieces you can call again and again. Branching, looping, and methods are the joints that let a program bend.

The mental model for this phase: control flow is about choosing which statements run and how often, and methods are about naming a chunk of statements so you can reuse it. Everything below is a variation on those two themes.

if / else - making a decision

The most basic branch: give Java a boolean expression - something evaluating to true or false - and it picks a path.

int temperature = 30;

if (temperature > 25) {
    System.out.println("Warm");
} else if (temperature > 10) {
    System.out.println("Mild");
} else {
    System.out.println("Cold");
}
Warm

What just happened: Java checked the conditions top to bottom and ran the first block whose condition was true. temperature > 25 is 30 > 25, true, so it printed Warm and skipped the rest entirely - once a branch wins, the others aren't even evaluated. Parentheses around the condition are required in Java (unlike Python), and braces { } group each branch's statements.

Conditions are boolean expressions, built with comparison operators (>, <, >=, <=, ==, !=) and combined with && (and), || (or), and ! (not):

boolean loggedIn = true;
int age = 20;

if (loggedIn && age >= 18) {
    System.out.println("Access granted");
}
Access granted

What just happened: loggedIn && age >= 18 is true && (20 >= 18), i.e. true && true, so the block ran. && is short-circuiting: if the left side were false, Java wouldn't check the right side at all, since the whole thing can't be true anymore. That's not just a speed trick - it lets you write guards like if (user != null && user.isActive()) where the second check is only safe because the first passed.

💡 Key point. Keep conditions explicit and readable. if (count > 0) says exactly what it means. Resist nested ternaries or stacked negations - code is read far more than it's written, and a clear if is a gift to whoever debugs this at 2 a.m.

switch - one value, many cases

When checking a single value against many possible matches, a tall stack of else if gets noisy. switch is built for that. Java has two forms, and the difference matters.

The classic switch statement

The old form has a sharp edge worth knowing. Here it is, done correctly:

int day = 3;
String name;

switch (day) {
    case 1:
        name = "Monday";
        break;
    case 2:
        name = "Tuesday";
        break;
    case 3:
        name = "Wednesday";
        break;
    default:
        name = "Unknown";
}

System.out.println(name);
Wednesday

What just happened: switch (day) jumped to the case matching day (3), set name to "Wednesday", and break stopped it. default is the catch-all when nothing matches. Burn that break into memory.

⚠️ Gotcha - fall-through. In the classic switch, execution does not stop at the end of a case. It runs straight into the next case until it hits a break (or the end of the switch). Forget a break and you get a bug that's hard to spot:

int day = 1;
switch (day) {
    case 1:
        System.out.println("Monday");
        // no break - execution falls through!
    case 2:
        System.out.println("Tuesday");
        break;
}
Monday
Tuesday

What just happened: day was 1, so it matched case 1 and printed "Monday". No break, so execution fell through into case 2 and printed "Tuesday" too - even though day isn't 2. The single most common switch mistake, and exactly why the modern form exists. (Fall-through is occasionally useful when several cases should share code, but that's a deliberate, documented choice - never an accident.)

The modern switch expression

Newer Java (14+) gives you a switch expression with arrow syntax: it returns a value, never falls through, and reads cleanly.

int day = 3;

String name = switch (day) {
    case 1 -> "Monday";
    case 2 -> "Tuesday";
    case 3 -> "Wednesday";
    default -> "Unknown";
};

System.out.println(name);
Wednesday

What just happened: The whole switch (...) { ... } evaluated to a value assigned straight into name

  • notice the = before it and ; after the closing brace. Each case ... -> runs only its own branch with no fall-through, so there's no break to forget. You can list several values in one case (case 1, 2, 3 -> ...), and the compiler can even warn you if you miss a possible value. Same idea as the classic form, minus the footgun.

💡 Key point. Reach for the switch expression (->) by default in modern Java: safer (no fall-through), more concise, and it produces a value you can assign or return directly. Keep the classic form only for older code or genuine fall-through needs.

Loops - repeating work

Three loop shapes cover almost everything, differing in when you know how many times to repeat.

for - when you're counting, or you know the iteration count up front:

for (int i = 0; i < 3; i++) {
    System.out.println("Pass " + i);
}
Pass 0
Pass 1
Pass 2

What just happened: The for header has three parts: init (int i = 0, runs once), condition (i < 3, checked before every pass), and update (i++, adds one to i after each pass). i walked through 0, 1, 2, and the loop stopped the moment i reached 3.

while - when you repeat until some condition flips, and you don't know the count ahead of time:

int countdown = 3;
while (countdown > 0) {
    System.out.println(countdown);
    countdown--;
}
3
2
1

What just happened: while (countdown > 0) checked the condition before each pass and ran the body as long as it held. Each pass printed countdown, then countdown-- subtracted one. When countdown hit 0, the condition became false and the loop ended. ⚠️ Forget the countdown-- and the condition never changes - an infinite loop, the classic "why is my program frozen?" bug.

Enhanced for (for-each) - to visit every element of a collection or array without caring about index numbers:

List<String> names = List.of("Ada", "Linus", "Grace");

for (String name : names) {
    System.out.println(name);
}
Ada
Linus
Grace

What just happened: for (String name : names) reads as "for each name in names." Each pass binds name to the next element, in order, until the list runs out - no index, no i++, no off-by-one risk. Prefer it whenever you don't need the index. (List.of(...) came from Phase 3.)

💡 Key point. Pick the loop that says what you mean: for-each to walk a collection, for to count or need the index, while to loop until a condition changes.

Methods - naming reusable logic

Once you've written the same handful of statements twice, it's time for a method.

📝 Method - a named, reusable block of logic with a return type (what it hands back, or void for nothing), a name, a list of parameters (its inputs), and a body. An access modifier like public or private controls who's allowed to call it. You "call" a method by its name to run its body.

public class Calculator {

    // a method: returns an int, named add, takes two int parameters
    public static int add(int a, int b) {
        return a + b;
    }

    public static void main(String[] args) {
        int sum = add(3, 4);
        System.out.println(sum);
    }
}
7

What just happened: public static int add(int a, int b) declares a method named add taking two int parameters and returning an int. Inside main, add(3, 4) called it with 3 and 4, which became a and b; return a + b handed back 7, stored in sum. The return type comes first in Java (int add(...)), unlike Go where it comes last.

You've been staring at one keyword on every method so far: static. Just enough to read it.

📝 static vs instance. A static method belongs to the class itself - call it without creating an object (Calculator.add(3, 4)). A non-static (instance) method belongs to an individual object. We lean on static heavily now since we haven't built objects yet - that's all of Phase 5. For now, static is why main can run before any object exists.

💡 Key point. A good method does one thing and has a name that says what that thing is. Struggling to name it often signals it's doing too much - split it.

Method overloading - same name, different parameters

Sometimes you want one logical operation that works on different inputs. Java lets you give several methods the same name as long as their parameter lists differ - different types, or a different count. This is overloading.

📝 Overloading - defining multiple methods with the same name but distinct parameter lists. The compiler decides which one to call by looking at the types and number of arguments at the call site.

public class Printer {

    public static void show(int x) {
        System.out.println("int: " + x);
    }

    public static void show(String x) {
        System.out.println("String: " + x);
    }

    public static void show(int x, int y) {
        System.out.println("two ints: " + x + ", " + y);
    }

    public static void main(String[] args) {
        show(42);
        show("hello");
        show(1, 2);
    }
}
int: 42
String: hello
two ints: 1, 2

What just happened: All three methods are named show, but each takes a different parameter list. show(42) matched show(int x); show("hello") matched the String version; show(1, 2) matched the two-parameter one. One name, three behaviors, chosen by what you pass in - no need for showInt, showString, showTwoInts.

💡 Resolved at compile time. The compiler picks the overload by inspecting argument types while it compiles, baked in before your code runs - a real distinction from the next concept.

⚠️ Don't confuse overloading with overriding. They sound alike but are opposites. Overloading is same name, different parameters, picked at compile time. Overriding is when a subclass replaces an inherited method (same name, same parameters), decided at runtime by the actual object - needs inheritance, met in Phase 6.

Recap

  1. if / else branches on a boolean expression and runs the first matching block; build conditions with >, ==, &&, ||, !, and lean on short-circuiting for safe guards.
  2. switch matches one value against many cases. ⚠️ The classic statement falls through without break; prefer the modern switch expression (->), which returns a value and never falls through.
  3. Loops come in three shapes: for for counting, while for looping until a condition flips, and the enhanced for-each for walking a collection without an index.
  4. A method packages reusable logic with a return type, a name, parameters, and an access modifier; static methods belong to the class (no object needed), which is why main is static.
  5. Overloading gives one name several parameter lists, and the compiler picks the right one by argument types - distinct from overriding (a runtime, inheritance concept coming in Phase 6).

Next: we stop writing everything as static helpers and build the real thing Java is named for - classes and objects, your own types with their own data and behavior.

Quick check

Test yourself on the ideas most likely to bite - fall-through, loop choice, and overloading:

[
  {
    "q": "In a classic `switch` statement, what happens if a matching `case` has no `break`?",
    "choices": [
      "Execution falls through and runs the following case(s) until it hits a break or the end",
      "Java throws a compile error demanding a break",
      "Only that case runs, then the switch ends automatically",
      "The default case runs instead"
    ],
    "answer": 0,
    "explain": "Without `break`, execution falls through into the next case and keeps going. This is the most common switch bug, and exactly why the modern switch expression (with `->` arrows) never falls through."
  },
  {
    "q": "You want to visit every element of a `List<String>` and don't need the index. Which loop fits best?",
    "choices": [
      "The enhanced for-each: `for (String s : list)`",
      "A `while` loop with a manual counter",
      "A classic `for` loop with `i++`",
      "A `switch` statement"
    ],
    "answer": 0,
    "explain": "The for-each loop binds each element in turn with no index, no `i++`, and no off-by-one risk. Use a classic `for` only when you actually need the index."
  },
  {
    "q": "Given `show(int x)` and `show(String x)`, how does Java decide which `show` runs when you call `show(42)`?",
    "choices": [
      "The compiler picks the `int` version based on the argument type, at compile time",
      "The JVM picks one at random at runtime",
      "It always calls the first method declared",
      "It calls both versions in order"
    ],
    "answer": 0,
    "explain": "This is overloading: the compiler resolves which overload to call by inspecting the argument types while it compiles. `42` is an int, so `show(int x)` is chosen - the decision is made before the program runs."
  }
]

Before the quiz: without looking back, say (or jot down) the core idea of this phase in your own words.

Check your understanding 3 questions

1. In a classic `switch` statement, what happens if a matching `case` has no `break`?

2. You want to visit every element of a `List<String>` and don't need the index. Which loop fits best?

3. Given `show(int x)` and `show(String x)`, how does Java decide which `show` runs when you call `show(42)`?