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, truncatesSystem.out.println(7 / 2.0); // 3.5 โ one double promotes the expressionSystem.out.println(7 % 2); // 1 โ remainderInteger 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 landdouble avgRight = (double) total / count; // 3.4 โ cast first, then divideThe 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 objectsSystem.out.println(a.equals(b)); // true โ same charactersBurn 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'tString 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 happenswitch (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
- Cast before dividing when you want a decimal result.
.equals()for objects,==for primitives โ no exceptions until you deliberately need identity.- Put the null-check on the left of
&&. - Braces always; let the formatter police it.
- Arrow switch over colon switch in all new code; prefer exhaustive switches over
defaultfor enums. - For-each unless you need the index.
- 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.