Topic 31 of 119 4. Control Flow in Java Interactive Tutorial

Java Decision-Making Statements: Choosing Which Code Runs

9 mins read Last updated: 22 Aug 2026 20,363 views

Direct Answer & Core Concept

Java gives you four decision-making constructs, and choosing between them is mostly a question of what kind of condition you have. An `if` handles a single boolean test and needs no `else`. An `if-else` splits into two mutually exclusive paths. An `if-else-if` ladder handles a range of conditions in order, which is right for thresholds where each test is a comparison. A `switch` matches one value against a fixed set of cases, and the modern **switch expression** form returns a value directly instead of needing a `break`. Two rules catch newcomers out. First, a condition must be strictly `boolean`, so `if (1)` and `if (str)` are both compile errors, unlike C where any nonzero value is true. Second, `switch` on a `String` uses `equals` internally rather than `==`, so it behaves the way you expect. Beyond the syntax, block scoping is strict: a variable declared inside a branch is not visible outside it, and the compiler enforces definite assignment, so a variable assigned in only one branch must be assigned in all of them before use. For readable code, prefer guard clauses with early returns over deeply nested ladders, and prefer a `switch` expression over a statement whenever the result is a value.

Detailed Explanation & Mechanics

1. The Four Constructs at a Glance

Java gives you four decision-making constructs, and the choice between them comes down to what shape your condition has rather than to any preference about style.

ConstructShape of the problemProduces a value?
ifOne boolean testNo, it is a statement
if-elseTwo mutually exclusive pathsNo, it is a statement
if-else if ladderA range of thresholds, tested in orderNo, it is a statement
switch expressionOne value matched against a fixed set of casesYes, it is an expression

The distinction in the last column is the important one and it drives everything else. An if is a **statement**, so it performs an action and yields nothing. A switch used in expression form is an **expression**, so it produces a value you can assign, return, or pass along.

Main.java
Run Online
if (temperature > 30) {
    System.out.println("Hot");
    // performs an action
}String label = switch (temperature) {
    // produces a value
    default -> "thawing";
};

2. The Condition Must Be Strictly Boolean

This is the first thing that catches people arriving from C or C++, where if (1) is legal because any nonzero value counts as true. Java has no such rule.

Main.java
Run Online
if (1) {
} // error: incompatible types: int cannot be converted to boolean
if (str) {
} // error: incompatible types: String cannot be converted to boolean

The rule applies to every conditional construct, and it is a compile error rather than a runtime surprise, which is the kind way to learn it.

Main.java
Run Online
int count = items.size();
if (count) {
} // error
if (count > 0) {
} // correct, say what you mean

That last line is worth internalising as a style point too. if (count > 0) says something to the next reader even though if (count != 0) would compile.

The Boolean wrapper is the exception, and it is a trap rather than a convenience.

Main.java
Run Online
Boolean flag = null;
if (flag) {
} // throws NullPointerException on unboxing

Because a Boolean has to be unboxed before it can be tested, and unboxing null throws. Use a primitive boolean, or guard with flag != null && flag.

3. The Four Forms in Practice

The basic if needs nothing else, and is the right choice when there is genuinely only one thing to check.

Main.java
Run Online
if (temperature > 30) {
    System.out.println("Hot weather");
}

Adding else gives you two exclusive paths, and the two are guaranteed to be mutually exclusive because only one runs.

Main.java
Run Online
if (temperature > 30) {
    System.out.println("Hot");
} else {
    System.out.println("Not hot");
}

A ladder extends this to many conditions, and the crucial detail is that it is tested **in order**, so the first match wins and everything after it is skipped.

Main.java
Run Online
if (temperature > 30) {
    System.out.println("Hot");
} else if (temperature >= 20) {
    System.out.println("Pleasant");
} else {
    System.out.println("Cold");
}

With temperature of 28 the first test fails and the second matches, so the output is Pleasant and the else never runs. That ordering matters whenever the conditions overlap.

Main.java
Run Online
// order changed, and the answer changes with it
if (temperature >= 20) {
    System.out.println("Pleasant");
    // now this matches first
} else if (temperature > 30) {
    System.out.println("Hot");
    // now unreachable for 28
}

A switch matches a single value against a set of cases, which suits discrete choices rather than ranges.

Main.java
Run Online
switch (day) {
    case "MON": case "TUE": System.out.println("Week start");
    break;
    default: System.out.println("Midweek");
}

Note the stacked cases. In the traditional form, falling from one case into the next is **intentional** and is how you group several values into one branch.

4. The Dangling Else

An else always attaches to the **nearest** unmatched if, which matters only when you leave the braces off.

Main.java
Run Online
if (x == 0) if (x == 1) System.out.println("both");
else System.out.println("else binds to the inner if");

The else belongs to the inner if, not the outer one, because the inner if is the closest one without an else. When x is 0 the output is the second line, since the inner condition fails and its else runs.

With braces the structure is unambiguous and the nesting reads the way it actually works.

Main.java
Run Online
if (x == 0) {
    if (x == 1) {
        System.out.println("both");
    } else {
        System.out.println("inner else");
    }
}

The rule is simply to always use braces, even for a single statement. Modern style makes the braceless form rare for exactly this reason.

Main.java
Run Online
else attaches to the NEAREST if above it if (a) -> outer if, has no else of its own if (b) -> inner if, unmatched ...;
else -> therefore binds to the INNER if braces remove the question entirely, and cost one keystroke

5. Scoping Is Strict and Compiler-Enforced

A variable declared inside a branch belongs to that branch, and the compiler will not let you use it outside. This is stricter than the "leaky" scoping of old C.

Main.java
Run Online
if (temperature > 30) {
    int message = "Hot weather";
    System.out.println(message);
    // fine inside
}System.out.println(message);
// error: cannot find symbol

The declaration's scope is the block it appears in, so the same name can be reused in a different block without conflict.

Main.java
Run Online
if (temperature > 30) {
    int temp = 100;
    System.out.println(temp);
}if (temperature < 0) {
    int temp = -273;
    // a different variable, no clash
    System.out.println(temp);
}

That is convenient, but it also means a name reused in two branches can refer to two different values, so a repeated name is worth avoiding when the branches are related.

6. Definite Assignment

If you declare a variable without initialising it, the compiler requires proof that it has a value before you read it. Every path through the branches must assign it.

Main.java
Run Online
int result;
if (temperature > 30) {
    result = 1;
} else {
    result = 2;
}System.out.println(result);
// fine, both paths assign

Omit the else and the compiler rejects the read, because there is a path that leaves result unassigned.

Main.java
Run Online
int result;
if (temperature > 30) {
    result = 1;
}System.out.println(result);
// error: variable result might not have been initialized

This is a genuine safety net rather than an inconvenience. The error message names the exact problem, and it catches the classic bug where an unhandled branch leaves a variable at zero or null.

Main.java
Run Online
String name = null;
if (temperature > 30) {
    name = "Hot";
}System.out.println(name.length());
// rejected, not silently a NullPointerException later

Assigning a default up front is the other way to satisfy it, and is often clearer when zero or null is a sensible value.

Main.java
Run Online
int result = 0;
if (temperature > 30) {
    result = 1;
}

7. Guard Clauses Beat Nesting

A ladder with one return per branch is functionally identical to a ladder of if blocks, but it reads better because each condition sits at the same indentation and there is no nested block to track.

Main.java
Run Online
// nested
String grade;
if (score >= 0) {
    if (score < 50) {
        grade = "F";
    } else {
        grade = "pass";
    }
} else {
    grade = "invalid";
}// guard clauses
String grade;
if (score < 0) grade = "invalid";
else if (score < 50) grade = "F";
else grade = "pass";

The **early return** form is best of all when the function has nothing left to do after the decision, since it removes the variable entirely.

Main.java
Run Online
static String grade(int score) {
    if (score < 50) return "F";
    if (score < 70) return "C";
    if (score < 90) return "B";
    return "A";
}

The condition that should be checked first goes first, and each one is independent, so there is no nesting to reason about. This is the single most effective readability technique for conditionals.

8. The switch Expression

The traditional switch **statement** executes statements and needs a break to avoid falling into the next case.

Main.java
Run Online
int n = 2;
switch (n) {
    case 1: System.out.println("one");
    case 2: System.out.println("two");
    case 3: System.out.println("three");
    break;
    case 4: System.out.println("four");
}

That prints two **and** three, because there is no break after case 2 and execution falls through. This is legal, occasionally deliberate, and a very common source of bugs.

Main.java
Run Online
fall-through in the traditional form case 2 has no break -> prints two, then continues into case 3 deliberate use: case 1: case 2: case 3: share one body accidental use: forgot the break

The **arrow form** has no fall-through, so break is neither needed nor allowed, and it produces a value.

Main.java
Run Online
String s = switch (n) {
    case 1 -> "one";
    case 2 -> "two";
    default -> "many";
};
System.out.println(s);
// two

Several values can share a case with a comma, which removes the stacking that the traditional form needs.

Main.java
Run Online
String band = switch (n) {
    case 0, 1, 2 -> "low";
    default -> "high";
};

When a case needs more than one statement, use a block and yield to say what the value is.

Main.java
Run Online
int v = switch (n) {
    case 1 -> 10;
    case 2 -> {
        int doubled = 20;
        yield doubled * 2;
    } default -> 0;
};

A switch used as an expression must be **exhaustive**, or the compiler refuses it.

Main.java
Run Online
String r = switch (n) {
    case 1 -> "one";
    // no default
};
// error: the switch expression does not cover all possible input values

Adding default fixes it, and that requirement is a feature: it means the compiler has checked that you handled every case.

9. switch on String and Enum

A switch on a String compares using equals, not ==. This is the opposite of how strings behave with the equality operator, and it is the behaviour you would want.

Main.java
Run Online
String day = new String("MON");
switch (day) {
    case "MON": System.out.println("Monday");
    break;
    default: System.out.println("?");
}// prints Monday, even though day came from new String

If you had written this with if, you would need day.equals("MON") or Objects.equals, because day == "MON" would be false. The switch case label does the right thing for you.

Main.java
Run Online
// the equivalent if, note what it takes
if ("MON".equals(day)) {
    ...
}

A switch on an enum is the clearest case of all, and an enum without a matching case simply falls to default.

Main.java
Run Online
Thread.State state = Thread.State.NEW;
switch (state) {
    case NEW: System.out.println("new thread");
    break;
    case RUNNABLE: System.out.println("running");
    break;
    default: System.out.println("other");
}

The selector of a switch must be one of a small set of types. Exactly these compile.

Selector typeAllowed
byte, short, char, intYes, all integer types except long
StringYes, compared with equals
An enum typeYes, the clearest use of all
longNo
float or doubleNo
An arbitrary objectNo, unless it is a sealed type with pattern matching

The gap between the integer types is the one that bites, since int is allowed but long is not and the two look like a pair.

Main.java
Run Online
switch (someInt) {
} // fine
switch (someLong) {
} // rejected, long is not a valid selector

If you need to switch on a long, an if-else ladder or a lookup map is the honest answer.

10. Pattern Matching for instanceof

The traditional way to test a type and then use it required two steps, because the compiler would not let you cast inside the if.

Main.java
Run Online
Object o = "hello";
if (o instanceof String) {
    String s =(String) o;
    // an explicit cast that could fail
    System.out.println(s.length());
}

Pattern matching, final since Java 16, merges the test and the binding into one step and makes the cast unnecessary.

Main.java
Run Online
if (o instanceof String s) {
    System.out.println(s.length());
    // s is already a String
}

The variable s is in scope **only** where the test succeeded, which is exactly the safety the two-step version was missing.

Main.java
Run Online
Object p = 42;
if (p instanceof String s) {
    System.out.println("unreachable");
} else {
    System.out.println("s is not in scope here");
}

This pairs naturally with && for a combined type and content test, which is where the null-safe comparison idiom from the ternary topic comes from.

Main.java
Run Online
if (name instanceof String s && s.length() > 3) {
    System.out.println("a string of some length: " + s);
}

Note that pattern matching in switch is a **separate and newer feature**. It is standard from Java 21, but on Java 17 it is still a preview feature, so code using case Integer i -> will not compile on 17 without --enable-preview.

Main.java
Run Online
// Java 21 and later
String describe(Object o) {
    return switch (o) {
        case Integer i when i > 100 -> "big int";
        case Integer i -> "int";
        case String s -> "string";
        default -> "something else";
    };
}// on Java 17 the same code reports
// patterns in switch statements are a preview feature and are disabled by default

11. Choosing Between Them

A short decision guide, based on the shape of the condition rather than on style.

Your situationUseWhy
One boolean testifNothing else is needed
Two pathsif-elseThe direct form
Ranges or thresholdsif-else if ladderEach test is a comparison
Many discrete valuesswitchOne test instead of a chain
The result is a valueswitch expressionNo temporary variable, no break
Early exit is possibleGuard clausesRemoves nesting entirely
Nesting beyond three levelsExtract a methodThe nesting is hiding something
A value of a fixed enum setswitch on the enumExhaustive and clear
Type plus content testPattern matchingNo cast, safe binding

The most common mistake is reaching for a switch when the conditions are ranges, or for a ladder when the values are discrete.

Main.java
Run Online
// a switch cannot express a range, this belongs in an if
if (score >= 90) {
    ...
}// a ladder of exact values is exactly what switch is for
switch (status) {
    case 1 -> ...;
    case 2 -> ...;
}

12. Pitfalls Worth Knowing

Each of these is a real failure mode rather than a style opinion.

PitfallWhat happens
if (1) or if (str)Compile error, Java requires boolean
Boolean wrapper in a conditionNullPointerException on unboxing
Missing braces with an elseThe else binds to the inner if
Missing break in a switch statementFalls through to the next case
Using a branch variable outside itCompile error, cannot find symbol
Reading a variable assigned in one branch onlyCompile error, might not have been initialized
Ladder order not matching the dataA broad condition shadows the narrow ones
Non-exhaustive switch expressionCompile error, add default
Pattern switch on Java 17Compile error, still a preview feature

The ladder ordering bug is the subtlest of these, since it compiles and runs and simply produces the wrong answer.

Main.java
Run Online
// wrong order, the broad test shadows the narrow one
if (score > 50) System.out.println("passed");
else if (score > 90) System.out.println("excellent");
// a score of 95 prints passed, never excellent

13. Cheat Sheet

QuestionAnswer
Decision constructsif, if-else, ladder, switch
Condition typeMust be boolean
if (1)Compile error, int cannot be converted to boolean
if (str)Compile error, String cannot be converted to boolean
Boolean null in a conditionNullPointerException on unboxing
Ladder evaluationIn order, first match wins, rest skipped
else bindingAlways the nearest unmatched if
Branch variable scopeThe enclosing block only
Definite assignmentEvery path must assign before use
Guard clausesPrefer to nesting, use early return
Switch statement fall-throughYes, without break
Switch arrow formNo fall-through, no break allowed
Switch expressionYields a value, must be exhaustive
Exhaustive switch expressionNeeds default or all cases
Multiple case valuescase 1, 2, 3 ->
Multi-statement caseBlock plus yield
Switch on StringUses equals, not ==
Switch on long or floatNot allowed
instanceof patternStandard since Java 16
Pattern switchStandard in Java 21, preview on 17
Switch rangesNot supported, use an if

The one-line version: reach for if when you are testing a condition, for a ladder when you are testing ordered ranges, and for a switch expression when you are matching one value against a set and want a result back. Guard clauses are what keep all three readable, because they keep the nesting flat and let each condition say one thing.

Runnable Code Example: Java Decision-Making Statements

Java 17+
Main.java
Run Online
public class DecisionMakingDemo {
    public static void main(String[] args) {
        //
        1. a single boolean test, no else needed int temperature = 28;
        if (temperature > 30) {
            System.out.println("Hot weather");
        } //
        2. two mutually exclusive paths if (temperature > 30) {
            System.out.println("Hot");
        } else {
            System.out.println("Not hot");
        } //
        3. a ladder, tested in order, for thresholds if (temperature > 30) {
            System.out.println("Hot");
        } else if (temperature >= 20) {
            System.out.println("Pleasant");
        } else {
            System.out.println("Cold");
        } //
        4. a switch expression, which yields a value instead of falling through String description = switch (temperature) {
            case 0, 1, 2 -> "freezing";
            default -> "thawing";
        };
        System.out.println("Switch says: " + description);
        // guard clauses with early return read better than nesting
        System.out.println(grade(95));
        System.out.println(grade(85));
        System.out.println(grade(10));
    } // early returns instead of a four level ladder
    static String grade(int score) {
        if (score < 50) return "F";
        if (score < 70) return "C";
        if (score < 90) return "B";
        return "A";
    }
}

Common Java Technical Interview Questions & Answers

Interview Prep
Q1

Can you use an integer inside an if condition in Java, for example if(1)?

No. The compiler rejects it with incompatible types: int cannot be converted to boolean. Java has no truthy or falsy values, unlike C and C++ where any nonzero integer counts as true. The same applies to a String, which is rejected the same way. Write if (count > 0) rather than relying on a nonzero test, since the explicit form also says what you mean. The one exception is the Boolean wrapper, which is accepted but throws a NullPointerException when it is null, because it has to be unboxed before it can be tested.
Q2

What is the main advantage of decision-making structures in software?

They let a program behave differently at run time based on data it did not know at compile time, which is the whole basis of conditional logic. Every validation rule, permission check, retry policy and state machine in an application is built from them. More usefully, they let you express the decision in one place, close to the condition, rather than scattering checks through the code. The practical benefit is that the decision becomes readable and testable, since a condition either holds or does not and you can assert on it directly.
Q3

Which decision construct should I use?

Choose by the shape of your condition rather than by preference. A single boolean test is an if. Two paths is if-else. Ordered ranges or thresholds are an if-else-if ladder, tested in order so the first match wins. Many discrete values against a fixed set is a switch. If the outcome is a value rather than an action, use a switch expression so you avoid a temporary variable and the break statements. Whichever you pick, if you can exit early, prefer guard clauses with returns, since they keep every condition at the same indentation.
Q4

Why does an else bind to the inner if when I leave out the braces?

Because an else always attaches to the nearest if above it that does not already have an else. An outer if with no else of its own is skipped, so the else lands on the inner one. This is only ambiguous when you leave the braces off, so the rule is simply to always use braces even around a single statement. The cost is one keystroke, and it also stops a later editor inserting a second statement into a branch that was meant to hold one.
Q5

Why does a switch expression need a default case?

Because a switch used as an expression must be exhaustive, and the compiler enforces it. If you leave out a case and omit default, the compiler reports that the switch expression does not cover all possible input values. This is a safety feature rather than an inconvenience, because it means the compiler has verified that every case was considered. The same requirement applies to arrow and colon forms alike, and it is the main reason a switch expression catches a newly added enum constant at compile time instead of at run time.
Q6

What is the difference between fall-through and the arrow form in a switch?

In the traditional colon form, a case without a break falls into the next case and keeps executing. That is occasionally deliberate, for grouping several values into one branch by stacking cases, and frequently accidental, which is why missing breaks are a classic bug. The arrow form has no fall-through at all, so each case ends where it ends, and break is neither needed nor allowed. Since the arrow form also yields a value, it is the better default for new code unless you specifically want stacked cases.
Q7

Does a switch on a String use == or equals?

It uses equals, not ==, which is the behaviour you would want and the opposite of what the equality operator does on strings. So switch (day) with a case of "MON" matches a String built with new String("MON"), which would fail if you had written the equivalent if with day == "MON". Writing that if by hand means using "MON".equals(day) or Objects.equals, which is the change to make if you prefer a switch to an if.
Q8

Why can I not read a variable I assigned inside only one branch?

Because Java enforces definite assignment. A local variable declared without an initialiser must be provably assigned on every path before it is read, and the compiler reports that the variable might not have been initialized. Adding an else that assigns it, or giving it a default value at the declaration, satisfies the rule. This is a genuine safety net, since it catches the bug where an unhandled branch silently leaves a variable at zero or null and the failure surfaces much later.
Q9

Are variables declared inside an if block visible afterwards?

No. A variable declared inside a block belongs to that block, and using it outside is a compile error reporting that the symbol cannot be found. This is stricter than the leaky scoping of older C. The upside is that you can reuse the same name in a different branch without a clash, though for branches that are related it is better to use distinct names so the reader is not left wondering whether they are the same variable.
Q10

What is pattern matching for instanceof and when can I use it?

It is final from Java 16. Instead of testing the type and then casting, you bind the value in the test itself with if (o instanceof String s), and s is a String with no cast needed. The variable is in scope only where the test succeeded, which removes the redundancy and the risk of the two steps disagreeing. It combines well with && for a type plus content test. Note that pattern matching in switch is a different and newer feature: standard from Java 21, but still a preview feature on Java 17, so that code will not compile on 17 without enabling preview.
Q11

Which types can a switch selector be?

The four integer types byte, short, char and int, plus String and an enum type. The gap that surprises people is long, which is rejected even though int is allowed, and float and double are rejected as well, as is an arbitrary object unless it is a sealed type handled with pattern matching. If you need to switch on a long or a floating point value, an if-else ladder or a lookup map is the right tool.
Q12

Why does my ladder produce the wrong branch for some values?

Almost always because the tests are in the wrong order for the data. A ladder returns on the first match, so a broad condition placed above a narrow one shadows it. If you test score > 50 before score > 90, a score of 95 prints the broad result and never reaches the narrow branch. Order the tests from most specific to least specific, or from highest threshold to lowest, and the ladder behaves. This is the most common logical bug in conditionals because it compiles, runs, and silently gives the wrong answer.

Key Takeaways, Best Practices & Clean Code Rules

Java Best Practice & Production Rules

Choose the construct that matches the shape of your condition rather than the one you have seen most. A single boolean test is an if, two paths is an if-else, ordered ranges are an if-else-if ladder, and many discrete values against a fixed set are a switch, where the arrow form is the better default because it has no fall-through and yields a value. Whenever a method can exit early, prefer guard clauses with returns over nesting, since each condition then sits at the same indentation and there is no nested block for the reader to track. Always use braces around branch bodies, because a braceless else silently attaches to the nearest if and the structure stops matching what you meant. Order a ladder from most specific to least specific, since a ladder returns on the first match and a broad condition placed above a narrow one shadows it, which is the most common logical bug in conditional code because it compiles and silently answers wrongly. Prefer a switch expression over a switch statement whenever the outcome is a value, and let the compiler enforce exhaustiveness by always including a default, since that requirement is what turns a newly added enum constant into a compile error instead of a production surprise. Use instanceof pattern matching where you would otherwise cast after a type test, since it binds the value in the condition and keeps it out of scope where the test failed. Keep conditions as boolean comparisons rather than relying on a numeric or object truthiness, because Java has no such notion and if (count) is a compile error while if (count > 0) also documents the intent. Declare variables where they are used rather than at the top of the method, so the reader is not left wondering whether a zero or null is meaningful, and if a variable must be declared outside a branch, give it a sensible default rather than relying on every path assigning it.
We value your privacy & transparency

OperateTools uses cookies and local storage to ensure basic tool functionality, preserve preferences, and analyze aggregated traffic. We never sell your personal data or index your document files. Learn more in our Privacy Policy.