Conditionals, Looked At Closely
Three subtle things about if/else that decide whether your code does what you meant
There is a class of bug in Java that you can stare at for five minutes before you see it. The code compiles. The code runs. It just does something you did not intend. And almost always, the cause is something small: the braces are not where you thought they were, or the conditions are checked in the wrong order, or half of a compound check silently never ran.
In a nutshell
Three ideas do almost all of the work when you are writing non-trivial conditional code, and each one has an associated “silent bug” that you will hit at some point this quarter.
Chains pick one path, and only one. A chain of else if runs exactly one branch. Separate if statements run every branch whose condition is true. Order matters: check the more restrictive condition first, or the specific branch becomes unreachable.
Braces decide what the if is holding onto. Without braces, an if controls exactly the next single statement. A stray semicolon after if (...) ends the if silently. When ifs nest without braces, an else binds to the nearest preceding unmatched if, regardless of indentation.
&& and || skip their right side on purpose. When the left side of && is false, the right side is not evaluated. When the left side of || is true, the right side is not evaluated. This is not an optimization — it is a feature you rely on to write safe guards like s != null && s.equals(...).
When you are done with this lesson you will be able to trace an else-if chain by hand, spot the dangling-else pitfall when you see it, and write null-safe guards with short-circuit &&.
Quick reference
Chain shapes
| Shape | When to use |
|---|---|
if (...) { } |
A single optional action (with or without a follow-up else). |
if (...) { } else if (...) { } ... else { } |
Mutually exclusive branches. Exactly one runs. |
Separate if statements, no else |
Independent checks. Each one runs if its condition is true; none affect the others. |
Rule: in an ascending else-if chain of ranges, check the most restrictive (largest threshold) first. Reversing the order makes the larger branch unreachable.
Short-circuit evaluation
| Expression | When the right side runs |
|---|---|
A && B |
only if A is true |
A || B |
only if A is false |
Idiom: put the null check or the bounds check on the left so the right side is safe.
if (s != null && s.equals("quit")) { ... } // safe
if (i < arr.length && arr[i] > 0) { ... } // safe
De Morgan’s laws (for negating compounds)
| Original | Equivalent negation |
|---|---|
!(A && B) |
!A || !B |
!(A || B) |
!A && !B |
Worked example. “Accept ages 13 to 19 inclusive” is age >= 13 && age <= 19. Negating that — “reject everyone outside that range” — is age < 13 || age > 19. Both the && flipped to ||, and each comparison flipped to its opposite. That pattern is De Morgan’s at work, and it is a quick way to rewrite an acceptance check into a rejection check (which is often how validation guards are written — see lesson 3-c).
Dangling else — always use braces
Without braces, else binds to the nearest preceding unmatched if. Indentation does not change this rule. The fix is to always wrap branches in { }, even single-statement ones.
Deep dive
1. Chains pick one path, and only one
Consider a grading table:
90 and up → A
80 to 89 → B
70 to 79 → C
60 to 69 → D
below 60 → F
A natural first attempt, written by someone who has only seen if and not yet else if:
String grade = "F";
if (score >= 60) { grade = "D"; }
if (score >= 70) { grade = "C"; }
if (score >= 80) { grade = "B"; }
if (score >= 90) { grade = "A"; }
Trace this with score = 95. The first if is true, so grade becomes “D”. The second is also true, so “C”. The third true, so “B”. The fourth true, so “A”. Four separate statements, final answer happens to be right.
Now try score = 85. First: “D”. Second: “C”. Third: “B”. Fourth false, skip. Answer: “B”. Also right.
So far, so good. The bug hides in the ordering. Reverse it:
String grade = "F";
if (score >= 90) { grade = "A"; }
if (score >= 80) { grade = "B"; }
if (score >= 70) { grade = "C"; }
if (score >= 60) { grade = "D"; }
Trace score = 95 again. First: “A”. Second: “B”. Third: “C”. Fourth: “D”. Final answer: D. The grade is wrong because each if overwrites the previous one. These four statements are independent. They all run. The order of writing to grade determines the winner.
The fix is to tell Java these branches are mutually exclusive:
String 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"; }
Now exactly one assignment happens. Java checks conditions top to bottom. The first true condition wins; the rest are skipped. Order of writing to grade stops mattering because only one write ever happens.
One detail worth noticing: the chain ends with a bare else. Try dropping it — Java will refuse to compile the program that tries to use grade afterward. The compiler cannot prove that grade was assigned on every possible path through the code, and Java’s definite assignment rule demands that proof. The terminal else is how you satisfy it: since one of the branches must run, grade is always assigned. This is why the chain-with-else pattern is so common — it is structurally honest about the fact that every score maps to exactly one tier.
This is the real difference between separate if statements and an else if chain. Separate ifs are independent decisions — all of them run, all of them can fire. An else if chain is one decision with multiple possible outcomes.
The other ordering trap. Even in a proper else if chain, the order of conditions still matters, but differently. Flip the first two:
if (score >= 80) { grade = "B"; }
else if (score >= 90) { grade = "A"; }
...
Now score = 95 passes >= 80 first, gets “B”, and the chain stops. The score >= 90 branch is unreachable. No error, no warning — just wrong answers, forever.
Rule of thumb: in an else if chain of >= ranges, check the largest threshold first. In a chain of <= ranges, check the smallest first. Generally, check the more restrictive condition first.
2. Braces decide what the if is holding onto
Java lets you write an if without braces, if the body is a single statement:
if (x > 0)
System.out.println("positive");
That compiles. It runs. It does what it looks like. So what’s the problem?
The problem is that indentation is for you, not for the compiler. When you come back later and add another print:
if (x > 0)
System.out.println("positive");
System.out.println("done");
Your eye groups the two print statements under the if. Java does not. Without braces, the if controls exactly the next statement — one println. The second line is outside the if and runs unconditionally. Try x = -5: nothing gets filtered, done still prints.
Always using braces makes this impossible:
if (x > 0) {
System.out.println("positive");
System.out.println("done");
}
Now the block is explicit. Any code you add between the braces is protected by the condition.
The semicolon trap. Closely related, harder to spot:
if (x > 0);
{
System.out.println("inside");
}
The ; right after ) is an empty statement. It is what the if is controlling. The { ... } block that follows is not attached to the if at all — it is a standalone block that runs regardless. Result: inside prints every time. Compiler is happy. You are confused.
Dangling else. Now nested ifs without braces:
if (a > 0)
if (a > 10)
System.out.println("big");
else
System.out.println("???");
Read that carefully. The indentation suggests the else is paired with a > 0. It is not. Java’s rule: else binds to the nearest preceding unmatched if — which is a > 10, not a > 0. For a = 5, this code prints ??? (because a > 0 is true but a > 10 is false). For a = -3, it prints nothing (the whole block is skipped, including the else).
Braces make the real structure visible:
if (a > 0) {
if (a > 10) {
System.out.println("big");
} else {
System.out.println("small positive");
}
}
Indentation is for humans. Braces are for the compiler. Use both.
3. && and || are lazy on purpose
One vocabulary word first. A reference variable — any variable of a non-primitive type, like String — can hold the special value null, meaning “no object attached.” Calling a method on a null reference is illegal; Java throws a NullPointerException at runtime and your program stops. This comes up all the time in real Java, and it is the reason for the pattern in this section.
Suppose s is a String variable that might be null (say, an optional command-line argument the user did not supply). Look at this line:
if (s.equals("quit")) { ... }
s.equals(...) is a method call on s. When s is null, that call throws NullPointerException. The program crashes.
You might try to guard against it:
if (s.equals("quit") && s != null) { ... }
Still crashes. Java evaluates s.equals(...) first because it is on the left — that is where the exception happens. The null check on the right never gets a chance to save you.
Swap the order:
if (s != null && s.equals("quit")) { ... }
Now it works, and the reason is the topic of this section. Java’s && and || are short-circuit operators. They evaluate left to right, and they stop as soon as the answer is determined.
For &&: the moment the left side is false, the whole expression is false regardless of the right side. Java does not bother evaluating the right. So in s != null && s.equals(...), if s is null, the left is false, and s.equals(...) is never touched. Safe.
For ||: the moment the left side is true, the whole expression is true regardless of the right side. The right side is skipped.
This is not a micro-optimization. It is a safety feature, and it changes how you write guards:
if (i < array.length && array[i] > 0) { ... }
// ^ bounds check ^ element access — safe, because
// bounds check short-circuits when out of range
if (denom != 0 && numer / denom > 1) { ... }
// ^ guard ^ division — safe, because
// denom=0 short-circuits the divide
In each case the left condition protects the right from ever being evaluated. That is the entire point.
Reflection. In your own words, explain why
s != null && s.equals(...)is safe buts.equals(...) && s != nullis not. What does short-circuit evaluation have to do with it, and what would change about writing defensive code if Java evaluated both sides of&&unconditionally?
NullPointerException when s might be null?&& short-circuits left to right. In B, when s is null, the left side s != null is false, so the right side is skipped — no crash. In A, the call s.equals(...) happens first and throws on null before the guard can save you. Order matters when one condition protects the other.
The cousin operators that don’t short-circuit. Java also has & and |. Those are actually bitwise operators — they do bit-level integer arithmetic (5 & 3 is 1, if that is meaningful to you yet) and are unrelated to boolean logic in spirit. On booleans they also happen to compute AND and OR, but without short-circuiting, so both sides always evaluate. Drop one & by accident in a null-guard and the safety disappears — s != null & s.equals("quit") evaluates both sides every time and crashes on null. The rule for CS1: use && and || for conditions. If a & or | shows up in your code, double-check that it was not a typo.
Before you leave
Three ideas. None of them are about syntax — they are all about what the compiler does with the syntax. A chain of else if runs exactly one branch; separate ifs run every one that matches. Braces decide what the if is holding onto, and the semicolon-after-if trap is the specific form of this bug that the compiler does not warn you about. && and || skip their right side on purpose, which is what makes s != null && s.equals(...) safe.
The week’s first lab uses all three ideas together — chain ordering, braces, and short-circuit. The compound-condition patterns also show up in the validation code in the follow-up lab.
Want to practice? Work through the Week 3 quiz-prep set on the practice platform.