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.
![]()
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.

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.

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

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 type | Since | Note |
|---|---|---|
byte, short, char, int | Java 1.0 | the original set |
Byte, Short, Character, Integer | Java 5 | unboxed automatically |
enum | Java 5 | case labels are the bare constant names |
String | Java 7 | compared with .equals(), not == |
| any reference type with patterns | Java 21 | see 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

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 chain | switch | |
|---|---|---|
| Tests | any boolean expression | one value against constants |
| Ranges and compound conditions | yes | no, only equality |
| Selector types | anything that yields a boolean | int family, String, enum, patterns |
| Number of branches it stays readable at | two or three | many |
| Missing case | silently does nothing | expression form is a compile error |
| Dispatch cost | one comparison per branch, in order | one 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.