javac does not care what your code looks like. Whitespace carries zero meaning in the Java Language Specification, so the compiler reads your file as a stream of tokens and skips every space you typed. Fine for the machine. A problem for you, because indentation is the part of the code that talks to humans. When the layout says one thing and the braces say another, the reader trusts the layout, and the bug ships.
Here are four bugs I have either shipped, reviewed, or nearly missed. Every snippet compiles, and in every one the indentation is lying.
- The extra semicolon that turns a condition into a no-op
java
if (cart.total() > FREE_SHIPPING_MIN);
applyFreeShipping(cart);
chargeCustomer(cart);
The layout says: carts above the threshold get free shipping. The semicolon right after the condition says otherwise. A lone semicolon is a complete, empty statement, so it becomes the entire if body, and applyFreeShipping runs for every order, threshold or not.
The same trap exists under for and while: for (Item item : items); is a loop that iterates over everything and does nothing, then the indented line below it runs once. Static analysis knows this pattern (SonarQube rule S1116, IDE inspections flag it), but only when someone runs it.
Fixed:
java
if (cart.total() > FREE_SHIPPING_MIN) {
applyFreeShipping(cart);
}
chargeCustomer(cart);
- The dangling else that picks the wrong if
java
if (customer.isVip())
if (order.isGiftWrapped())
includeGiftCard(order);
else
chargeUpgradeFee(order);
Read it the way the indentation suggests: VIP orders get a gift card, everyone else pays an upgrade fee. Java binds else to the nearest if, so the fee is actually charged to VIP customers whose order has no gift wrap. Non-VIP customers never reach the else at all.
java
if (customer.isVip()) {
if (order.isGiftWrapped()) {
includeGiftCard(order);
} else {
chargeUpgradeFee(order);
}
}
Same tokens, different program. Braces settle ownership of the else. Indentation never can.
- The closing brace that ends the block one line early
java
if (!response.isOk()) {
log.warn("bad response, will retry");
}
retry(request);
This reads as "log and retry on failure", but the brace after the log line already closed the if. retry runs on every request, success or not. The only thing claiming it is conditional is the indentation, and indentation has no vote.
This one shows up after refactors: someone deletes a line, the closing brace lands one row too high, and nobody re-formats the block. With the brace parked at the right margin and the layout untouched, code review walks right past it.
java
if (!response.isOk()) {
log.warn("bad response, will retry");
retry(request);
}
- The unbraced guard chain, a.k.a. the goto fail shape
java
if (session == null)
return false;
if (!session.signatureValid())
return false;
revokeSession(session.userId());
return grantAccess(session);
Every line under the guards looks like part of the chain. Only the first two are. revokeSession belongs to no if at all, so it runs on every login attempt, including successful ones, right before access is granted. When the session is null it does not just run pointlessly, it throws.
The shape has a famous ancestor: CVE-2014-1266, the Apple goto fail bug. One duplicated line under an unbraced if chain in C skipped certificate verification on every SSL connection in iOS for over a year. Different language, same mechanism: unbraced guards plus a little drift equals a security bug that compiles.
java
if (session == null) {
return false;
}
if (!session.signatureValid()) {
revokeSession(session.userId());
return false;
}
return grantAccess(session);
Why Java puts up with this
Python made indentation part of the syntax, so the layout cannot lie. Java moved all structure into braces and semicolons and left the layout as free space. The trade is that you can format code any way you like, including ways that mislead. Indentation ends up working like a comment written in whitespace, and comments rot.
Let a machine own the layout
The fix for all four bugs is not reading harder. People read less carefully under deadline, not more. The fix is giving ownership of layout to a formatter so no human re-indents by hand and drift cannot survive:
Format on save: IntelliJ save actions, or format on save in VS Code. The file stays in canonical shape, so a misplaced brace becomes visible immediately.
Enforce it in CI: Spotless, Checkstyle, or fmt-maven-plugin failing the build. Drift stops at the pull request instead of production.
Quick one-offs: for a snippet from a code review or a machine without your IDE configured, paste it into JavaFmt, a free online Java formatter. It runs in the browser, code never leaves the tab, and it applies the classic four-space conventions.
If you want the longer background: does indentation matter in Java covers why teams standardize at all, and Java line wrapping rules covers the part of the layout that formatters still argue about.
Three habits, in order of impact: always brace single statements, format on save, fail the build on drift. The compiler will never care about your indentation. Make something that does.
Top comments (0)