01 · Java basics — note 6 of 12
Conditions and switch
Choose the right branching tool for every decision, from simple guards to pattern matching.
Conditional statements evaluate boolean expressions and match discrete values to execute exactly one chosen branch.
The idea in real life
Think of a train track switch at a railway junction. As an oncoming train reaches the junction, the track switch shifts the rails so the train rolls down one specific path while leaving all other tracks closed. Conditional statements work like that track switch in your code: they inspect an incoming condition or value, direct execution down the selected branch, and bypass every alternate path.
Why it exists
- Algorithms must adapt their behaviour to user input and changing state rather than running the exact same fixed list of statements every time.
- Multi-branch conditionals let you handle numerical ranges, boundaries, and fallback default cases in a clear top-to-bottom sequence.
- Matching one value against many fixed keys with
switchreads more clearly than a long chain of equality checks. - Modern switch expressions turn branching into value-producing expressions while guaranteeing exhaustiveness at compile time.
How it works
if, else if and else
Evaluate boolean conditions in sequence until the first true branch runs.
The if statement evaluates a boolean condition. If the condition is true, the statements inside its block execute. If the condition is false, Java skips that block and checks the next else if condition, continuing down the chain until it finds a condition that evaluates to true.
If none of the conditions evaluate to true, the optional else block executes as a fallback. A pair of curly braces defines a block of statements that run together when the condition is met.
The following program evaluates an exam score and assigns a letter grade based on numeric ranges:
public class Main {
public static void main(String[] args) {
int score = 85;
char grade;
if (score >= 90) {
grade = 'A';
} else if (score >= 80) {
grade = 'B';
} else if (score >= 70) {
grade = 'C';
} else if (score >= 60) {
grade = 'D';
} else {
grade = 'F';
}
System.out.println("Score: " + score);
System.out.println("Grade: " + grade);
}
}Score: 85
Grade: BIn this program:
scoreholds the student's test score, set to 85.gradestores the assigned letter grade character.- The condition
score >= 90evaluates tofalse, so Java skips the first block. - The condition
score >= 80evaluates totrue, so Java assigns'B'togradeand immediately skips the remainingelse ifandelsebranches.
The ternary operator
Produce one of two values using a single conditional expression.
The ternary operator (? :) is a compact expression that selects between two values based on a boolean condition. Its syntax takes three operands:
condition ? expressionIfTrue : expressionIfFalseFirst, Java evaluates condition. If it is true, the operator evaluates and yields expressionIfTrue. If it is false, the operator evaluates and yields expressionIfFalse.
Unlike an if statement, the ternary construct is an expression, meaning it computes and returns a concrete value. You can assign that value directly to a variable or pass it directly into a method call.
The ternary operator works best for short, two-way decisions such as computing an absolute value or finding the maximum of two numbers:
public class Main {
public static void main(String[] args) {
int first = 14;
int second = 25;
// Choose the larger number in a single concise line
int max = (first > second) ? first : second;
// Determine whether a score passed
int score = 72;
String result = (score >= 60) ? "Pass" : "Fail";
System.out.println("Max: " + max);
System.out.println("Result: " + result);
}
}Max: 25
Result: PassIn this program:
firstandsecondhold the two integer values being compared.maxreceives 25 becausefirst > secondevaluates tofalse, selectingsecond.scoreholds the test result 72, andresultreceives"Pass"becausescore >= 60evaluates totrue.
Avoid chaining or nesting ternary operators. Writing expressions like a ? b : c ? d : e hurts readability, obscures operator precedence, and makes logic errors difficult to spot during code reviews.
Classic switch
Match a single discrete value against constant case labels.
A classic switch statement evaluates a single selector expression and compares its value against constant case labels. When a matching label is found, execution jumps directly to that branch and runs its statements until it encounters a break statement or reaches the end of the switch block.
The break statement terminates execution of the switch body and jumps to the first statement following the closing curly brace. If no case matches, execution jumps to the optional default label.
Java permits the selector expression to be one of the following types:
- Primitive integer types:
byte,short,char, andint. Stringliterals and variables.- Enumerated types (enums are introduced in chapter 02).
Primitive floating-point types (float, double), 64-bit integers (long), and boolean cannot be used as switch selectors.
Every case label must be a compile-time constant, such as a literal value or a final variable initialized with a constant expression. You cannot use variable names or range checks such as case x > 5:.
The following program maps an integer day code to the corresponding day name:
public class Main {
public static void main(String[] args) {
int dayCode = 3;
String dayName;
switch (dayCode) {
case 1:
dayName = "Monday";
break;
case 2:
dayName = "Tuesday";
break;
case 3:
dayName = "Wednesday";
break;
case 4:
dayName = "Thursday";
break;
case 5:
dayName = "Friday";
break;
default:
dayName = "Weekend";
break;
}
System.out.println("Day 3: " + dayName);
}
}Day 3: WednesdayIn this program:
dayCodeholds the integer day identifier, set to 3.dayNamestores the resulting text description.- Execution matches
case 3:, assigns"Wednesday"todayName, andbreakexits the switch.
Fall-through
Execution slides into following cases when a break statement is omitted.
In a classic switch statement, execution does not stop automatically at the end of a case block. If you omit the break statement, execution continues directly into the subsequent case branches, running their statements even though their labels do not match the selector. This behaviour is called fall-through.
Accidental fall-through is a well-known source of bugs in classic switch statements. However, fall-through can also be used intentionally to share a single block of logic across multiple case labels.
The following program demonstrates both an unintended cascade and an intentional case grouping:
public class Main {
public static void main(String[] args) {
int stage = 1;
System.out.println("Starting pipeline at stage " + stage + ":");
// Unintended fall-through: missing break statements
switch (stage) {
case 1:
System.out.println("Stage 1 complete");
case 2:
System.out.println("Stage 2 complete");
case 3:
System.out.println("Stage 3 complete");
break;
default:
System.out.println("Invalid stage");
}
// Intentional fall-through: grouping case labels
int dayOfWeek = 6;
String dayType;
switch (dayOfWeek) {
case 1:
case 2:
case 3:
case 4:
case 5:
dayType = "Weekday";
break;
case 6:
case 7:
dayType = "Weekend";
break;
default:
dayType = "Unknown";
break;
}
System.out.println("Day 6 type: " + dayType);
}
}Starting pipeline at stage 1:
Stage 1 complete
Stage 2 complete
Stage 3 complete
Day 6 type: WeekendIn this program:
- In the first switch,
stageis 1. Becausecase 1:andcase 2:lackbreakstatements, execution falls through and prints all three completion messages before reaching thebreakon stage 3. - In the second switch, cases 1 through 5 are stacked together so any weekday executes the exact same assignment before breaking.
Going deeper
Switch expressions
Return a value directly from a switch with arrow labels.
Standardized in Java 14 (JEP 361) and fully supported on JDK 17, switch expressions transform the switch construct into a value-producing expression.
Switch expressions introduce arrow labels (->). An arrow label executes only the expression or block to its right. There is no fall-through between arrow cases, so you never write break.
You can also list multiple comma-separated constants on a single case line, eliminating the need to stack multiple empty labels.
When a branch requires multiple statements, you enclose them in a block { ... } and use the yield keyword to return the value for that branch.
The following program uses a switch expression to classify days and compute values:
public class Main {
public static void main(String[] args) {
int day = 6;
// Arrow syntax with multiple comma-separated labels
String dayType = switch (day) {
case 1, 2, 3, 4, 5 -> "Weekday";
case 6, 7 -> "Weekend";
default -> "Unknown";
};
// Block body with yield for multi-statement branches
int code = 2;
int processedValue = switch (code) {
case 1 -> 100;
case 2 -> {
int factor = 15;
int base = 20;
yield factor * base;
}
default -> 0;
};
System.out.println("Day type: " + dayType);
System.out.println("Processed value: " + processedValue);
}
}Day type: Weekend
Processed value: 300In this program:
dayTypereceives"Weekend"directly from the switch expression. Notice the semicolon;at the end of the assignment statement.- In the second switch,
case 2runs a block that declares local variablesfactorandbase, and returns their product 300 usingyield.
Exhaustiveness
Switch expressions must cover every conceivable value of the selector type.
A switch expression must be exhaustive: it must provide a value for every possible input value of the selector type. If any valid input could slip through without producing a value, the compiler rejects the program.
Classic switch statements do not enforce exhaustiveness because a statement does not return a value; unhandled values simply fall out of the switch body. A switch expression, by contrast, must always yield a result to its enclosing statement or variable assignment.
Because types such as int, char, and String have vast or infinite domains of possible values, you must provide a default branch when switching over them:
Main.java:5: error: the switch expression does not cover all possible input values
String dayName = switch (day) {
^
1 errorThe only exception occurs when switching over an enum type (introduced in chapter 02), where the compiler can verify that every declared enum constant is covered by an explicit case label.
Early returns
Flatten deeply nested conditions by returning immediately when a branch matches.
Deeply nested if-else blocks create complex indentation structures often described as the pyramid of doom. You can flatten these structures using early returns (also known as guard clauses).
A guard clause checks boundary conditions, special edge cases, or invalid inputs at the start of a method and exits immediately with a return statement. Once those checks pass, the remaining logic runs at the base indentation level without nested else blocks.
The following program demonstrates how early returns keep branching flat and readable:
public class Main {
// Returns the shipping fee based on distance and membership status.
static int calculateShippingFee(int distanceKm, boolean isMember) {
if (distanceKm <= 0) {
return 0;
}
if (isMember) {
return 0;
}
if (distanceKm > 100) {
return 25;
}
return 10;
}
public static void main(String[] args) {
System.out.println("Local member fee: " + calculateShippingFee(15, true));
System.out.println("Long distance non-member: " + calculateShippingFee(120, false));
System.out.println("Short distance non-member: " + calculateShippingFee(30, false));
}
}Local member fee: 0
Long distance non-member: 25
Short distance non-member: 10In this program:
- Static methods group code within the class (methods are covered in note 8).
calculateShippingFeeinspects the inputsdistanceKmandisMember.- Non-positive distances and active memberships exit immediately with a fee of 0, keeping the remaining distance logic un-nested.
Choosing between if, ternary and switch
Match the construct to the shape of the decision you need to make.
Selecting the clearest branching construct depends on whether you are testing ranges, combining boolean conditions, or matching discrete keys:
| Construct | Best used for | Evaluates | Handles ranges? | Produces a value? |
|---|---|---|---|---|
if / else if | Complex conditions, ranges, and state checks | Any boolean expression | Yes (score >= 80) | No (statement) |
Ternary (? :) | Simple binary choices assigned in a single line | One boolean condition | Yes | Yes (expression) |
| Switch expression | Matching a single variable against discrete keys | Primitive integers, strings, enums | No (exact keys only) | Yes (expression) |
| Classic switch | Legacy code or intentional statement fall-through | Primitive integers, strings, enums | No (exact keys only) | No (statement) |
Follow these guidelines:
- Use the ternary operator when selecting between two simple values to assign or return.
- Use if / else if / else when checking numeric ranges, floating-point numbers, or multiple boolean expressions connected with logical AND (
&&) or OR (||). - Use switch expressions when matching a single discrete variable against three or more known constant values or categories.
Under the hood
Pattern matching for switch
Inspect types and test guard conditions directly inside switch cases.
In Java 21 and newer (finalized in JEP 441), switch statements and expressions support pattern matching. Instead of testing only constant primitive keys or strings, a switch can match an object's runtime type, bind it to a local pattern variable, and apply boolean guards.
Before Java 21, passing null to a switch on a String throws a NullPointerException before any case is checked. In Java 21 or newer, you can handle null safely using an explicit case null -> branch.
You can also attach a boolean guard to any pattern using the when keyword. The case branch executes only if the type matches and the guard condition evaluates to true.
The following program runs on Java 21 or newer to inspect input values:
public class Main {
// Returns a description of an object based on its type and value.
static String describeInput(Object input) {
return switch (input) {
case null -> "Empty reference";
case String text when text.isEmpty() -> "Blank text value";
case String text -> "Text of length " + text.length() + ": " + text;
default -> "Unsupported value";
};
}
public static void main(String[] args) {
System.out.println(describeInput("Algoz"));
System.out.println(describeInput(""));
System.out.println(describeInput(null));
}
}Text of length 5: Algoz
Blank text value
Empty referenceIn this program:
describeInputaccepts anObjectreference (objects and class hierarchies are covered in chapter 02).case null ->catches null references safely without throwing an exception.case String text when text.isEmpty()checks thatinputis aStringand tests its content with a boolean guard before the more generalcase String textbranch.
How switch compiles
The JVM selects between indexed jumps and binary search tables.
When the Java compiler translates a switch construct into bytecode, it chooses between two dedicated instructions based on the density of the case values: tableswitch and lookupswitch.
The tableswitch instruction is used when case values form a dense, contiguous sequence of integers (such as 1, 2, and 3). The JVM creates an indexed jump table in bytecode. At run time, the selector value is used as an index into that table, so the JVM jumps straight to the right case.
The lookupswitch instruction is used when case values are sparse, with large numerical gaps between keys (such as 10, 500, and 10000). Allocating an indexed table for sparse keys would waste bytecode space on empty slots. Instead, the compiler stores an ordered list of key-and-offset pairs. At run time, the JVM searches the sorted keys for the selector; because they are sorted, it can use a fast search such as binary search (chapter 18).
Disassembling compiled switch methods with javap -c reveals how the compiler chooses between these two bytecode instructions:
Compiled from "SwitchDemo.java"
public class SwitchDemo {
public static int denseSwitch(int);
Code:
0: iload_0
1: tableswitch { // 1 to 3
1: 28
2: 31
3: 34
default: 37
}
28: bipush 10
30: ireturn
31: bipush 20
33: ireturn
34: bipush 30
36: ireturn
37: iconst_0
38: ireturn
public static int sparseSwitch(int);
Code:
0: iload_0
1: lookupswitch { // 3
10: 36
500: 39
10000: 43
default: 46
}
36: bipush 100
38: ireturn
39: sipush 5000
42: ireturn
43: ldc #7 // int 99999
45: ireturn
46: iconst_0
47: ireturn
}When switching on a String, the compiler evaluates hashCode() first using a lookup table on integer hash codes, and then verifies the match using equals() to prevent hash collision errors.
Dangling else
An ambiguous else clause always binds to the closest unclosed if statement.
The dangling else problem is a classic grammatical ambiguity in programming languages when if statements are nested without curly braces. When an inner if is followed by an else, does the else belong to the outer condition or the inner one?
Under the Java Language Specification (JLS §14.9.1), an else clause always binds to the nearest preceding if statement that does not already have an else.
The compiler completely ignores visual indentation. If you indent an else to align with the outer if without using braces, the compiler still attaches the else to the inner if:
public class Main {
public static void main(String[] args) {
int x = 15;
int y = 5;
// Misleading indentation: the else attaches to the inner if
if (x > 10)
if (y > 10)
System.out.println("Both greater than 10");
else
System.out.println("Misleading output: x is not greater than 10");
}
}Misleading output: x is not greater than 10In this program:
xis 15, so the outer conditionx > 10evaluates totrue.- Execution enters the inner statement
if (y > 10). Becauseyis 5, that condition isfalse. - The compiler bound the
elsetoif (y > 10), so it runs theelsebranch and prints thatxis not greater than 10, even thoughxis 15.
Always enclosing every conditional body in explicit curly braces {} eliminates this ambiguity completely.
In Java
The following program combines conditional branching, switch expressions, early returns, and the ternary operator to process student grades and determine academic standing:
public class Main {
// Returns the letter grade corresponding to a numeric score.
static char getLetterGrade(int score) {
if (score >= 90) {
return 'A';
} else if (score >= 80) {
return 'B';
} else if (score >= 70) {
return 'C';
} else if (score >= 60) {
return 'D';
} else {
return 'F';
}
}
// Returns a performance description based on the letter grade.
static String describeGrade(char grade) {
return switch (grade) {
case 'A' -> "Excellent work";
case 'B', 'C' -> "Satisfactory progress";
case 'D', 'F' -> {
String warning = "Academic review required";
yield warning;
}
default -> "Invalid grade";
};
}
// Returns a status string indicating whether the student passed.
static String getStatus(int score) {
return (score >= 60) ? "Passed" : "Failed";
}
public static void main(String[] args) {
int studentScore = 82;
char grade = getLetterGrade(studentScore);
String description = describeGrade(grade);
String status = getStatus(studentScore);
System.out.println("Score: " + studentScore);
System.out.println("Grade: " + grade);
System.out.println("Status: " + status);
System.out.println("Remarks: " + description);
}
}Score: 82
Grade: B
Status: Passed
Remarks: Satisfactory progressIn this program:
getLetterGradeuses anif-elseladder to classify a continuous numeric score into a letter grade character.describeGradeuses a switch expression with multi-label cases and ayieldblock to return a descriptive remark.getStatususes the ternary operator to return"Passed"or"Failed"based on the score threshold.maincoordinates the pipeline and prints the final academic evaluation.
Traps
Omitting break in classic switch statements
In classic switch statements, omitting
breakcauses execution to fall through into subsequent cases, running unrelated code unintentionally. Use modern switch expressions with arrow syntax->to eliminate fall-through entirely.✕ WRONGcase 1: doAction();✓ RIGHTcase 1: doAction(); break;Using assignment = instead of equality == in a condition
For numeric types,
if (count = 5)triggers a compiler error because an integer cannot convert to a boolean. Withbooleanvariables, however,if (isValid = false)assignsfalsetoisValidand evaluates tofalse, compiling cleanly but introducing a silent logic bug. Writeif (!isValid)instead.✕ WRONGif (isValid = false)✓ RIGHTif (isValid == false)Comparing strings with == instead of equals
The
==operator tests whether two variables point to the same object. It does not compare text content. Comparing strings with==causes unpredictable failures. String comparison and the string pool are covered in note 10, strings.✕ WRONGif (command == "start")✓ RIGHTif (command.equals("start"))Dangling else binding to the wrong if statement
Without curly braces, an
elsekeyword binds to the nearest precedingifstatement regardless of your indentation. Always enclose everyifandelsebody in explicit curly braces{}to avoid inverted logic bugs.✕ WRONGif (ready) if (hasAccess) run(); else reject();✓ RIGHTif (ready) { if (hasAccess) run(); } else { reject(); }Omitting default in a switch expression
Switch expressions must be exhaustive because they evaluate to a value. For open-ended types such as
intorString, omitting thedefaultbranch produces a compile error:the switch expression does not cover all possible input values.✕ WRONGint fee = switch (tier) { case 1 -> 5; };✓ RIGHTint fee = switch (tier) { case 1 -> 5; default -> 0; };Nesting ternary operators and destroying readability
Chaining multiple
? :operators obscures operator precedence and creates code that is difficult to trace. Use standardif-elseblocks or a switch expression whenever selecting among three or more outcomes.✕ WRONGint score = a > b ? c > d ? 10 : 20 : 30;✓ RIGHTint score; if (a > b) { score = (c > d) ? 10 : 20; } else { score = 30; }