Conditionals
Java’s conditional constructs are familiar from the C-family — if/else, the conditional operator ?:, and switch — with substantial modern additions: the switch expression (Java 14), pattern matching for switch (Java 21), and pattern bindings in if via instanceof (Java 16). The combination admits expressing what older Java code wrote as long if-cascades or visitor-based dispatchers in a more declarative form. This page covers the principal selection constructs; the Pattern matching page covers the pattern grammar in detail.
if and else
The basic if statement evaluates its boolean controlling expression and executes the body if the result is true:
if (n > 0) {
process(n);
}
The body may be any statement; the conventional form uses a compound statement (block) even for a single line. The else clause attaches to the immediately preceding if:
if (x > 0) {
classifyPositive(x);
} else if (x < 0) {
classifyNegative(x);
} else {
classifyZero();
}
Java does not have elif; else if is two keywords.
Boolean-only controlling expression
Unlike C, the controlling expression of if must be of type boolean (or Boolean, with auto-unboxing). Implicit conversion from integer or pointer to boolean is not admitted:
int count = 5;
if (count) { // ERROR: int cannot be converted to boolean
// ...
}
if (count != 0) { // OK
// ...
}
String value = maybeNull();
if (value) { // ERROR: String cannot be converted to boolean
// ...
}
if (value != null) { // OK
// ...
}
The strictness eliminates the C-family if (x = y) typo (= versus ==): if (x = 5) is a compile-time error in Java because the result of the assignment is int, not boolean. The exception is when the assignment is to a boolean:
boolean ready = false;
if (ready = check()) { // OK: the assignment is to a boolean variable
// ...
}
Pattern bindings
Java 16 admitted pattern bindings in if via instanceof:
if (obj instanceof String s) {
System.out.println(s.length());
}
The bound variable s is in scope where the test holds. The construction is the conventional contemporary replacement for the older if (obj instanceof String) { String s = (String) obj; … } pattern. Pattern bindings extend to records and sealed types in switch; the full grammar is in Pattern matching.
The conditional operator
The ternary ?: operator selects between two expressions:
int max = (a > b) ? a : b;
String s = (n == 1) ? "" : "s";
The two arms must have a common type, with the standard’s elaborate rules. Mixing types unwisely produces compilation errors. Nested conditionals are syntactically legal but quickly become unreadable; one or two levels is typically the practical limit.
The switch statement
The classical switch:
switch (token.kind()) {
case PLUS:
result = a + b;
break;
case MINUS:
result = a - b;
break;
case TIMES:
case DIVIDE:
result = applyMulDiv(token.kind(), a, b);
break;
default:
throw new IllegalStateException();
}
The switch statement supports several discriminant types:
- All primitive integer types except
long. - The
chartype. - The wrapper types (autoboxed and unboxed).
String(since Java 7).enumtypes.
Notably, long, float, double, boolean, and arbitrary reference types are not admitted in the classical switch. The pattern-matching switch (Java 21) extends the discriminant types substantially.
Fallthrough between cases is implicit unless break is reached:
switch (state) {
case IDLE:
prepare();
// FALLTHROUGH (no break)
case RUNNING:
execute();
break;
}
Implicit fallthrough between substantive cases is a frequent source of bugs; modern Java code uses the switch expression (with arrow syntax) to eliminate the issue.
The switch expression
Java 14 introduced the switch expression: a value-yielding form of switch with the arrow syntax that disallows fallthrough:
double area = switch (shape) {
case Circle c -> Math.PI * c.radius() * c.radius();
case Square s -> s.side() * s.side();
case Triangle t -> 0.5 * t.base() * t.height();
case null -> throw new NullPointerException();
};
The form has several substantial advantages:
- It is an expression, so it can appear anywhere a value is expected (initialisers, return values, lambda bodies).
- The arrow syntax (
->) eliminates implicit fallthrough. - The compiler checks for exhaustiveness and warns if not every case is covered.
- Pattern matching (since Java 21) admits type, record, and constant patterns in case labels.
The arrow form admits multiple labels per arm:
String dayType(DayOfWeek d) {
return switch (d) {
case MONDAY, TUESDAY, WEDNESDAY, THURSDAY, FRIDAY -> "weekday";
case SATURDAY, SUNDAY -> "weekend";
};
}
For arms that need a block (multiple statements or a non-trivial computation), use { … yield value; }:
String classify(int n) {
return switch (n) {
case 0 -> "zero";
default -> {
int abs = Math.abs(n);
String sign = n > 0 ? "positive" : "negative";
yield sign + " " + abs;
}
};
}
The yield keyword (Java 14 contextual keyword) supplies the value for a block-bodied arm. It is distinct from return, which would exit the enclosing method.
Exhaustiveness
The compiler attempts to determine whether the patterns cover every possible input; if not, a compile-time error is produced for switch expressions:
public enum Color { RED, GREEN, BLUE }
String describe(Color c) {
return switch (c) {
case RED -> "red";
case GREEN -> "green";
// ERROR: 'BLUE' is unhandled; switch expression requires exhaustiveness
};
}
The exhaustiveness check catches newly added enum values that the switch does not handle. Adding a default arm or covering the missing values silences the error.
For sealed types (Java 17+), the compiler tracks the subtype set and enforces exhaustiveness:
public sealed interface Shape permits Circle, Square, Triangle { }
double area(Shape s) {
return switch (s) {
case Circle c -> Math.PI * c.radius() * c.radius();
case Square sq -> sq.side() * sq.side();
case Triangle t -> 0.5 * t.base() * t.height();
};
}
The sealed-type mechanism gives Java a closed-set discrimination similar to algebraic data types in functional languages; the full treatment is in Pattern matching and Classes, records, and sealed types.
Selection idioms
Several patterns recur in idiomatic Java control flow.
Early return
public Result parse(String input) {
if (input == null || input.isEmpty()) return Result.empty();
if (input.length() > MAX_LENGTH) return Result.tooLong();
if (!isValid(input)) return Result.malformed();
/* main body */
return Result.ok(/* ... */);
}
The pattern reduces nesting and keeps the precondition checks visually separate from the substantive body.
Switch expression for value mapping
String classify(Status s) {
return switch (s) {
case OK -> "ok";
case WARNING -> "warn";
case ERROR -> "error";
case CRITICAL -> "critical";
};
}
The form is the conventional contemporary replacement for Map<Status, String> lookups when the set of values is small and known at compile time.
Pattern matching with instanceof
if (obj instanceof String s && !s.isEmpty()) {
System.out.println(s.toUpperCase());
}
if (token instanceof NumberToken nt && nt.value() > 0) {
/* nt is in scope here */
}
The instanceof pattern admits the type test, the cast, and a binding in one expression. The construction is the conventional way to dispatch on a runtime type without a full switch.
Conditional expression for short selections
String separator = items.size() == 1 ? "" : ", ";
return String.format("%d item%s", items.size(), separator);
The conditional operator is the conventional choice for short selections inside larger expressions.
Switch on tuples (via record patterns)
Java 21 admits record patterns in switch:
public record Pair<A, B>(A first, B second) { }
String describe(Pair<Status, Status> p) {
return switch (p) {
case Pair(Status.OK, Status.OK) -> "both ok";
case Pair(Status.OK, var s) -> "first ok, second " + s;
case Pair(var s, Status.OK) -> "first " + s + ", second ok";
case Pair(Status s1, Status s2) -> "neither ok: " + s1 + ", " + s2;
};
}
The construction admits compact discrimination over multiple axes; it is the closest Java comes to algebraic-data-type pattern matching.
A note on the absence of switch fallthrough in expression form
The switch expression with arrows is the conventional contemporary form because it eliminates the fallthrough trap. The classical switch statement remains in the language; new code should prefer the expression form unless there is a specific reason (a side-effecting body that doesn’t yield a value, an explicit fallthrough that the construction makes clear) to use the statement form.
The expression form’s exhaustiveness checking, value-returning shape, and arrow syntax together make it substantially safer to write and easier to read than the classical statement. The construction is one of Java’s most useful additions of the past decade.