Technology  /  Java

โ˜• Java 15 guides ยท updated 2026

From the JVM to streams, generics, and concurrency โ€” the language behind most enterprise backends, taught with modern Java in mind.

Java Operators and Control Flow: From if/else to Modern Switch Expressions

Control flow is where programs stop being calculators and start making decisions. Javaโ€™s tools here look familiar if youโ€™ve seen any C-family language โ€” but there are Java-specific behaviors (integer division, string equality, the modern switch) that separate people who write Java from people who guessed their way through it.

This guide covers operators, branching, loops, and the switch expression introduced in Java 14 that older tutorials still donโ€™t teach.


Arithmetic Operators โ€” and the Division Trap

The basics: +, -, *, /, % (remainder). One behavior causes more beginner bugs than the rest combined:

System.out.println(7 / 2); // 3 โ€” int / int stays int, truncates
System.out.println(7 / 2.0); // 3.5 โ€” one double promotes the expression
System.out.println(7 % 2); // 1 โ€” remainder

Integer division truncates toward zero. It does not round. If youโ€™re computing an average and getting suspiciously clean numbers, this is why:

int total = 17, count = 5;
double avgWrong = total / count; // 3.0 โ€” division already happened in int land
double avgRight = (double) total / count; // 3.4 โ€” cast first, then divide

The remainder operator % is more useful than it looks โ€” even/odd checks (n % 2 == 0), cycling through indexes (i % array.length), extracting digits (n % 10). One subtlety: with negative operands the sign follows the dividend โ€” -7 % 2 is -1, not 1. If you need a true always-positive modulus, use Math.floorMod(-7, 2) โ†’ 1.

Increment/decrement come in two flavors:

int i = 5;
System.out.println(i++); // prints 5, then i becomes 6 (post-increment)
System.out.println(++i); // i becomes 7, then prints 7 (pre-increment)

Honest advice: never write code where the difference matters. i++ on its own line, always. Cleverness with arr[i++] = i++ is how you fail code review.


Comparison and the String Equality Rule

==, !=, <, >, <=, >= work as expected on primitives. On objects, == compares references โ€” do they point at the same object โ€” which is almost never what you want:

String a = "hello";
String b = new String("hello");
System.out.println(a == b); // false โ€” different objects
System.out.println(a.equals(b)); // true โ€” same characters

Burn this in: == for primitives, .equals() for objects. The cruel part is that == on strings sometimes works (identical literals are pooled into one object), so the bug hides during testing and surfaces with runtime-constructed strings. Same story for Integer outside the โˆ’128..127 cache โ€” covered in the data types guide.


Logical Operators and Short-Circuiting

&& (and), || (or), ! (not) โ€” with a property youโ€™ll actively exploit: short-circuit evaluation. Java stops evaluating as soon as the answer is known:

if (user != null && user.isActive()) { ... }

If user is null, the right side never runs โ€” no NullPointerException. This null-check-then-use pattern is idiomatic Java; the ordering is load-bearing. Flip the operands and it crashes.

Same idea guards expensive work:

if (cache.contains(key) || database.lookup(key) != null) { ... }
// the DB call only happens on a cache miss

(You may meet & and | as non-short-circuit logical operators or bitwise operators. In application code, their appearance in a condition is usually a typo for &&/||.)


The Ternary Operator

A compact if/else that produces a value:

String label = score >= 50 ? "pass" : "fail";

Great for simple two-way choices, especially in assignments and return statements. Terrible when nested:

// please don't
String grade = s > 90 ? "A" : s > 80 ? "B" : s > 70 ? "C" : "F";

Three or more branches? Use if/else or โ€” better for fixed sets โ€” the switch expression below.


if / else if / else

Nothing exotic, but two Java-specific notes:

if (temperature > 30) {
System.out.println("Hot");
} else if (temperature > 20) {
System.out.println("Pleasant");
} else {
System.out.println("Cold");
}

First, conditions must be actual booleans. if (count) is a compile error in Java, not a truthiness check. This annoys Python and JavaScript people for a day and then quietly prevents a class of bugs forever. The classic C bug if (x = 5) (assignment instead of comparison) also refuses to compile, because x = 5 evaluates to an int, not a boolean.

Second, always use braces, even for one-liners. The infamous Apple goto fail SSL vulnerability was an unbraced-if bug in C. Java teams almost universally mandate braces; let your formatter enforce it.


Modern Switch: Expressions, Arrows, No Fall-Through

Old-style switch (youโ€™ll still see it in legacy code) had a design flaw: forgetting break silently falls through to the next case:

// old style โ€” fall-through bug waiting to happen
switch (day) {
case "SAT":
System.out.println("Weekend");
break; // forget this, and SAT also prints "Weekday"
case "MON":
System.out.println("Weekday");
break;
}

Java 14 finalized switch expressions with arrow syntax โ€” no fall-through, multiple labels per case, and the whole thing returns a value:

String type = switch (day) {
case "SAT", "SUN" -> "Weekend";
case "MON", "TUE", "WED", "THU", "FRI" -> "Weekday";
default -> "Unknown";
};

This is strictly better: itโ€™s shorter, break-free, and the compiler forces you to handle every case (via default, or exhaustively for enums and sealed types โ€” no default needed when all cases are covered, and adding a new enum constant then becomes a compile error at every switch that misses it; thatโ€™s a refactoring safety net, so prefer exhaustive switches over lazy defaults).

Multi-statement branches use a block with yield:

int fee = switch (tier) {
case "GOLD" -> 0;
case "SILVER" -> {
int base = 50;
yield base - discount;
}
default -> 100;
};

Java 21 extends switch further with pattern matching โ€” matching on types and destructuring records โ€” which we touch in the records guide. For now: if youโ€™re writing a new switch, use the arrow form.


Loops

while and do/while

while (queue.hasWork()) {
queue.processNext();
}
do {
input = scanner.nextLine(); // runs at least once
} while (!input.equals("quit"));

while checks first (may run zero times); do/while checks after (runs at least once โ€” natural for โ€œprompt until valid inputโ€). In practice do/while is rare; donโ€™t force it.

The classic for loop

for (int i = 0; i < 5; i++) {
System.out.println("i = " + i);
}

Init, condition, update โ€” all visible on one line, with i scoped to the loop. Use it when you genuinely need the index: iterating two arrays in lockstep, stepping by 2, counting down.

The enhanced for loop (for-each)

When you just want each element, skip the index bookkeeping:

List<String> cities = List.of("Delhi", "Mumbai", "Pune");
for (String city : cities) {
System.out.println(city);
}

Read : as โ€œinโ€. This should be your default loop โ€” no off-by-one errors possible, works on arrays and anything Iterable. Its limits: no index available, canโ€™t remove elements mid-loop (that throws ConcurrentModificationException โ€” use an Iterator or removeIf for that), canโ€™t iterate two collections in parallel.

break, continue, and labeled loops

for (Order order : orders) {
if (order.isCancelled()) continue; // skip to the next iteration
if (order.isFraud()) break; // abandon the loop entirely
process(order);
}

For nested loops, break only exits the innermost one. Javaโ€™s answer is a labeled break:

outer:
for (int i = 0; i < rows; i++) {
for (int j = 0; j < cols; j++) {
if (grid[i][j] == target) break outer; // exits both loops
}
}

Labels are legal and occasionally the cleanest option, but if you reach for one, first ask whether extracting the nested loops into a method with return reads better. It usually does.


Putting It Together: FizzBuzz, Then Better FizzBuzz

The interview classic, with the standard solution:

for (int i = 1; i <= 100; i++) {
if (i % 15 == 0) System.out.println("FizzBuzz");
else if (i % 3 == 0) System.out.println("Fizz");
else if (i % 5 == 0) System.out.println("Buzz");
else System.out.println(i);
}

The one insight worth remembering: order matters โ€” the % 15 check must come first, because if/else if chains take the first matching branch, and any multiple of 15 also matches % 3. A surprising number of candidates put % 3 first and canโ€™t see why 15 prints โ€œFizzโ€. Thatโ€™s the whole lesson of if/else if in one bug.

A more Java-flavored variant returning a value with a switch expression:

static String fizzbuzz(int i) {
return switch ((i % 3 == 0 ? 1 : 0) + (i % 5 == 0 ? 2 : 0)) {
case 1 -> "Fizz";
case 2 -> "Buzz";
case 3 -> "FizzBuzz";
default -> String.valueOf(i);
};
}

Not necessarily better โ€” but it shows conditions computing values instead of steering print statements, which is the mental shift that streams (a few chapters ahead) will complete.


Three Small Exercises That Expose Big Misunderstandings

Each of these takes five minutes and each targets a specific mental model. Write them without running first; then run and compare.

1. The grade banding function. Write String band(int marks) returning โ€œdistinctionโ€ (โ‰ฅ75), โ€œfirstโ€ (โ‰ฅ60), โ€œsecondโ€ (โ‰ฅ50), โ€œpassโ€ (โ‰ฅ35), โ€œfailโ€ (below). First with if/else-if, then as a single switch-free chain of ternaries, then decide which youโ€™d defend in code review and why. (Most people land back on if/else-if โ€” the exercise is feeling why the ternary chain fights the reader.)

2. The digit summer. int digitSum(int n) โ€” sum the digits of a non-negative number using only % and /. (1234 โ†’ 10.) This forces the two integer operators to work together: n % 10 peels the last digit, n / 10 discards it, and the loop runs while n > 0. If you reflexively converted to a String, do it again arithmetically โ€” the numeric version is what interviews and embedded thinking expect.

3. The leap-year predicate. boolean isLeap(int year): divisible by 4, except centuries, unless divisible by 400. One boolean expression, no ifs: year % 4 == 0 && (year % 100 != 0 || year % 400 == 0). The exercise is operator precedence and parenthesization under pressure โ€” and confirming your version against a few known years (2000: leap, 1900: not, 2024: leap).

Frequently Asked Questions

Does Java have an exponent operator? No โ€” ^ is bitwise XOR, a classic trap (2 ^ 3 is 1, not 8). Use Math.pow(2, 3), which returns double, or a loop for integer powers where precision matters.

Why does switch on String work but on long not? Historical implementation choices: switch supports int-compatible types (byte, short, char, int), enums, and String (compiled via hashCode + equals). long, double, and arbitrary objects need if/else or โ€” on modern Java โ€” pattern-matching switch, which extends the reach to type patterns.

Is there a goto? The keyword is reserved but unusable. Labeled break/continue cover the legitimate cases (escaping nested loops); everything else that goto did, methods and early returns do better.

How do I loop a fixed number of times most idiomatically? The classic for (int i = 0; i < n; i++) remains the answer when the index is used; IntStream.range(0, n).forEach(...) exists but earns its place only inside stream-heavy code. Donโ€™t contort either way.

Habits That Will Serve You

  1. Cast before dividing when you want a decimal result.
  2. .equals() for objects, == for primitives โ€” no exceptions until you deliberately need identity.
  3. Put the null-check on the left of &&.
  4. Braces always; let the formatter police it.
  5. Arrow switch over colon switch in all new code; prefer exhaustive switches over default for enums.
  6. For-each unless you need the index.
  7. If a loop body exceeds a screen, extract a method. Control flow should be scannable.

Next in the series: Strings in Java โ€” immutability, the string pool, StringBuilder, and why + in a loop is a performance bug.