Command Palette

Search for a command to run...

[Java Basics] Conditional Statements in Java: if, else if, else and switch

Every program in this series so far has run straight through from top to bottom. A conditional statement is the point where that stops being true: the program looks at a value and takes one path instead of another. Java gives you two constructs for it, if and switch, plus the ternary operator when a decision fits inside an expression.

Neither construct is complicated, and both carry traps that survive into professional code — an else that binds to the wrong if, a missing break that quietly gives every gold customer the silver discount, an arrow-form switch that refuses to compile because it is not exhaustive. Every program, every compiler message and every stack trace below was compiled and run on OpenJDK 21.0.6.

Conditional statements in Java: if, else if, else and switch

Start with if, because switch is a specialised version of the same idea.

if, else and the else-if chain

The smallest form is one condition and one block. The condition goes in parentheses, the body in braces:

int age = 20;

if (age >= 18) {
    System.out.println("adult");
}

Add else for the other path, and else if for a third, fourth and fifth:

int score = 72;

if (score >= 90) {
    System.out.println("grade A");
} else if (score >= 80) {
    System.out.println("grade B");
} else if (score >= 70) {
    System.out.println("grade C");
} else {
    System.out.println("grade D or worse");
}

There is no elseif keyword in Java. else if is literally an else whose body is another if statement; the chain you see above is a stack of nested if statements that convention lets you write on one indentation level.

The chain stops at the first true condition

That nesting has a consequence worth proving rather than asserting: the conditions are evaluated top to bottom, the first one that is true runs its branch, and every condition below it is never evaluated at all. Here is a version that prints a line whenever a condition is actually asked:

public class Chain {
    static boolean check(String label, boolean result) {
        System.out.println("  evaluating " + label);
        return result;
    }

    public static void main(String[] args) {
        int score = 72;
        System.out.println("score = " + score);

        if (check("score >= 90", score >= 90)) {
            System.out.println("grade A");
        } else if (check("score >= 80", score >= 80)) {
            System.out.println("grade B");
        } else if (check("score >= 70", score >= 70)) {
            System.out.println("grade C");
        } else if (check("score >= 60", score >= 60)) {
            System.out.println("grade D");
        } else {
            System.out.println("grade F");
        }
    }
}
score = 72
  evaluating score >= 90
  evaluating score >= 80
  evaluating score >= 70
grade C

Three conditions were evaluated, not four. score >= 60 was never asked, and neither was the else.

An else-if chain is evaluated top to bottom and stops at the first true condition

This matters for more than speed. If a condition calls a method that logs, mutates state or hits a database, putting it late in a chain means it may never run. Conditions in an if chain should be side-effect free for exactly that reason.

Separate ifs are not a chain

Delete the else keywords and you no longer have a chain — you have four independent statements, all of which are evaluated:

int score = 95;

if (score >= 90) System.out.println("grade A");
if (score >= 80) System.out.println("grade B");
if (score >= 70) System.out.println("grade C");
if (score >= 60) System.out.println("grade D");
grade A
grade B
grade C
grade D

Four lines instead of one. When branches are meant to be mutually exclusive, they need else.

The order of the conditions is part of the logic

Because the first match wins, an overlapping chain written in the wrong order collapses to its first branch:

int score = 95;

if (score >= 60) {
    System.out.println("grade D");
} else if (score >= 70) {
    System.out.println("grade C");
} else if (score >= 80) {
    System.out.println("grade B");
} else if (score >= 90) {
    System.out.println("grade A");
}
grade D

95 >= 60 is true, so nothing else is ever consulted. The compiler has no opinion about this — the branches below are reachable in principle, just not for any value that reaches the first one. With overlapping ranges, order from the narrowest condition to the widest.

Braces are optional, and that is where the bugs are

Java lets you drop the braces when the branch body is a single statement:

if (age >= 18)
    System.out.println("adult");

That is legal, and it is the source of a whole family of bugs, because the compiler decides what "the body" is from the grammar and the reader decides from the indentation. When they disagree, the compiler wins:

public class Braces {
    static void openConnection() { System.out.println("openConnection()"); }
    static void sendPayload()    { System.out.println("sendPayload()"); }

    public static void main(String[] args) {
        boolean connected = false;

        if (connected)
            openConnection();
            sendPayload();

        System.out.println("main finished");
    }
}
sendPayload()
main finished

connected is false, and the payload was sent anyway. The if owns exactly one statement — openConnection(); — and sendPayload(); is simply the next statement in main. Plain javac says nothing, and neither does javac -Xlint:all.

The goto fail shape

Apple's 2014 "goto fail" bug in SSL certificate validation had this exact shape: a duplicated line under a brace-less if, indented as though it belonged to the branch, running unconditionally instead. The consequence was that the remaining checks were skipped and invalid signatures were reported as valid.

Java catches one half of that pattern. Transcribe it literally and the duplicated return makes the code after it dead:

static int verify() {
    int err;
    if ((err = hash("serverRandom")) != 0)
        return err;
    if ((err = hash("signedParams")) != 0)
        return err;
        return err;
    err = rawVerify();
    return err;
}
GotoFailA.java:19: error: unreachable statement
        err = rawVerify();
        ^
1 error

Java's definite-unreachability rule refuses to compile a statement that can never run. That is a real safety net — and it only fires when the stray line makes something else unreachable. Change the duplicated statement to an assignment and the same misindentation compiles silently:

public class GotoFailB {
    static boolean isAdmin(String user) { return user.equals("root"); }

    static boolean allowed(String user, String action) {
        boolean granted = false;
        if (isAdmin(user))
            granted = true;
        if (action.equals("read"))
            granted = true;
            granted = true;
        return granted;
    }

    public static void main(String[] args) {
        System.out.println("root  / read   -> " + allowed("root", "read"));
        System.out.println("guest / read   -> " + allowed("guest", "read"));
        System.out.println("guest / delete -> " + allowed("guest", "delete"));
    }
}
root  / read   -> true
guest / read   -> true
guest / delete -> true

Everything is permitted. javac -Xlint:all reports nothing, because there is no dead code and no type error — only a statement that is not where its indentation says it is.

⚠️ Use braces on every branch, including one-liners. It costs two characters and removes the entire class of bug above, plus the one in the next section.

The dangling else: which if does an else belong to?

Nest two brace-less if statements and add an else, and there are two readings. Java's rule: an else binds to the nearest preceding if that does not already have one — regardless of how you indented it.

public class Dangling {
    static void noBraces(int x, int y) {
        System.out.println("noBraces(x=" + x + ", y=" + y + ")");
        if (x > 10)
            if (y > 10)
                System.out.println("   both are big");
        else
            System.out.println("   x is not big");
    }

    static void withBraces(int x, int y) {
        System.out.println("withBraces(x=" + x + ", y=" + y + ")");
        if (x > 10) {
            if (y > 10)
                System.out.println("   both are big");
        } else {
            System.out.println("   x is not big");
        }
    }

    public static void main(String[] args) {
        noBraces(5, 20);
        withBraces(5, 20);
        noBraces(20, 5);
        withBraces(20, 5);
    }
}
noBraces(x=5, y=20)
withBraces(x=5, y=20)
   x is not big
noBraces(x=20, y=5)
   x is not big
withBraces(x=20, y=5)

The two methods are opposites. With x = 5, the brace-less version prints nothing while the braced version prints the message; with x = 20, y = 5 it is the other way round.

The dangling else binds to the nearest unmatched if, not to the one you lined it up with

In noBraces, the else belongs to if (y > 10), so the whole thing is one statement guarded by if (x > 10). When x is 5, that guard is false and neither branch runs. The indentation says otherwise and the indentation is wrong.

Braces make the binding explicit, which is why every serious style guide requires them here.

The condition must be a boolean, and nothing else

In C, if (x = 5) compiles: the assignment produces 5, and any non-zero value is truthy. Java has no truthiness. The condition of an if must be boolean or Boolean, so the classic typo is a compile error rather than a silent bug:

int x = 0;
if (x = 5) {
    System.out.println("x is five");
}
NotBoolean.java:4: error: incompatible types: int cannot be converted to boolean
        if (x = 5) {
              ^
1 error

The same applies to any non-boolean value, including literals and references:

if (1) { }
TruthyInt.java:3: error: incompatible types: int cannot be converted to boolean
        if (1) {
            ^
1 error
String name = "An";
if (name) { }
TruthyRef.java:4: error: incompatible types: String cannot be converted to boolean
        if (name) {
            ^
1 error

There is no if (list) meaning "not empty" and no if (str) meaning "not null". Write the comparison you mean: if (name != null), if (!name.isEmpty()), if (count != 0).

The one assignment Java does accept

The type rule closes the int case, but it cannot close the boolean one. done = true is an assignment expression of type boolean, so it is a legal condition:

boolean done = false;
System.out.println("before: done = " + done);
if (done = true) {
    System.out.println("this branch always runs");
}
System.out.println("after:  done = " + done);
before: done = false
this branch always runs
after:  done = true

The branch always runs and the variable is overwritten on the way in. javac -Xlint:all says nothing about it. This is the one place where Java gives you no protection, so read if conditions on boolean variables carefully, and prefer if (done) over if (done == true) — the shorter form has no = to lose.

Comparing values inside a condition

Conditions are usually comparisons, and Java has two kinds. For primitives, == compares values and is what you want. For objects — String included — == compares references, meaning "are these the same object", which is almost never the question you are asking.

Scanner sc = new Scanner(System.in);
System.out.print("Type yes: ");
String answer = sc.nextLine();

if (answer == "yes") {
    System.out.println("== says they match");
} else {
    System.out.println("== says they do NOT match");
}

if (answer.equals("yes")) {
    System.out.println(".equals() says they match");
} else {
    System.out.println(".equals() says they do NOT match");
}
printf 'yes\n' | java StringEq
Type yes: == says they do NOT match
.equals() says they match

The user typed exactly yes and == still said no, because a string built at runtime is a different object from the literal in the source. Use .equals() for content.

.equals() has one failure mode of its own: it is an instance method, so calling it on null throws.

String input = null;
System.out.println("\"yes\".equals(input) -> " + "yes".equals(input));
System.out.println("Objects.equals(input, \"yes\") -> " + Objects.equals(input, "yes"));
System.out.println("Objects.equals(null, null)   -> " + Objects.equals(null, null));
System.out.println(input.equals("yes"));
"yes".equals(input) -> false
Objects.equals(input, "yes") -> false
Objects.equals(null, null)   -> true
Exception in thread "main" java.lang.NullPointerException: Cannot invoke "String.equals(Object)" because "input" is null
	at Compare.main(Compare.java:9)

Two null-safe idioms come out of that. Put the literal on the left — "yes".equals(input) — and the receiver can never be null. Or call java.util.Objects.equals(a, b), which handles null on both sides and returns true when both are null. Strings have more to them than this; the article on String in this series covers identity, the pool and .equals() in depth.

The ternary operator as a compact if/else

if is a statement: it runs code, it does not produce a value. The ternary operator ?: is the expression form of the same decision, so it can sit on the right-hand side of an assignment:

int a = 7, b = 12;

int max;
if (a > b) {
    max = a;
} else {
    max = b;
}

int max2 = (a > b) ? a : b;

int n = 9;
System.out.println(n + " is " + (n % 2 == 0 ? "even" : "odd"));
if/else  max = 12
ternary  max = 12
9 is odd

Use it when both branches produce a value for the same variable, and especially inside string concatenation or a method argument, where an if statement cannot go. The operator itself — precedence, the autoboxing trap — belongs to the article on operators in this series.

Where it stops helping is nesting. A chain of ternaries is a chain of else if written without the words:

int score = 72;
String grade = score >= 90 ? "A" : score >= 80 ? "B" : score >= 70 ? "C" : score >= 60 ? "D" : "F";
nested ternary grade = C

That is correct and it is hard to scan. Two levels is usually the limit; past that, an if chain or a switch expression reads better.

switch: the classic colon form

switch compares one value against a list of constants. In the classic form, each constant is a case label ending in a colon, and default catches everything else:

static void describe(int day) {
    System.out.print("day " + day + " -> ");
    switch (day) {
        case 1:
            System.out.println("Monday");
            break;
        case 2:
            System.out.println("Tuesday");
            break;
        case 3:
            System.out.println("Wednesday");
            break;
        default:
            System.out.println("not a weekday I know");
    }
}
day 1 -> Monday
day 3 -> Wednesday
day 9 -> not a weekday I know

The important word there is label. A case is not a block. It is a place to jump to.

Fall-through is the default, not a bug in your compiler

Once control enters at a matching label, it keeps running downwards through everything below — other case labels included — until a break (or a return, or the end of the switch) stops it. Remove the breaks from the program above and the difference is immediate:

static void noBreak(int day) {
    System.out.print("day " + day + " -> ");
    switch (day) {
        case 1: System.out.print("Mon ");
        case 2: System.out.print("Tue ");
        case 3: System.out.print("Wed ");
        default: System.out.print("other");
    }
    System.out.println();
}
day 1 -> Mon Tue Wed other
day 3 -> Wed other
day 9 -> other

day 1 printed four things. With the breaks restored, the same calls print one each:

day 1 -> Mon 
day 3 -> Wed 
day 9 -> other

Control enters a switch at the matched case and falls through until a break stops it

Plain javac compiles the broken version without a word. javac -Xlint:fallthrough does warn:

FallThrough.java:7: warning: [fallthrough] possible fall-through into case
            case 2:
            ^
FallThrough.java:9: warning: [fallthrough] possible fall-through into case
            case 3:
            ^
FallThrough.java:11: warning: [fallthrough] possible fall-through into case
            default:
            ^
3 warnings

Turn that lint on. The bug it catches is the kind that reaches production, because it only shows up for the inputs that match an early label:

static double discount(String tier, double price) {
    double rate = 0;
    switch (tier) {
        case "gold":
            rate = 0.20;
        case "silver":
            rate = 0.10;
            break;
        case "bronze":
            rate = 0.05;
            break;
        default:
            rate = 0;
    }
    return price * (1 - rate);
}
gold   1000 -> 900.0
silver 1000 -> 900.0
bronze 1000 -> 950.0

Gold pays exactly what silver pays. One missing break, and the tier that matters most is the one that is wrong.

The one time fall-through is what you want

Stacked labels with no statements between them share a body, which is the clean way to express "any of these":

static int daysIn(int month, int year) {
    int days;
    switch (month) {
        case 1: case 3: case 5: case 7: case 8: case 10: case 12:
            days = 31;
            break;
        case 4: case 6: case 9: case 11:
            days = 30;
            break;
        case 2:
            days = (year % 4 == 0 && year % 100 != 0) || year % 400 == 0 ? 29 : 28;
            break;
        default:
            throw new IllegalArgumentException("month out of range: " + month);
    }
    return days;
}
2026-01 -> 31
2026-04 -> 30
2026-02 -> 28
2028-02 -> 29
Exception in thread "main" java.lang.IllegalArgumentException: month out of range: 13
	at GroupedCases.daysIn(GroupedCases.java:15)
	at GroupedCases.main(GroupedCases.java:25)

This is grouping, not fall-through into another body, and -Xlint:fallthrough does not warn about it. Anything else — a case that runs its own statements and then deliberately continues into the next one — deserves a comment saying so, because every reader will otherwise assume you forgot the break.

default, and where it can go

default is optional. Leave it out of a switch statement and an unmatched value simply does nothing.

It also does not have to be last. The compiler treats it as one more label, so it can sit anywhere — and if it has no break, control falls out of it into whatever label follows:

static void middleNoBreak(int n) {
    System.out.print("middle(" + n + ") -> ");
    switch (n) {
        case 1:
            System.out.print("one ");
            break;
        default:
            System.out.print("other ");
        case 2:
            System.out.print("two ");
            break;
    }
    System.out.println();
}
middle(1) -> one 
middle(2) -> two 
middle(7) -> other two

middle(7) matched default, printed other, then fell through into case 2. That is legal and almost always a mistake. Put default last and give it a break or a throw.

What types can switch accept?

The selector — the expression in switch (...) — is restricted. These work:

Selector typeSinceNote
byte, short, char, intJava 1.0the original set
Byte, Short, Character, IntegerJava 5unboxed automatically
enumJava 5case labels are the bare constant names
StringJava 7compared with .equals(), not ==
any reference type with patternsJava 21see the pattern matching section

And these do not. long, float, double and boolean are rejected outright:

long id = 3L;
switch (id) {
    case 1L:
        System.out.println("one");
        break;
    default:
        System.out.println("other");
}
SwitchLong.java:4: error: selector type long is not allowed
        switch (id) {
               ^
1 error

double and boolean give the same message with their own type name. For a long, either cast it when the range allows or use an if chain. For a boolean, an if/else already is the switch.

One more rule: a case label must be a compile-time constant. A static final int works, a plain local variable does not:

int limit = 10;
switch (n) {
    case limit: System.out.println("matched the local"); break;
    default:    System.out.println("no match");
}
CaseConst.java:9: error: constant expression required
            case limit: System.out.println("matched the local"); break;
                 ^
1 error

A final int limit = 10; local is a constant and does compile. Duplicate labels are rejected too: error: duplicate case label.

Switching on a String has one runtime hazard. The classic form calls a method on the selector, so null throws before any label is considered:

String cmd = null;
switch (cmd) {
    case "list": System.out.println("listing"); break;
    default:     System.out.println("unknown command");
}
Exception in thread "main" java.lang.NullPointerException: Cannot invoke "String.hashCode()" because "<local2>" is null
	at SwitchNull.main(SwitchNull.java:4)

String.hashCode() in that message is the desugaring showing through: a String switch compiles into a hash lookup followed by an equals check. default does not catch null here — guard before the switch, or use the Java 21 form described below, which can label null explicitly.

Modern switch: arrow labels, expressions and yield

Java 14 finalised a second syntax. Replace the colon with -> and the label owns exactly one statement, expression or block — and control never falls through to the next label:

enum Day { MON, TUE, WED, THU, FRI, SAT, SUN }

static void classify(Day d) {
    System.out.print(d + " -> ");
    switch (d) {
        case MON, TUE, WED, THU, FRI -> System.out.println("work day");
        case SAT, SUN                -> System.out.println("weekend");
    }
}
MON -> work day
SAT -> weekend

Two things changed at once. There is no break because there is nothing to break out of, and a single label can list several constants separated by commas, which replaces the stacked-label trick.

The two forms cannot be mixed inside one switch:

MixForms.java:6: error: different case kinds used in the switch
            case 2: System.out.println("two"); break;
            ^
1 error

The bigger change is that a switch can be an expression — it produces a value, so it can be assigned:

static int daysIn(int month, int year) {
    return switch (month) {
        case 1, 3, 5, 7, 8, 10, 12 -> 31;
        case 4, 6, 9, 11 -> 30;
        case 2 -> (year % 4 == 0 && year % 100 != 0) || year % 400 == 0 ? 29 : 28;
        default -> throw new IllegalArgumentException("month out of range: " + month);
    };
}
daysIn(2, 2026) = 28
daysIn(2, 2028) = 29

Note the semicolon after the closing brace: the whole switch is one expression inside a return statement. A throw is allowed as an arm because it completes abruptly instead of producing a value.

When an arm needs more than one line, use a block — and then yield supplies the value the arm produces:

static String bucket(int n) {
    return switch (n) {
        case 0 -> "zero";
        case 1, 2, 3 -> "small";
        default -> {
            String size = n > 100 ? "huge" : "medium";
            System.out.println("   (computing bucket for " + n + ")");
            yield size + " (" + n + ")";
        }
    };
}
bucket(0)   = zero
bucket(2)   = small
   (computing bucket for 50)
bucket(50)  = medium (50)
   (computing bucket for 500)
bucket(500) = huge (500)

yield is not return. return inside a switch expression is a compile error, because it would try to leave the enclosing method from inside an expression:

BadYield.java:5: error: attempt to return out of a switch expression
            default -> { return "other"; }
                         ^
1 error

The colon form with break, the arrow form, and a switch expression that yields a value

A switch expression must produce a value on every path

A switch statement may ignore inputs it does not handle. A switch expression may not: it has to produce a value whatever comes in, so the compiler demands that the arms be exhaustive. Over an enum that means every constant, or a default:

enum Status { NEW, PAID, SHIPPED }

static String label(Status s) {
    return switch (s) {
        case NEW -> "waiting for payment";
        case PAID -> "ready to pack";
    };
}
EnumMissing.java:5: error: the switch expression does not cover all possible input values
        return switch (s) {
               ^
1 error

Add case SHIPPED -> "on the way"; and it compiles:

waiting for payment
ready to pack
on the way

This is a feature, not an obstacle. Listing every constant and omitting default means that the day someone adds a fourth constant to the enum, the compiler points at every switch that has to be updated. A default arm silences that warning forever.

The same exhaustiveness rule does not apply to a switch statement in arrow form — an unmatched value just does nothing:

enum Day { MON, SAT, SUN }

Day d = Day.SUN;
switch (d) {
    case MON -> System.out.println("work day");
}
System.out.println("nothing matched, and that is fine for a statement");
nothing matched, and that is fine for a statement

Pattern matching for switch, final in Java 21

Java 21 finalised pattern matching for switch (JEP 441), after preview releases in Java 17 through 20. A case label can now be a type pattern that both tests the type and binds a variable, optionally narrowed by a when guard, and null becomes a label you can write:

static String describe(Object o) {
    return switch (o) {
        case null                   -> "nothing at all";
        case Integer i when i > 100 -> "a big int: " + i;
        case Integer i              -> "an int: " + i;
        case String s               -> "a String of length " + s.length();
        case Double d               -> "a double: " + d;
        default                     -> "a " + o.getClass().getSimpleName();
    };
}
an int: 7
a big int: 500
a String of length 5
a double: 3.5
nothing at all
a LocalDate

Three rules are worth remembering. Order matters, because the first matching pattern wins — the guarded Integer i when i > 100 has to come before the plain Integer i or it can never match. Without a case null, a pattern switch throws instead of falling to default:

Exception in thread "main" java.lang.NullPointerException
	at java.base/java.util.Objects.requireNonNull(Objects.java:233)
	at PatternNull.describe(PatternNull.java:3)
	at PatternNull.main(PatternNull.java:11)

And case null, default -> is a legal combined label when you want both handled the same way.

On an older JDK the whole construct is rejected, with the release named in the message:

PatternPreview.java:4: error: patterns in switch statements are not supported in -source 20
            case Integer i -> "int " + i;
                 ^
  (use -source 21 or higher to enable patterns in switch statements)
1 error

Java 21 also relaxed a smaller rule: an enum case label may now be qualified, so case Status.PAID: compiles. Under --release 17 the same line is rejected with error: an enum switch case label must be the unqualified name of an enumeration constant.

if chain or switch: which one reads better?

if chainswitch
Testsany boolean expressionone value against constants
Ranges and compound conditionsyesno, only equality
Selector typesanything that yields a booleanint family, String, enum, patterns
Number of branches it stays readable attwo or threemany
Missing casesilently does nothingexpression form is a compile error
Dispatch costone comparison per branch, in orderone jump for a dense set of labels

The dispatch difference is visible in the bytecode rather than in a stopwatch. A four-way if chain compiles to four sequential comparisons; the same four cases in a switch compile to a single tableswitch, a jump table indexed by the value:

javap -c Dispatch
  static int withIfChain(int);
       0: iload_0
       1: iconst_1
       2: if_icmpne     8
       5: bipush        10
       7: ireturn
       8: iload_0
       9: iconst_2
      10: if_icmpne     16
      ...

  static int withSwitch(int);
       0: iload_0
       1: tableswitch   { // 1 to 4
                     1: 32
                     2: 35
                     3: 38
                     4: 41
               default: 44
          }

With sparse labels — 1, 500, 9000 — javac emits lookupswitch instead, a sorted table searched by the JVM. For the handful of branches most programs contain the difference does not matter; the reason to reach for switch is that it says "one value, many constants" in a way an if chain does not.

In practice: switch for a value tested against a fixed set of constants, especially an enum or a command string; if for ranges, for compound conditions, and for anything involving more than one variable. A menu dispatcher is the clearest case for a switch expression:

import java.util.Scanner;

public class Menu {
    public static void main(String[] args) {
        Scanner sc = new Scanner(System.in);
        System.out.print("Command (list / add / quit): ");
        String cmd = sc.nextLine().trim().toLowerCase();

        String reply = switch (cmd) {
            case "list", "ls" -> "showing every item";
            case "add", "new" -> "creating an item";
            case "quit", "exit" -> "goodbye";
            default -> "unknown command: " + cmd;
        };
        System.out.println(reply);
    }
}
printf 'new\n' | java Menu
Command (list / add / quit): creating an item

Common mistakes, with the real errors

A missing break in the colon form. Shown above: the gold tier silently gets the silver discount. Compile with -Xlint:fallthrough, or use arrow labels, where the mistake cannot be expressed.

= where == was meant. On an int this is a compile error, incompatible types: int cannot be converted to boolean. On a boolean it compiles and the branch always runs. Write if (done), never if (done == true).

Comparing a String with ==. No error, no warning, and it appears to work in tests where both strings are literals and share the pool entry. It fails the moment one of them comes from input, a file or a network call. Use .equals(), "literal".equals(x) or Objects.equals(a, b).

Unreachable code after a return inside a branch. Java rejects it:

static String grade(int score) {
    if (score >= 60) {
        return "pass";
        System.out.println("logging a pass");
    }
    return "fail";
}
Unreachable.java:5: error: unreachable statement
            System.out.println("logging a pass");
            ^
1 error

The related error catches the opposite mistake — a method whose only return is inside an if, so the path where the condition is false falls off the end:

MissingReturn.java:6: error: missing return statement
    }
    ^
1 error

A stray semicolon after the condition. if (x > 10); is an if whose body is the empty statement, and the block that follows is a plain block that always runs:

int x = 3;
if (x > 10);
{
    System.out.println("x is greater than 10");
}
System.out.println("x = " + x);
x is greater than 10
x = 3

Plain javac is silent. -Xlint:all catches it:

EmptyIf.java:4: warning: [empty] empty statement after if
        if (x > 10);
                   ^
1 warning

FAQ

Is there an elseif keyword in Java?

No. It is two words, else if, and it is not a special construct — the else simply owns another if statement. That is also why the chain has no fixed limit and why the conditions are evaluated strictly in order.

Why does my switch run several cases at once?

A case label marks an entry point, not a block. Once control jumps to a matching label it keeps running through the labels below it until a break, a return or the closing brace. Add the missing break, or convert the switch to arrow labels, which cannot fall through.

Can a switch work on a long or a double?

No. javac reports error: selector type long is not allowed, and the same for double, float and boolean. Allowed selectors are byte, short, char, int and their wrappers, String, enum, and — since Java 21 — any reference type when the labels are patterns.

What is the difference between yield and return in a switch?

yield supplies the value of the enclosing switch expression from inside a block arm; execution continues after the switch. return leaves the whole method, which is why using it inside a switch expression is a compile error: attempt to return out of a switch expression.

Do I need a default in every switch?

In a switch statement, no — an unmatched value does nothing. In a switch expression, the arms must cover every possible input, so you need either a default or, over an enum, one arm per constant. Prefer listing the constants: the compiler then tells you about every switch that needs updating when a constant is added.

Should I always use braces on an if?

Yes. The brace-less form is legal and it is the shape behind both the misplaced-statement bug and the dangling else. Two characters per branch buys you a body that means what its indentation says.

Conclusion

if tests any boolean expression, top to bottom, first match wins, and everything below the match is never evaluated. switch tests one value against constants, and in its classic form a matched case is an entry point that keeps running until a break. The arrow form removes fall-through entirely, the expression form produces a value and forces you to handle every input, and Java 21's patterns extend the same syntax to types. Braces on every branch, .equals() for objects, and -Xlint:all on the compiler cover most of what remains.

A conditional runs a branch once. The next article covers the other way a program stops running straight through: loops — for, while and do-while, and how to choose between them.

Related Posts

[Java Basics] Variable Scope in Java: Local, Field and Static

Variable scope in Java explained: local variables, parameters, instance fields and static fields, block scope, shadowing, definite assignment and lifetime, with every cannot find symbol error reproduced on JDK 21.

[Java Basics] Inheritance in Java: extends and super

How extends works in Java, what a subclass inherits and what it does not, why constructors are never inherited, how super(...) chains constructors up to java.lang.Object and back down, field hiding versus overriding, protected across packages, final classes, the fragile base class problem, and when composition is the better answer.

[Java Basics] Basic Array Algorithms in Java: Max, Min, Search and Sort

Max, min, sum, average, filtering, reversing, linear and binary search, bubble, selection and insertion sort, and Arrays.sort in Java - every algorithm instrumented with comparison and swap counters and every output produced on JDK 21.

[Java Basics] Exception Handling in Java: try, catch, finally, throw and throws

Exception handling in Java on OpenJDK 21: the Throwable hierarchy, try-catch-finally and what finally really guarantees, multi-catch and catch ordering, throw versus throws, checked versus unchecked exceptions, stack unwinding and how to read a stack trace, plus the traps — the empty catch, catch (Exception e), and return inside finally.