JAVA / OPERATORS
Logical operators and short-circuit evaluation
Combine boolean tests with &&, ||, and !, and predict exactly which operands Java evaluates so a guard protects the code beside it.
What you will learn
- Predict which operands run: && stops at the first false, || at the first true
- Put a null or bounds check on the left of && to guard the call on the right
- Pick & or | on booleans when both operands must run for their side effects
- Spot bugs where a mutating call sits on the skipped side of && or ||
Understanding Logical operators and short-circuit evaluation
In Java, &&, || and ! accept boolean operands only and produce a boolean. There is no implicit truthiness, so if (count) does not compile the way it would in C or Python; you write if (count > 0). The single-character &, | and ^ also accept two booleans and return the same boolean answers as their two-character cousins, which is exactly why a beginner can type & instead of && and still get code that compiles cleanly.
The real difference is that && and || are branching hidden inside an expression, not functions of two values. Read a && b as: evaluate a; if it is false the answer is already false, so b is never evaluated at all; otherwise the answer is whatever b evaluates to. || mirrors that, stopping the moment an operand is true. The skipped operand is not computed and discarded, it never runs, so any method call, increment, or exception waiting inside it simply does not happen.
That turns the left operand into a guard for everything to its right, which is how s != null && s.isEmpty() stays safe and i < a.length && a[i] > 0 avoids an index exception. Order therefore becomes part of correctness rather than style: swapping the operands can turn working code into a crash even though the logical meaning looks unchanged. Reach for & or | on booleans only when you deliberately want both sides evaluated, for instance when each side records something you need no matter how the test comes out.
public class ShortCircuitDemo {
static boolean report(String label, boolean value) {
System.out.println(" ran " + label + " -> " + value);
return value;
}
public static void main(String[] args) {
System.out.println("false && true:");
System.out.println(" result " + (report("left", false) && report("right", true)));
System.out.println("true || false:");
System.out.println(" result " + (report("left", true) || report("right", false)));
System.out.println("false & true:");
System.out.println(" result " + (report("left", false) & report("right", true)));
}
}&& and || evaluate their right operand only when the left one has not already decided the result, so the left operand acts as a guard for the right.
Worked examples
Null check as a guard
Shows how the left operand of && keeps a method call from running on a null reference.
public class NullGuard {
static boolean startsWithA(String s) {
return s != null && s.startsWith("A");
}
public static void main(String[] args) {
System.out.println(startsWithA("Apple"));
System.out.println(startsWithA("Banana"));
System.out.println(startsWithA(null));
}
}Example explained
Line 1s != null is evaluated first; for the null argument it is false, so the whole && is already false.
Line 2s.startsWith("A") is therefore never called on null, and that is the only reason no NullPointerException occurs.
Line 3"Banana" passes the null check, so the second operand does run and reports false.
Line 4Replacing && with & would call startsWith on every invocation, and the third line would throw instead of printing false.
Side effect on the skipped side
Compares && and & when the right operand mutates state.
public class SideEffects {
static int calls = 0;
static boolean touch() {
calls++;
return true;
}
public static void main(String[] args) {
boolean ready = false;
if (ready && touch()) {
System.out.println("body with &&");
}
System.out.println("calls after && : " + calls);
if (ready & touch()) {
System.out.println("body with &");
}
System.out.println("calls after & : " + calls);
}
}Example explained
Line 1ready is false, so && settles the result immediately and touch() never executes, leaving calls at 0.
Line 2& has no short circuit, so touch() runs anyway and pushes calls to 1.
Line 3Neither if body prints, because both conditions are false; the operators differ only in what was evaluated on the way to that answer.
Line 4Since the boolean result is identical, nothing will flag the swap for you: only the counter reveals it.
Bounds check before indexing
Demonstrates that the order of the two operands decides whether an out-of-range index is ever read.
public class BoundsOrder {
public static void main(String[] args) {
int[] data = {5, 8, 13};
for (int i = 0; i <= data.length; i++) {
boolean big = i < data.length && data[i] > 10;
System.out.println("i=" + i + " big=" + big);
}
}
}Example explained
Line 1The loop condition uses <= on purpose so that i reaches 3, one past the last valid index.
Line 2At i == 3 the guard i < data.length is false, so data[3] is never evaluated and no ArrayIndexOutOfBoundsException is thrown.
Line 3At i == 2 the guard passes, the index expression runs, and 13 > 10 yields true.
Line 4Writing data[i] > 10 && i < data.length reads the element before testing the bound, so the last iteration would crash.
Important notes
&& binds tighter than ||, so a || b && c means a || (b && c); add parentheses when you mix them, or the guarding will not sit where you expect.
There is no short-circuit XOR: ^ needs both boolean operands to know the answer, so Java has no ^^ operator.
Common mistakes
Guarding on the wrong side, as in data[i] > 10 && i < data.length: the element is read first, so an out-of-range i still throws ArrayIndexOutOfBoundsException.
Typing & in a null check, as in s != null & s.startsWith("A"): it compiles and returns a boolean, but startsWith runs on null and throws NullPointerException.
Hiding count++ or a queue-consuming call on the right of && or ||, which produces wrong totals because that operand runs only on some inputs.
Try it yourself
Change, predict, then run
Write boolean isVowelAt(String s, int i) that returns s != null && i >= 0 && i < s.length() && "aeiou".indexOf(s.charAt(i)) >= 0, then call it with (null, 0), ("java", 1) and ("java", 9). Swap the last two conditions and note which call now throws.
Open the Java workspaceCheck your understanding
A helper load(k) inserts an entry into a cache and returns true. Code reads: if (cache.containsKey(k) || load(k)) { use(k); }. A teammate changes || to | because "they mean the same thing". What is the effect?
- load(k) now runs on every call, so a key that was already cached gets reloaded and overwritten each time
- Nothing changes, because | and || produce the same boolean result
- The expression becomes a bitwise int operation and no longer compiles
- containsKey(k) is skipped whenever load(k) returns true
Show answer
| yields the same boolean as ||, but it evaluates both operands unconditionally, so load's side effect happens even on a cache hit and the fast path disappears. Option 1 is tempting precisely because the returned value is identical; the difference lies in what gets evaluated, and a side-effecting right operand makes that difference observable.