The examples on this page are compact source files: a void main() method with IO.println, which you can save as Main.java and run with java Main.java. The decisions work exactly the same inside an ordinary class with public static void main(String[] args).

Conditions are boolean

Every decision depends on a condition: an expression that is true or false. The general idea of selection is the same in every language. In Java, a condition is an expression of type boolean, usually a comparison (<, >=, ==, !=) or several comparisons joined with && (and), || (or) and ! (not). && and || stop as soon as the answer is known (see variables, types and operators).

void main() {
    int openTickets = 3;
    boolean needsAttention = openTickets > 0;

    IO.println(needsAttention);
    IO.println(openTickets > 5);
    IO.println(openTickets > 0 && openTickets < 10);
}
true
false
true

Java is strict about this. A number, a string or an object is never accepted where a boolean is expected, so the compiler stops you before the program runs:

void main() {
    int openTickets = 3;
    if (openTickets) {
        IO.println("Work to do");
    }
}
Main.java:3: error: incompatible types: int cannot be converted to boolean

The same strictness catches most cases of the classic typo = (assignment) instead of == (comparison). if (status = "Open") assigns a String, which is not a boolean, so it does not compile. There is one exception, and it is a real trap: when the variable itself is a boolean, the assignment is a valid condition.

void main() {
    boolean escalated = false;

    if (escalated = true) {
        IO.println("Paging the incident manager");
    }
    IO.println("escalated is now " + escalated);
}
Paging the incident manager
escalated is now true

if, else if and else

An if statement runs a block when its condition is true. The condition always sits in parentheses and the block in braces { }; indentation means nothing to the compiler. An optional else block runs when the condition is false, and else if adds more alternatives. Java tests the conditions from top to bottom and runs only the block of the first condition that is true.

void main() {
    double temperature = 31.5;

    if (temperature >= 30) {
        IO.println("Heat alert: check the transformer cooling.");
    } else if (temperature <= 0) {
        IO.println("Frost alert: check the outdoor meters.");
    } else {
        IO.println("Normal temperature.");
    }
}
Heat alert: check the transformer cooling.

There is no elif keyword: else if is an else whose block happens to be another if statement. Because the first true condition wins, put the narrowest test first, as in any else-if chain.

Always use braces

When a block contains a single statement, Java allows you to leave out the braces. Only that one statement then belongs to the if; the next line runs every time, however it is indented.

void main() {
    int usageKwh = 120;

    if (usageKwh > 500)
        IO.println("High usage this month.");
        IO.println("Sending usage warning to the customer.");
}
Sending usage warning to the customer.

The warning is sent although usage is far below the limit. Most Java style guides therefore require braces around every block, even a one-line one, and tools such as Checkstyle can enforce that (see testing and code quality).

The conditional operator

When an if/else only chooses between two values, the conditional operator ?: (often called the ternary operator, because it has three operands) is shorter. condition ? valueIfTrue : valueIfFalse is an expression, so it can stand anywhere a value is expected.

void main() {
    int stock = 0;
    String status = stock > 0 ? "in stock" : "sold out";
    IO.println(status);

    int tickets = 1;
    IO.println(tickets + (tickets == 1 ? " ticket" : " tickets") + " open");

    int score = 85;
    String grade = score >= 90 ? "A" : score >= 80 ? "B" : "C";
    IO.println(grade);
}
sold out
1 ticket open
B

Note the parentheses around the conditional operator inside the string concatenation: without them, + would be applied first. Nested conditional operators, as in the last example, work but quickly become hard to read; a switch expression (below) or an if chain is usually clearer.

The classic switch statement

A switch statement compares one value against a list of case labels and jumps to the one that matches. The optional default label is used when nothing matches. Since Java 14, one label can list several values separated by commas.

void main() {
    String priority = "P2";

    switch (priority) {
        case "P1":
            IO.println("Respond within 15 minutes.");
            break;
        case "P2", "P3":
            IO.println("Respond within 4 hours.");
            break;
        default:
            IO.println("Respond on the next working day.");
    }
}
Respond within 4 hours.

A switch works on int, char, short and byte (and their wrapper classes), on String and on enums. With patterns (below) it also works on any other reference type. A long, double, float or boolean selector is rejected in Java 25 with error: primitive patterns are a preview feature: switching on those types is still a preview feature in JDK 25–27.

Fall-through

With colon labels (case X:), a case is only an entry point. Once Java has jumped to a matching label, it runs every statement below it, through the following labels, until it meets a break or the end of the switch. This behavior, inherited from C, is called fall-through, and a forgotten break is one of the best-known bugs in Java code.

void main() {
    Status status = Status.IN_PROGRESS;

    switch (status) {
        case OPEN:
            IO.println("open");
            break;
        case IN_PROGRESS:
            IO.println("in progress");
            // break forgotten here
        case RESOLVED:
            IO.println("resolved (fell through)");
            break;
    }
}

enum Status { OPEN, IN_PROGRESS, RESOLVED }
in progress
resolved (fell through)

The ticket is in progress, but the program also reports it as resolved. The enum Status, declared below main, is explained on records, enums and sealed types; inside the switch its constants are written without the Status. prefix.

The position of default does not change this rule. In the next example default sits in the middle: a value that matches no label jumps to it and then falls through into the cases below it.

void main() {
    for (String priority : List.of("P1", "P9")) {
        IO.println("Priority " + priority + ":");
        switch (priority) {
            case "P1":
                IO.println("  page the on-call engineer");
            default:
                IO.println("  create a ticket");
            case "P2":
                IO.println("  notify the service desk");
                break;
            case "P3":
                IO.println("  add to the backlog");
        }
    }
}
Priority P1:
  page the on-call engineer
  create a ticket
  notify the service desk
Priority P9:
  create a ticket
  notify the service desk

Switch expressions

Most switches exist to pick a value: a response time for a priority, a price for a tariff. For that, Java 14 added the switch expression: a switch that produces a value, so it can be assigned, returned or passed as an argument. It ends with a semicolon like any other expression statement.

void main() {
    for (String priority : List.of("P1", "P2", "P3", "P4")) {
        int minutes = switch (priority) {
            case "P1" -> 15;
            case "P2" -> 60;
            case "P3" -> 240;
            default -> 1440;
        };
        IO.println(priority + ": respond within " + minutes + " minutes");
    }
}
P1: respond within 15 minutes
P2: respond within 60 minutes
P3: respond within 240 minutes
P4: respond within 1440 minutes

The for-each loop around it runs the switch once for each priority.

Arrow labels

An arrow case label, case X ->, is followed by exactly one thing: an expression, a block in braces, or a throw statement. Only that part runs; there is no fall-through and no break. Arrow labels work in switch expressions and in ordinary switch statements, which makes them the safer choice for new code even when no value is produced.

void main() {
    Status status = Status.IN_PROGRESS;

    switch (status) {
        case OPEN -> IO.println("open");
        case IN_PROGRESS -> IO.println("in progress");
        case RESOLVED -> {
            IO.println("resolved");
            IO.println("sending satisfaction survey");
        }
    }
}

enum Status { OPEN, IN_PROGRESS, RESOLVED }
in progress

One switch uses either colons or arrows, never both. Mixing them is a compile error.

yield: a value from a block

When one case of a switch expression needs several statements, put them in a block and finish it with a yield statement, which hands the value back to the switch expression. A return would try to leave the whole method instead, and is not allowed there.

void main() {
    String tariff = "Dynamic";
    int hour = 18;

    double pricePerKwh = switch (tariff) {
        case "Day" -> 0.32;
        case "Night" -> 0.24;
        case "Dynamic" -> {
            double base = 0.25;
            boolean peak = hour >= 17 && hour < 21;
            yield peak ? base * 1.5 : base;
        }
        default -> throw new IllegalArgumentException("Unknown tariff: " + tariff);
    };
    IO.println("Price per kWh: " + pricePerKwh);
}
Price per kWh: 0.375

A block that ends without yield (or throw) is a compile error:

void main() {
    int level = 2;
    String team = switch (level) {
        case 1 -> "Service desk";
        case 2 -> {
            IO.println("Escalating to second line");
        }
        default -> "Engineering";
    };
}
Main.java:7: error: switch rule completes without providing a value

yield also works in a switch expression with colon labels (case "Day": yield 0.32;), but that form falls through like the classic statement. Prefer arrows.

Exhaustiveness

A switch expression must produce a value for every input it could receive. This property is called exhaustiveness, and in Java it is checked by the compiler: if a value could slip through, the code does not compile. For a String or an int there are endless possible values, so you need a default.

void main() {
    String tariff = "Night";
    double pricePerKwh = switch (tariff) {
        case "Day" -> 0.32;
        case "Evening" -> 0.28;
    };
    IO.println(pricePerKwh);
}
Main.java:3: error: the switch expression does not cover all possible input values

For an enum, listing every constant is enough, and it is better than adding a default. If someone later adds a constant such as ON_HOLD to the enum, every switch expression that does not handle it stops compiling, so the compiler shows you each place to update. A default would silently swallow the new value.

void main() {
    for (Status status : Status.values()) {
        String label = switch (status) {
            case OPEN -> "Waiting for an engineer";
            case IN_PROGRESS -> "Being worked on";
            case RESOLVED -> "Fixed, waiting for confirmation";
        };
        IO.println(status + ": " + label);
    }
}

enum Status { OPEN, IN_PROGRESS, RESOLVED }
OPEN: Waiting for an engineer
IN_PROGRESS: Being worked on
RESOLVED: Fixed, waiting for confirmation

Exhaustiveness is required for switch expressions and for switch statements that use patterns or case null. A classic switch statement on a string, number or enum may still leave cases out; it then simply does nothing for them.

Pattern matching for instanceof

Sometimes a variable has a general type, such as Object, and you need to know what it really holds. The instanceof operator tests that at run time. Before Java 16 you then had to cast the value yourself. With pattern matching for instanceof, the test and the cast happen in one step: o instanceof String s is true when o holds a String, and in that case declares a variable s of type String.

void main() {
    Object[] fields = { "INC-10042", 350, "Amara Okafor" };

    for (Object field : fields) {
        if (field instanceof String s && s.startsWith("INC-")) {
            IO.println("ticket number " + s.substring(4));
        } else if (field instanceof Integer kwh) {
            IO.println("reading of " + kwh + " kWh, costs EUR " + kwh * 0.30);
        } else {
            IO.println("other: " + field);
        }
    }
}
ticket number 10042
reading of 350 kWh, costs EUR 105.0
other: Amara Okafor

The variable exists only where the compiler can prove the test succeeded. After && that is the case, so s.startsWith is safe. After || it is not, because the right-hand side also runs when the test failed:

void main() {
    Object field = 350;
    if (field instanceof String s || s.isEmpty()) {
        IO.println("text field");
    }
}
Main.java:3: error: cannot find symbol

The scope follows the flow of the program. A common style tests for the wrong type first and leaves the method early; after that if, the variable is available for the rest of the method.

void main() {
    IO.println(describe("INC-10042"));
    IO.println(describe(42));
}

String describe(Object value) {
    if (!(value instanceof String id)) {
        return "not a ticket id";
    }
    return "ticket " + id + " (" + id.length() + " characters)";
}
ticket INC-10042 (9 characters)
not a ticket id

null instanceof String is always false, so a pattern variable is never null. Writing your own methods such as describe is covered on methods and parameters.

Pattern matching for switch

Since Java 21, a case label can hold a pattern instead of a constant. A type pattern such as case Integer i matches when the value is an Integer and gives it a name, exactly like instanceof. The cases are tried from top to bottom and the first match wins.

Guards with when

A guard adds an extra condition to a pattern with the keyword when: the case only matches when the pattern matches and the boolean after when is true. A plain case null handles a null value, and default catches the rest.

void main() {
    Object[] values = { 5, 42, "INC-10042", null, 1.5 };
    for (Object value : values) {
        IO.println(kind(value));
    }
}

String kind(Object value) {
    return switch (value) {
        case null -> "missing value";
        case Integer i when i > 10 -> "large number " + i;
        case Integer i -> "small number " + i;
        case String s -> "text of " + s.length() + " characters";
        default -> "something else: " + value.getClass().getSimpleName();
    };
}
small number 5
large number 42
text of 9 characters
missing value
something else: Double

Order matters. A case that can never be reached because an earlier case already matches everything it would match is said to be dominated, and the compiler rejects it. Put the guarded, more specific case first:

void main() {
    Object value = 42;
    String label = switch (value) {
        case Integer i -> "number";
        case Integer i when i > 10 -> "large number";
        default -> "other";
    };
    IO.println(label);
}
Main.java:5: error: this case label is dominated by a preceding case label

null in a switch

Without a case null, a switch throws a NullPointerException as soon as the selector is null, even when there is a default. This was the rule before patterns existed, and it still is:

void main() {
    String priority = null;
    switch (priority) {
        case "P1" -> IO.println("urgent");
        default -> IO.println("normal");
    }
}
Exception in thread "main" java.lang.NullPointerException: Cannot invoke "String.hashCode()" because "<local2>" is null

The message says "<local2>" instead of a variable name. In source-file mode the compiler does not store the names of local variables in the class file, so the JVM can only report a slot number; here it is a hidden copy of priority that the compiler made for the switch. How to read these messages is explained on null, Optional and NullPointerException.

Sealed types and record patterns

Patterns become most useful together with two kinds of types from records, enums and sealed types. A sealed interface lists all the types that may implement it, so the compiler knows the switch below is exhaustive without a default. A record pattern such as Electricity(String id, double kwh) takes a record apart into its components, and a guard can use them right away.

void main() {
    List<Reading> readings = List.of(
            new Electricity("E-001", 350.5),
            new Electricity("E-002", 1500.0),
            new Gas("G-007", 12.25));

    for (Reading reading : readings) {
        String line = switch (reading) {
            case Electricity(String id, double kwh) when kwh > 1000 ->
                    id + ": high use, " + kwh + " kWh";
            case Electricity(String id, double kwh) -> id + ": " + kwh + " kWh";
            case Gas(String id, double m3) -> id + ": " + m3 + " m3";
        };
        IO.println(line);
    }
}

sealed interface Reading permits Electricity, Gas {}
record Electricity(String meterId, double kwh) implements Reading {}
record Gas(String meterId, double m3) implements Reading {}
E-001: 350.5 kWh
E-002: high use, 1500.0 kWh
G-007: 12.25 m3

Choosing the right construct

All of these constructs change the flow of a program; they differ in whether they run statements or produce a value, and in how much the compiler can check for you.

Construct Produces Best for
if / else nothing (runs statements) one or two conditions, or conditions on different variables
?: a value choosing between exactly two short values
switch statement with arrows nothing (runs statements) many cases on one value, each doing some work
switch expression a value mapping one value to a result, with the compiler checking that every case is covered
instanceof pattern a boolean (plus a variable) one type test followed by code that uses the value
classic switch with colons nothing (runs statements) existing code; deliberate fall-through (rare, and worth a comment)

A good rule of thumb: when every branch of an if/else if chain assigns the same variable, a switch expression says the same in fewer lines and lets the compiler check that no case is forgotten. When the same switch on a "kind" of object appears in many places, consider polymorphism instead, so that each type carries its own behavior.