Free degree-level programming lessons for careful independent study.

Degree Level Programmes · Programming 1 · Lesson 12

If Statements, Determinism and Safer Control Flow

Use if statements deliberately by understanding control-flow traces, state explosion, guard clauses and clearer alternatives.

Lesson Overview

Use if statements deliberately by understanding control-flow traces, state explosion, guard clauses and clearer alternatives.

Portfolio focus: Count the branches in a small function and list its possible traces.

ConceptSafer branching
Run fileIfStatementsDeterminismDemo.java
BaselineModeration allowed for Ada Switch result: 5.0 Dispatch result: 7.0 list items Saved request Java allows boolean assignment in a condition: ready is now true
Evidence3 tasks

Starter: think before typing

Before running this safer branching example, find the line where the main idea becomes active. Write a prediction: what must already be true for that line to work, and what should be different after it runs? The checked run ends with `Moderation allowed for Ada Switch result: 5.0 Dispatch result: 7.0 list items Saved request Java allows boolean assignment in a condition: ready is now true`; predict how the focus line helps produce that evidence.

Learning Objectives

  • Explain how an if statement creates alternative traces.
  • Estimate why nested branches become hard to test.
  • Use named predicates and guard clauses to flatten control flow.
  • Choose switch, lookup tables or polymorphism when they make branching clearer.

Learning Outcomes

  • By the end of the lesson, you can explain how an if statement creates alternative traces.
  • By the end of the lesson, you can estimate why nested branches become hard to test.
  • By the end of the lesson, you can use named predicates and guard clauses to flatten control flow.
  • By the end of the lesson, you can choose switch, lookup tables or polymorphism when they make branching clearer.

Why this idea exists

Conditional flow is implemented as jumps through execution. A loop is a predictable jump around a repeated structure; an `if` is an execution-time decision point where the program may split into different traces.

The danger is not the keyword itself. The danger is path multiplication: every extra condition can double the number of routes through the code, and later state changes can make those routes hard to test or reason about.

Good programmers therefore treat `if` as a design choice, not a reflex. Prefer deterministic loops with clear bounds, named predicates, guard clauses, switch expressions, lookup tables or polymorphism when they make the flow clearer.

Deep dive

What an if does and why it is risky

An if evaluates a boolean condition and jumps accordingly. That sounds simple, but each branch creates a different trace through the program. Two independent if statements can create four traces; three can create eight. Once those traces mutate state, testing and reasoning become much harder.

Golden rules for safer branching

Use if only when the program genuinely needs an execution-time decision. Avoid deep nesting. Name complicated predicates before using them. Prefer guard clauses for invalid cases so the normal path remains visible. If a function contains many branches, ask whether the design is missing a better abstraction.

The classic assignment trap

Java prevents the C/C++ integer version of if (x = 0), because an int cannot be used as a boolean. But Java still allows boolean assignment in a condition, such as if (ready = true). That is valid Java, changes state, and makes the branch misleading. Tool warnings, code review and clear predicate names all help.

Alternatives to long if-else chains

When the decision is about a fixed set of states, a switch expression may be clearer. When the decision maps a key to behaviour, a table or map can reduce branch noise. When behaviour varies by type or role, polymorphism can move the choice to the object design. None of these is automatically better; the question is whether the trace becomes easier to explain.

Switch over a closed set

If the possible operations are known in advance, model them as a closed set and switch over that set. In Java, an enum plus a switch expression makes the available choices explicit. The code still branches, but the branch shape is visible, compact and easier to test than a long chain of unrelated conditions.

Table-driven dispatch

A dispatch table turns a command or symbol into a function. This can reduce repeated branching when the program is really asking the same question over and over: which operation belongs to this key? The table must still be designed carefully, because hiding behaviour in a map can make debugging harder if names and tests are weak.

Early returns and guard clauses

Deeply nested if statements make readers hold several conditions in memory at once. Guard clauses reverse the shape: reject invalid cases early, then let the normal path run at the left margin. This is not just prettier code; it reduces indentation, clarifies failure reasons and makes each branch easier to test.

Side effects and short-circuiting

`&&` and `||` short-circuit. If the left side already decides the answer, the right side may not run. That is useful for safe checks, but dangerous when the right-hand side performs an effect you rely on later. Separate effects from tests when the order matters.

Type-level choices versus execution traces

C++ can remove some execution branches with templates or `if constexpr`. Java has different mechanisms, such as generics, overloads, sealed types and JIT optimisation, but the design lesson still transfers: if a decision is known by structure or type, avoid repeatedly asking it during execution unless that improves clarity.

Worked refactor: messy ifs to clear logic

Start by fixing accidental assignment in a condition. Then extract named predicates, separate side effects such as initialisation from boolean tests, and convert nested checks into guard clauses. The refactor is successful only if each failure path is easier to test and the normal path is easier to read.

Mini exercise: eliminate the ladder

Take a command ladder such as add/delete/list/unknown. First rewrite it as a switch expression. Then try a dispatch map from command text to behaviour. Compare the two versions: which is easier to extend, which is easier to debug, and which would a new programmer understand fastest?

From ladder to table

A command ladder asks the same question several times: is the command add, is it delete, is it list? If the command set is closed and small, an enum plus a switch is explicit. If command text maps cleanly to behaviour, use a dispatch map such as Map<String, Runnable> in Java. In C++, the same idea often appears as std::unordered_map<std::string, std::function<void()>>.

Programming insight

Paste an if-else command ladder into an AI assistant and ask it to produce both an enum plus switch version and a dispatch-table version. Then ask it to compare readability, debuggability and test impact. Use the result as a review prompt, not as a replacement for your own explanation.

Summary checklist

Minimise branches. Flatten logic with guard clauses. Name predicates. Never use assignment where you mean comparison. Keep conditions pure. Dispatch rather than branch when commands map to behaviours. Move decisions to type or structure when that genuinely makes the program easier to reason about.

Optional walkthrough video

When Graham's video walkthrough for if statements is available, embed it here beside the control-flow graph discussion. Until then, use this slot as a teaching prompt: explain the trace count, identify side effects, and justify either switch or table dispatch.

Lesson visual

A control-flow graph with one highlighted if branch, trace paths, guard clauses and a warning about path explosion.
A control-flow graph with one highlighted if branch, trace paths, guard clauses and a warning about path explosion.Download visual

Type this and run it

Create IfStatementsDeterminismDemo.java, type the program, and run it before changing anything. This section is about reproducing the checked baseline.

public class IfStatementsDeterminismDemo {
  enum Op { ADD, SUB, MUL, DIV }

  public static void main(String[] args) {
    User user = new User("Ada", true, false, "ADMIN");
    moderateIfAllowed(true, user);
    System.out.println("Switch result: " + apply(Op.DIV, 10, 2));
    System.out.println("Dispatch result: " + dispatch('+', 3, 4));
    runCommand("list");
    handle(new Request(true, true, false));
    demonstrateAssignmentTrap();
  }

  static void moderateIfAllowed(boolean isOpen, User user) {
    boolean canModerate = user.role().equals("ADMIN") || (user.active() && !user.banned());
    if (!isOpen) {
      System.out.println("Closed: no moderation trace.");
      return;
    }
    if (!canModerate) {
      System.out.println("Rejected: user cannot moderate.");
      return;
    }
    System.out.println("Moderation allowed for " + user.name());
  }

  static double apply(Op op, double a, double b) {
    return switch (op) {
      case ADD -> a + b;
      case SUB -> a - b;
      case MUL -> a * b;
      case DIV -> b != 0 ? a / b : 0;
    };
  }

  static double dispatch(char op, double a, double b) {
    java.util.Map<Character, Operation> ops = java.util.Map.of(
      '+', (left, right) -> left + right,
      '-', (left, right) -> left - right
    );
    Operation operation = ops.get(op);
    return operation == null ? 0 : operation.apply(a, b);
  }

  static void runCommand(String command) {
    java.util.Map<String, Runnable> commands = java.util.Map.of(
      "add", () -> System.out.println("add item"),
      "del", () -> System.out.println("delete item"),
      "list", () -> System.out.println("list items")
    );
    Runnable action = commands.getOrDefault(command, () -> System.out.println("Unknown command"));
    action.run();
  }

  static boolean handle(Request request) {
    if (!request.valid()) return false;
    if (!request.hasAuth()) return false;
    if (request.locked()) return false;
    return save(request);
  }

  static boolean save(Request request) {
    System.out.println("Saved request");
    return true;
  }

  static void demonstrateAssignmentTrap() {
    boolean ready = false;
    if (ready = true) {
      System.out.println("Java allows boolean assignment in a condition: ready is now " + ready);
    }
  }
}

interface Operation {
  double apply(double a, double b);
}

record User(String name, boolean active, boolean banned, String role) { }

record Request(boolean valid, boolean hasAuth, boolean locked) { }

Build and run it with:

javac IfStatementsDeterminismDemo.java && java IfStatementsDeterminismDemo

Expected baseline: Moderation allowed for Ada Switch result: 5.0 Dispatch result: 7.0 list items Saved request Java allows boolean assignment in a condition: ready is now true

Run the code in your browser

Use the editor as an experiment surface. First run the checked version, then make one small change to the part of the program that demonstrates safer branching and compare the new behaviour with the reference output.

Line-by-line explanation

Read the code as a sequence of responsibilities. The focus line for this lesson is enum Op { ADD, SUB, MUL, DIV }; the surrounding lines prepare it, use its result or make the behaviour observable.

public class IfStatementsDeterminismDemo {

This names the runnable class for the If Statements, Determinism and Safer Control Flow example, giving the compiler and JVM one clear unit to build.

enum Op { ADD, SUB, MUL, DIV }

This defines a closed set of named choices, which helps If Statements, Determinism and Safer Control Flow avoid mistyped or open-ended branch values.

public static void main(String[] args) {

This is the program entry point. In If Statements, Determinism and Safer Control Flow, it keeps the demonstration of safer branching in one traceable starting script.

User user = new User("Ada", true, false, "ADMIN");

This introduces user as named state for If Statements, Determinism and Safer Control Flow. Later lines can read, update, pass or print that specific value as evidence.

moderateIfAllowed(true, user);

This calls moderateIfAllowed with true, user in If Statements, Determinism and Safer Control Flow. Look for the method definition to see what work actually happens.

System.out.println("Switch result: " + apply(Op.DIV, 10, 2));

This prints "Switch result: " + apply(Op.DIV, 10, 2) as the observable evidence for If Statements, Determinism and Safer Control Flow. The output lets the learner check whether the safer branching idea behaved as predicted.

System.out.println("Dispatch result: " + dispatch('+', 3, 4));

This prints "Dispatch result: " + dispatch('+', 3, 4) as the observable evidence for If Statements, Determinism and Safer Control Flow. The output lets the learner check whether the safer branching idea behaved as predicted.

runCommand("list");

This calls runCommand with "list" in If Statements, Determinism and Safer Control Flow. Look for the method definition to see what work actually happens.

handle(new Request(true, true, false));

This constructs an object for the If Statements, Determinism and Safer Control Flow example, asking Java for a value with a specific type and behaviour.

demonstrateAssignmentTrap();

This calls demonstrateAssignmentTrap with the current arguments in If Statements, Determinism and Safer Control Flow. Look for the method definition to see what work actually happens.

}

This closes the innermost If Statements, Determinism and Safer Control Flow block, so the immediately preceding method, branch or loop has finished.

static void moderateIfAllowed(boolean isOpen, User user) {

This starts moderateIfAllowed, a named If Statements, Determinism and Safer Control Flow operation. Its parameters describe what information comes in; its body decides what work is done.

boolean canModerate = user.role().equals("ADMIN") || (user.active() && !user.banned());

This introduces canModerate as named state for If Statements, Determinism and Safer Control Flow. Later lines can read, update, pass or print that specific value as evidence.

if (!isOpen) {

This makes the If Statements, Determinism and Safer Control Flow decision point. Trace the condition first, then trace only the branch that can actually run.

System.out.println("Closed: no moderation trace.");

This prints "Closed: no moderation trace." as the observable evidence for If Statements, Determinism and Safer Control Flow. The output lets the learner check whether the safer branching idea behaved as predicted.

return;

This sends a If Statements, Determinism and Safer Control Flow result back to the caller, so the surrounding code can use the answer.

Worked example

From code to explanation

Problem: Use If Statements, Determinism and Safer Control Flow to complete a small portfolio-quality step: Count the branches in a small function and list its possible traces.

Method: Locate the line `enum Op { ADD, SUB, MUL, DIV }`, explain the exact role it plays, then decide what you would change to extend the example without changing the whole program.

Reveal worked answer

The checked run should produce `Moderation allowed for Ada Switch result: 5.0 Dispatch result: 7.0 list items Saved request Java allows boolean assignment in a condition: ready is now true`. A strong answer links the result back to safer branching: what was created, selected, stored, called or protected, and why that matters for the portfolio task.

Trace the program

Before: Before the key operation, identify the relevant value, object, branch or resource that the safer branching concept depends on.

During: Trace `enum Op { ADD, SUB, MUL, DIV }` as the If Statements, Determinism and Safer Control Flow example executes. Say whether that operation creates data, checks a condition, calls behaviour, stores information or crosses a boundary.

After: Compare the run with the expected evidence: `Moderation allowed for Ada Switch result: 5.0 Dispatch result: 7.0 list items Saved request Java allows boolean assignment in a condition: ready is now true`.

Change: Now rewrite a nested if using named predicates and guard clauses, run again, and explain the smallest reason the behaviour changed.

Common misconception

A common mistake in if statements, determinism and safer control flow is treating the example as a finished answer. For safer branching, the important question is narrower: which operation carries the idea, what does it make possible, and what would break if you changed it carelessly?

Quick checks

1. In this If Statements, Determinism and Safer Control Flow example, what is the best reason to focus on `enum Op { ADD, SUB, MUL, DIV }`?

2. Which evidence is strongest after you edit and rerun this example?

How to study this lesson

For If Statements, Determinism and Safer Control Flow, predict how safer branching changes the run before you press Run.

Use the first portfolio task as your main edit: Count the branches in a small function and list its possible traces.

Use the second task as your variation: Rewrite a nested if using named predicates and guard clauses.

Finish with evidence, not a diary entry: Replace an add/delete/list if-else command ladder with a switch, then with a dispatch map, and compare maintainability.

Portfolio Practice

  1. Count the branches in a small function and list its possible traces.
  2. Rewrite a nested if using named predicates and guard clauses.
  3. Replace an add/delete/list if-else command ladder with a switch, then with a dispatch map, and compare maintainability.

Final self-check

Can you explain the key operation?

Explain the line identified in the quick check in one or two sentences. Your answer should say what it does before the output Moderation allowed for Ada Switch result: 5.0 Dispatch result: 7.0 list items Saved request Java allows boolean assignment in a condition: ready is now true appears.

Can you justify the portfolio evidence?

Your evidence should include the original run, one edited run, and a short note explaining how the edit affected safer branching.

Study route

Practise safer branching by predicting the Java example, typing it, running it in the browser, tracing the result and saving portfolio evidence.

Next, move into Methods and Decomposition and carry forward one improvement from this lesson into the next program.