Operators and precedence
How Java groups and evaluates expressions, from the precedence ladder to i = i++, shifts, short-circuiting and the hidden cast in compound assignment.
On this page
➗ What are operators and precedence?
Operators are the symbols that turn values into new values: +, %, ++, &&, >>>, ?: and friends. Precedence decides how an expression without parentheses is grouped, so 2 + 3 * 4 is 2 + (3 * 4) and gives 14, not 20. Precedence answers "which operator grabs which operand". A separate rule, evaluation order, answers "which operand runs first", and Java fixes that one too: left to right.
For this lesson, we'll go operator family by operator family, and at each stop we'll look at the one behaviour that ends up in interview puzzles. Interviewers love this topic because a three-line snippet like i = i++ exposes whether you know what the compiler and the JVM really do, or whether you're guessing from habit.
Below the line (out of scope)
- Widening and narrowing rules in depth live in Type casting and numeric promotion
==on objects and strings is covered in String equality
The 30-second answer
Java groups operands by a fixed precedence ladder (postfix and unary bind tightest, assignment and lambda loosest) and then evaluates operands strictly left to right (JLS 15.7). Arithmetic on byte, short and char is promoted to int. && and || short-circuit; & and | always evaluate both sides. >> keeps the sign, >>> fills with zeros. Compound assignment like s += 1 hides a cast. And i = i++ leaves i unchanged, because the old value is saved on the operand stack before the increment, then written back.
What you’ll learn
- Separate precedence (grouping) from evaluation order (always left to right, JLS 15.7)
- Explain why
i = i++leavesiunchanged using the operand stack and bytecode - Choose between
&&and&, and between>>and>>>, and predict their results - Spot the ternary promotion and unboxing traps, and the implicit cast in
s += 1 - Use
instanceofpattern matching (Java 16) with correct flow scoping
01Picture the precedence ladder first
Think of precedence as a ladder. Operators near the top grab their operands first, the way multiplication grabs its neighbours before addition gets a look. Associativity breaks ties on the same rung: most binary operators group left to right, so 10 - 2 - 3 is (10 - 2) - 3 = 5, while assignment and the ternary group right to left, so a = b = 5 assigns b first.
Here's the whole ladder on one card, with the four rungs that cause the most bugs called out on the right.
The Java precedence ladder from postfix (binds tightest) down to assignment and lambda (binds loosest), with common traps annotated
Two rungs deserve a second look. Equality (==) sits above bitwise &, so x & 1 == 0 means x & (1 == 0) and does not compile for an int. And shift sits below addition, so 2 << 1 + 1 is 2 << 2 = 8. When in doubt, add parentheses: they cost nothing at runtime and save the next reader a trip to the spec.
02Under the hood: arithmetic, increments and the operand stack
Start with the arithmetic operators, because they carry three rules people forget:
- Promotion.
byte,shortandcharoperands becomeintbefore any arithmetic.longwins overint,floatoverlong,doubleover everything. - Overflow wraps silently.
Integer.MAX_VALUE + 1is-2147483648, and evenMath.abs(Integer.MIN_VALUE)stays negative. %takes the sign of the left operand.-7 % 3is-1and7 % -3is1. If you want a non-negative bucket index, useMath.floorMod(-7, 3), which returns2.%also works on doubles:5.5 % 2is1.5.
Division follows the same split. Integer / 0 throws ArithmeticException, but 1.0 / 0 is Infinity and 0.0 / 0 is NaN.
Now the famous one.
Try it yourself first
Before reading on, predict what each line prints. Then explain why in terms of what the JVM does, not "post-increment happens later".
public class Increments {
public static void main(String[] args) {
int i = 0;
i = i++;
System.out.println(i); // ?
int j = 0;
j = ++j;
System.out.println(j); // ?
int m = 5;
m = m++ + ++m;
System.out.println(m); // ?
}
}
The answers are 0, 1 and 12. Here's the bytecode javac emits for i = i++ (from javap -c), with i in local slot 1 because slot 0 holds args:
iconst_0 // push 0
istore_1 // i = 0
iload_1 // push a COPY of i (0) onto the operand stack
iinc 1, 1 // bump the local slot: i is now 1
istore_1 // pop the saved 0 and store it into i: i is 0 again
The JVM is a stack machine. Every expression is computed on a small per-frame operand stack, while variables live in local variable slots. Post-increment means "the value of this expression is the old value". The compiler honours that by loading the old value onto the stack before incrementing the slot. Then the assignment stores the stack value, wiping out the increment.
Step by step through iload_1, iinc and istore_1: the increment lands in the local slot but the saved old value on the operand stack overwrites it
Notice three things:
++jcompiles toiinc 1, 1theniload_1, so the stack holds the new value andjends as1.- In
m = m++ + ++m, the left operand runs first:m++yields5and setsmto6; then++msetsmto7and yields7.5 + 7 = 12is stored intom. - None of this is undefined behaviour. In C,
i = i++is undefined. In Java, the JLS pins it down, so the answer is always0.
03Under the hood: evaluation order, logic and bits
JLS 15.7 gives you three guarantees: the left operand of a binary operator is fully evaluated before any part of the right operand; every operand is evaluated before the operation runs; and method arguments are evaluated left to right. Precedence only decides grouping. Order is always left to right.
public class Order {
static int log(String name, int v) {
System.out.print(name);
return v;
}
public static void main(String[] args) {
System.out.println(" = " + (log("a", 1) + log("b", 2) * log("c", 3)));
// prints "abc = 7": * groups first, but a still runs first
int k = 1;
int r = k + (k = 5) * k; // 1 + 5 * 5
System.out.println(r); // 26
int[] arr = {0, 0};
int idx = 0;
arr[idx] = idx = 1; // array ref and index are evaluated first
System.out.println(arr[0] + "," + arr[1]); // 1,0
Object o = "hello";
if (o instanceof String s && s.length() > 3) { // Java 16 pattern
System.out.println(s.toUpperCase()); // HELLO
}
}
}
Logical operators. && and || short-circuit: if the left side decides the answer, the right side never runs. That's why s != null && s.length() > 0 is safe. On booleans, & and | are the non-short-circuit versions: s != null & s.length() > 0 evaluates both sides and throws NullPointerException when s is null. You'd only pick & deliberately when the right side has a side effect that must always happen, which is usually a design smell.
Bitwise and shifts. &, |, ^ and ~ work bit by bit on integers. Shifts have two surprises. First, the shift distance is masked: for int only the low 5 bits count, so 1 << 32 is 1, not 0 (for long it's the low 6 bits, so 1L << 32 is 4294967296). Second, the two right shifts differ on negatives:
| Expression | Result | Why |
|---|---|---|
-8 >> 1 |
-4 |
arithmetic shift copies the sign bit |
-8 >>> 1 |
2147483644 |
logical shift fills with zeros |
-1 >>> 28 |
15 |
the top four bits become the bottom four |
x & 1 |
0 or 1 |
cheap odd/even test, also works for negatives |
instanceof with a pattern (Java 16, JEP 394). o instanceof String s tests and binds in one step, and s is in scope only where the test is definitely true. That's why o instanceof String s && s.length() > 3 compiles but o instanceof String s || s.isEmpty() does not: on the right of ||, the test was false. Flow scoping also works in reverse: after if (!(o instanceof String s)) return;, s is usable for the rest of the method. Since Java 21 you can deconstruct records in the same place: o instanceof Point(int x, int y).
04Real world: where these bite in production
Hash-based bucketing is the classic incident. Someone writes buckets[key.hashCode() % n], it passes tests, and then a key with a negative hash code arrives and throws ArrayIndexOutOfBoundsException in production. HashMap itself sidesteps this with (n - 1) & hash on a power-of-two table, which is always non-negative. The fix in your own code is Math.floorMod(hash, n).
The other repeat offender is the mid-point calculation (low + high) / 2 in binary search. With indexes above about 1.07 billion the sum overflows to a negative number. The overflow-safe version is low + (high - low) / 2, or (low + high) >>> 1, which treats the overflowed sum as unsigned. That second form is what Arrays.binarySearch uses, and it's a nice detail to drop in an interview.
05Advanced: the ternary and compound assignment traps
The conditional operator ?: has its own typing rules (JLS 15.25), and they surprise people because the result type is decided at compile time from both branches, whichever one runs.
public class TernaryTraps {
public static void main(String[] args) {
System.out.println(true ? 1 : 2.0); // 1.0: int promoted to double
char c = 'x';
int zero = 0;
System.out.println(true ? c : 0); // x: constant 0 fits in char
System.out.println(true ? c : zero); // 120: char promoted to int
short s = 1;
s += 1; // fine: means s = (short) (s + 1)
// s = s + 1; // compile error: int cannot go into short
int x = 10;
x += 3.7; // compiles, x is 13: the .7 is silently cut off
Integer boxed = null;
boolean flag = false;
int n = flag ? 1 : boxed; // NullPointerException: unboxing
}
}
Many candidates are quick to say s += 1 and s = s + 1 are the same thing. They aren't. JLS 15.26.2 defines E1 op= E2 as E1 = (T) ((E1) op (E2)) with E1 evaluated once. The hidden cast is why s += 1 compiles and why x += 3.7 quietly truncates. It's also why byte b = -8; b >>>= 1; gives -4: the shift happens on the promoted int (2147483644) and the cast back to byte keeps only the low 8 bits.
The ternary NullPointerException deserves a sentence of its own. When one branch is int and the other is Integer, the result type is int, so the Integer branch gets unboxed even if it's null. Make both branches the same boxed type, or check for null first. We go deeper into unboxing in Wrapper classes and autoboxing.
Bit tricks rarely buy anything in modern Java. The JIT already turns x * 8 into a shift and x % 16 on a known non-negative value into a mask. Write the readable version first. Reach for >>> and masks only when they express intent, such as hashing, flags or unsigned arithmetic.
06How to say it in the interview
When I ask an operator puzzle, I'm listening for three things: do you separate grouping (precedence) from order (left to right), can you explain i = i++ in terms of the operand stack instead of "it happens later", and do you know about the hidden cast in compound assignment. Bonus points if you name the JLS section without being asked.
In the interview
"Precedence decides how operands are grouped, but Java always evaluates operands left to right, so side effects are predictable. i = i++ leaves i unchanged: javac loads the old value onto the operand stack, increments the local, then stores the old value back. && and || short-circuit, & and | don't. >> is sign-preserving, >>> is unsigned. And compound assignment includes an implicit narrowing cast, which is why short s; s += 1 compiles but s = s + 1 doesn't."
Here's how this might look on the whiteboard:
On the whiteboard
Operators
- precedence = grouping; order = always L to R (JLS 15.7)
- byte/short/char promote to int; overflow wraps
%sign follows dividend; use floorMod for bucketsi = i++: iload, iinc, istore, so i unchanged&&/||short-circuit;&/|evaluate both>>keeps sign;>>>zero-fills; shift distance masked?:type from both branches; watch unboxing NPEa op= bmeansa = (T)(a op b)