Java Module 2 – Variables, types and operators
Operators, precedence and short-circuiting
Java's operator precedence and associativity, left-to-right evaluation, why i = i++ leaves i unchanged, short-circuit && and ||, and the type of ?:.
What you will learn
- Evaluate expressions using Java's precedence and associativity rules
- Predict the order in which Java evaluates operands, and avoid side effects such as k = k++
- Prevent errors with && and || short-circuiting
- Determine the type of a conditional expression from its two branches
- Write string concatenation that adds numbers first where it must
Before you start
What you’ll learn · Before you start
What you will learn
- Evaluate expressions using Java's precedence and associativity rules
- Predict the order in which Java evaluates operands, and avoid side effects such as k = k++
- Prevent errors with && and || short-circuiting
- Determine the type of a conditional expression from its two branches
- Write string concatenation that adds numbers first where it must
Before you start
On this page
When an expression has several operators, operator precedence: The rule that decides how an expression with several operators is grouped: * binds tighter than +, so 1 + 2 * 3 is 1 + (2 * 3). Parentheses override it, and it never changes the left-to-right order of evaluation. decides how Java groups
it: * binds tighter than +, so 1 + 2 * 3 is 1 + (2 * 3), which is 7. associativity: The rule that groups a chain of operators of the same precedence: most binary operators group from the left, so 10 - 4 - 3 is (10 - 4) - 3, while assignment and ?: group from the right, so a = b = 8 assigns b first.
settles ties between operators of the same level: most binary operators group from left to right, so 10 - 4 - 3 is
3, while assignment and ?: group from right to left. Grouping is not the same as order: Java always evaluates the
operands from left to right, so in a() + b() * c() the method a runs first although the multiplication is grouped
first.
&& and || can skip work: they short-circuit evaluation: How && and || work: the right operand is evaluated only when the left one has not decided the result, so s != null && s.isEmpty() never calls a method on null. & and | always evaluate both sides., evaluating their right
operand only when the left one has not already decided the answer, which is what makes
s != null && s.isEmpty() safe. You will run twelve expressions through the precedence rules on JDK 25, step through
the order in which k = k++ runs, and meet the traps of ++, of string + and of the conditional operator’s type.
Precedence decides how an expression is grouped
The table lists Java’s operators from the tightest binding to the loosest. Operators on one row have the same precedence, and the last column says how a chain of them groups:
| Operators | What they are | A chain groups |
|---|---|---|
x++ x-- |
postfix increment and decrement | |
++x --x +x -x ~ ! (type) |
prefix and unary operators, casts | right to left |
* / % |
multiplication, division, remainder | left to right |
+ - |
addition and subtraction, and string + |
left to right |
<< >> >>> |
shifts | left to right |
< > <= >= instanceof |
comparisons and type tests | |
== != |
equality | left to right |
& |
bitwise AND, logical AND without short-circuit | left to right |
^ |
bitwise and logical exclusive OR | left to right |
| |
bitwise OR, logical OR without short-circuit | left to right |
&& |
conditional AND | left to right |
|| |
conditional OR | left to right |
? : |
conditional operator | right to left |
= += -= *= /= and the other compound assignments |
assignment | right to left |
Comparisons do not chain: a < b < c compares a boolean with c and does not compile. Write a < b && b < c.
// Twelve expressions: precedence decides how each one is grouped, associativity breaks ties.
void main() {
IO.println("1 + 2 * 3 = " + (1 + 2 * 3));
IO.println("(1 + 2) * 3 = " + ((1 + 2) * 3));
IO.println("10 - 4 - 3 = " + (10 - 4 - 3));
IO.println("100 / 10 / 5 = " + (100 / 10 / 5));
IO.println("2 * 3 % 4 = " + (2 * 3 % 4));
IO.println("-7 / 2 * 2 = " + (-7 / 2 * 2));
IO.println("1 << 2 + 1 = " + (1 << 2 + 1));
IO.println("6 & 3 + 1 = " + (6 & 3 + 1));
IO.println("5 > 3 == 2 > 1 = " + (5 > 3 == 2 > 1));
IO.println("true || false && false = " + (true || false && false));
IO.println("1 + 2 + \"3\" = " + (1 + 2 + "3"));
IO.println("\"1\" + 2 + 3 = " + ("1" + 2 + 3));
} from session import run, version
version()
run("""
java Precedence.java
""") Output
# openjdk version "25.0.4.1" LTS $ java Precedence.java 1 + 2 * 3 = 7 (1 + 2) * 3 = 9 10 - 4 - 3 = 3 100 / 10 / 5 = 2 2 * 3 % 4 = 2 -7 / 2 * 2 = -6 1 << 2 + 1 = 8 6 & 3 + 1 = 4 5 > 3 == 2 > 1 = true true || false && false = true 1 + 2 + "3" = 33 "1" + 2 + 3 = 123
Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 precedence_session.py
Each line of output shows an expression and its value. 2 * 3 % 4 groups from the left, so it is 6 % 4, which is 2.
-7 / 2 * 2 is (-7 / 2) * 2, and the integer division happens first, giving -6 instead of -7. 1 << 2 + 1 is
1 << 3, because + binds tighter than a shift, and 6 & 3 + 1 is 6 & 4 for the same reason: arithmetic binds
tighter than the bitwise operators, which surprises people in mask code. true || false && false is true because
&& binds tighter than ||. When the grouping is not obvious at a glance, add parentheses; they cost nothing.
The tree below shows the grouping of a longer condition, with the operator that binds most loosely at the top, and numbers its operands in the order the next section explains:
Precedence decides how the expression is grouped; evaluation still goes from left to right
Text description of the diagram
The expression `price + qty * 2 > limit && member` drawn as a tree, with the operator that binds most loosely at the top.
- At the top is `&&`. Its left operand is the comparison `>`, and its right operand is `member`.
- The left operand of `>` is the addition `+`, and its right operand is `limit`.
- The left operand of `+` is `price`, and its right operand is the multiplication `*` of `qty` and `2`.
So the expression means `((price + (qty * 2)) > limit) && member`. The operands are numbered in the order Java evaluates them, from left to right: price 1, qty 2, the literal 2 third, limit 4 and member 5. Because `&&` short-circuits, member is evaluated only when the comparison on its left is true.
Masking and adding without parentheses
flags & 3 + 1 is flags & 4, not (flags & 3) + 1, because + binds tighter than &: the run printed 4 for
6 & 3 + 1. Write (flags & 3) + 1 when you mean to mask first.
Checkpoint
Check: Precedence and associativity
Operands are evaluated from left to right
Precedence decides the shape of an expression, not when each part runs. Java evaluates the left operand of a binary operator fully before the right one, and the arguments of a method from left to right:
// Operands are evaluated from left to right, whatever the precedence says about grouping.
int show(String name, int value) {
IO.println(" evaluating " + name);
return value;
}
void main() {
IO.println("a + b * c:");
int r = show("a", 1) + show("b", 2) * show("c", 3);
IO.println(" result " + r);
int i = 1;
int j = i++ + i++; // 1 + 2, and i ends at 3
IO.println("i++ + i++ = " + j + ", then i = " + i);
int k = 5;
k = k++; // k++ gives 5, then the assignment stores 5 again
IO.println("k = k++ leaves k = " + k);
int[] seats = {10, 20, 30};
int index = 0;
seats[index] = index = 2; // the array element is chosen before index changes
IO.println("seats = " + Arrays.toString(seats) + ", index = " + index);
} from session import run, version
version()
run("""
java Order.java
!java ShortCircuit.java
""") Output
# openjdk version "25.0.4.1" LTS $ java Order.java a + b * c: evaluating a evaluating b evaluating c result 7 i++ + i++ = 3, then i = 3 k = k++ leaves k = 5 seats = [2, 20, 30], index = 2 $ java ShortCircuit.java true false false name == null || name.isEmpty(): true Exception in thread "main" java.lang.NullPointerException: Cannot invoke "String.isEmpty()" because "<local1>" is null at ShortCircuit.main(ShortCircuit.java:10) [exit status 1]
Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 order_session.py
show("a", 1) + show("b", 2) * show("c", 3)prints a, b and c in that order, then multiplies 2 by 3 and adds 1.i++ + i++readsias 1 and adds 1 to it, then reads 2 and adds 1 again, so the sum is 3 andiends at 3.k = k++leaveskunchanged. The postfixk++yields the old value, 5, and adds 1 tok, making it 6; then the assignment stores the old value, 5, back intok.seats[index] = index = 2picks the array element first, withindexstill 0, and only then evaluates the right side, soseats[0]becomes 2.
The third line is the one that surprises people most. Step through it and watch the value k holds after every step:
The drawing shows the whole scene; its 5 steps are listed below.
Every step as text
- The statement. k holds 5, and the statement is k = k++;. Java evaluates the left side of an assignment, its target, before anything on the right.
- The target first. The target is the variable k itself. Nothing is read or changed yet; Java only notes where the result will go.
- k++ yields the old value. The right side, k++, yields the value k has, 5, and then adds 1 to k, which now holds 6.
- The assignment stores 5. The assignment stores the value the right side yielded, 5, into k, over the 6. The run printed k = k++ leaves k = 5.
- Defined, and hard to read. The language fixes every one of these steps, so every JVM gives 5. A reader still has to work it out, so write k++; on a line of its own.
These rules are exact, so the results are the same on every JVM, but code that relies on them is hard to read. Keep
++ and -- in statements of their own, such as count++;, and never change a variable twice in one expression.
Key idea
Precedence only groups an expression; Java then evaluates its operands from left to right, the target of an
assignment first, so k = k++ stores the old value of k back into it.
Checkpoint
Check: Evaluation order and side effects
Short-circuit operators skip the right operand when the left decides
&& looks at its right operand only if the left one came out true, and || only if it came out false. That makes
them guards: s != null && !s.isBlank() never calls a method on null, because when s is null the left side is
already false. The first two lines of ShortCircuit in the session above show both operators at work. Step through
hasText for its three strings, then the same test written with &:
The drawing shows the whole scene; its 5 steps are listed below.
Every step as text
- Asha, the left side. hasText("Asha") checks its left operand first. s != null is true, so the left side has not decided the answer, and && goes on to the right one.
- Asha, the right side. !s.isBlank() is true for "Asha", so the result is true, the first word the run printed.
- Three spaces. For three spaces the left side is true again, but isBlank() is true, so !s.isBlank() is false and so is the result, the second word of the run.
- null. For null the left side is false, and a false left side decides &&. The right side is never evaluated, so no method is called on null, and the result is false.
- & has no shortcut. name != null & !name.isEmpty() uses &, which always evaluates both sides. With name null the right side runs anyway and throws NullPointerException, the last line of the run.
& and | also work on booleans and give the same results, but they always evaluate both sides. The last line of
ShortCircuit uses &, so name.isEmpty() runs on a null and throws NullPointerException. The message names
what was null; it calls the variable <local1> because java File.java compiles without the debugging information
that holds local names, and a class compiled with javac -g shows name instead. Use && and || for conditions,
and keep & and | for bit masks, the subject of the next lesson.
Checkpoint
Check: Short-circuit && and ||
A conditional expression takes its type from both branches
condition ? a : b, the conditional operator: The operator condition ? a : b, which evaluates a when the condition is true and b otherwise. Its type comes from both branches, so true ? 1 : 2.0 is the double 1.0., evaluates a when the condition
is true and b otherwise. Its type comes from both branches, and that has consequences:
Java (Ternary.java) · Ternary.java
// The type of a conditional expression comes from both of its branches.
void main() {
IO.println(true ? 1 : 2.0); // int and double: the result is a double
int code = 98;
IO.println(true ? 'a' : 0); // char and an int constant that fits in a char: a char
IO.println(true ? 'a' : code); // char and an int variable: an int
Object either = true ? 1 : "one";
IO.println(either.getClass().getSimpleName());
int score = 72;
String grade = score >= 90 ? "A" : score >= 75 ? "B" : score >= 60 ? "C" : "D";
IO.println("grade " + grade);
} Java (TernaryNull.java) · TernaryNull.java
// A cached value that is missing makes this line throw, although the variable could hold null.
void main() {
Map<String, Integer> stock = new HashMap<>();
boolean cached = true;
Integer count = cached ? stock.get("pens") : 0;
IO.println(count);
} from session import run, version
version()
run("""
java Ternary.java
!java TernaryNull.java
""") Output
# openjdk version "25.0.4.1" LTS $ java Ternary.java 1.0 a 97 Integer grade C $ java TernaryNull.java Exception in thread "main" java.lang.NullPointerException: Cannot invoke "java.lang.Integer.intValue()" because the return value of "java.util.Map.get(Object)" is null at TernaryNull.main(TernaryNull.java:5) [exit status 1]
Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 ternary_session.py
true ? 1 : 2.0prints1.0: with anintand adoublebranch the whole expression is adouble.true ? 'a' : 0printsa, buttrue ? 'a' : codeprints 97. Acharpaired with anintconstant that fits in acharstays achar; paired with anintvariable it becomes anint.- Branches of unrelated types, such as
1and"one", make it a reference expression: the1is boxed, so the value here is anInteger. - A chain such as
score >= 90 ? "A" : score >= 75 ? "B" : ...groups from the right, so it reads like a ladder ofelse if. Past two or three steps, anifor aswitchis clearer. - In
TernaryNull, one branch is anIntegerand the other theint0, so the expression’s type isint, and theIntegeris Turning a wrapper object such as Integer into its primitive value, an int here. Java does it on its own where a primitive is needed, and unboxing null throws NullPointerException..stock.get("pens")returnsnull, and unboxingnullthrows, even though the variable on the left could holdnull. WriteInteger.valueOf(0)in the second branch, or usestock.getOrDefault("pens", 0).
The branches decide the type, and the type decides what is printed. The run’s five lines, by the pair of branches:
| The two branches | The type of ?: |
The run printed |
|---|---|---|
1 and 2.0 |
double |
1.0 |
'a' and the constant 0 |
char |
a |
'a' and the int variable code |
int |
97 |
1 and "one" |
a reference, with 1 boxed |
Integer |
stock.get("pens") and 0 |
int, so the Integer is unboxed |
NullPointerException |
Checkpoint
Check: The type of a conditional expression
String concatenation groups from the left, so sums need parentheses
When either operand of + is a String, + joins text instead of adding. It still groups from the left, so
1 + 2 + "3" adds 1 and 2 first and then joins, giving "33", while "1" + 2 + 3 joins at every step and gives
"123", the last two lines of the twelve:
| The expression | Grouped as | Result |
|---|---|---|
1 + 2 + "3" |
(1 + 2) + "3": 3, then "33" |
"33" |
"1" + 2 + 3 |
("1" + 2) + 3: "12", then "123" |
"123" |
The common bug is "Total: " + a + b, which prints the two numbers side by side. Put arithmetic inside a string
expression in parentheses: "Total: " + (a + b).
== deserves one sentence here too: on primitive values it compares the values, but on objects it compares
references, so two different String objects with the same text are not ==. The lessons on strings and wrapper
classes later in this module show what to use instead.
Checkpoint
Check: String + and parentheses
Practise: five bugs from this module
Each method below compiles and runs, and each has one bug of a kind this lesson and the integer arithmetic lesson
describe: precedence, string +, the order of a short-circuit guard, the sign of % and integer division. Fix them:
// Exercise: each of these five methods compiles and runs, and each has one bug from this module. Fix them, then
// run this file: every check must say ok.
double average(int a, int b) {
return a + b / 2.0;
}
String label(int count) {
return "Items: " + count + 1; // the label shows the next count
}
int safeLength(String s) {
return s.length() > 0 && s != null ? s.length() : 0;
}
boolean isOdd(int n) {
return n % 2 == 1;
}
int percent(int part, int whole) {
return part / whole * 100;
}
int failures = 0;
void check(String what, boolean passed) {
if (!passed) failures++;
IO.println((passed ? "ok " : "FAILED ") + what);
}
boolean safeLengthOfNullIsZero() {
try {
return safeLength(null) == 0;
} catch (NullPointerException e) {
return false;
}
}
void main() {
check("average(3, 6) is 4.5", average(3, 6) == 4.5);
check("label(9) is \"Items: 10\"", label(9).equals("Items: 10"));
check("safeLength(\"abc\") is 3", safeLength("abc") == 3);
check("safeLength(null) is 0", safeLengthOfNullIsZero());
check("isOdd(7) and isOdd(-7)", isOdd(7) && isOdd(-7));
check("percent(45, 60) is 75", percent(45, 60) == 75);
IO.println(failures + " of 6 checks failed");
if (failures > 0) System.exit(1);
} As given, five of the six checks fail:
from session import run, version
version()
run("""
!java Bugs.java
""") Output
# openjdk version "25.0.4.1" LTS
$ java Bugs.java
FAILED average(3, 6) is 4.5
FAILED label(9) is "Items: 10"
ok safeLength("abc") is 3
FAILED safeLength(null) is 0
FAILED isOdd(7) and isOdd(-7)
FAILED percent(45, 60) is 75
5 of 6 checks failed
[exit status 1]
Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 exercise_session.py
First hint: read each return line and add the parentheses Java uses. a + b / 2.0 is a + (b / 2.0), and
s.length() > 0 && s != null calls length() before it checks for null.
With the six checks passing, try the questions below: grouping, the order of side effects, guards, the type of ?:
and string +. Your first answer to each one sets how hard the next is on that idea:
Practice that adapts to you
Practise this lesson
15 questions about 5 ideas of this lesson. Each answer is explained at once, and what you get right is kept in this browser.
Interview questions
Fresher · What is the difference between & and && in Java?
Both give the logical AND of two boolean values. The difference is that && stops early: if the left operand is
false, the right one is never evaluated, which is why s != null && s.isEmpty() cannot throw. & always evaluates
both operands, so the same expression with & throws NullPointerException when s is null, and side effects on
the right always happen. & also works on integers as a bitwise AND, which && does not. Use && in conditions and
& for bit masks.
Key takeaways
- Precedence decides grouping:
*/%before+-, then shifts, comparisons, equality,&,^,|,&&,||,?:and assignment; most binary operators group from left to right, assignment and?:from right to left. - Operands are always evaluated from left to right, whatever the grouping, and
k = k++leaveskunchanged. &&and||skip their right operand when the left one decides the result; put the null check first.- The type of
?:comes from both branches:true ? 1 : 2.0is1.0, and anIntegerbranch next to anintis unboxed, which throws fornull. - String
+groups from the left, so"Total: " + (a + b)needs its parentheses.
Terms in this lesson
- associativity
- The rule that groups a chain of operators of the same precedence: most binary operators group from the left, so 10 - 4 - 3 is (10 - 4) - 3, while assignment and ?: group from the right, so a = b = 8 assigns b first.
- conditional operator
- The operator condition ? a : b, which evaluates a when the condition is true and b otherwise. Its type comes from both branches, so true ? 1 : 2.0 is the double 1.0.
- operator precedence
- The rule that decides how an expression with several operators is grouped: * binds tighter than +, so 1 + 2 * 3 is 1 + (2 * 3). Parentheses override it, and it never changes the left-to-right order of evaluation.
- short-circuit evaluation
- How && and || work: the right operand is evaluated only when the left one has not decided the result, so s != null && s.isEmpty() never calls a method on null. & and | always evaluate both sides.
References
- The Java Language Specification, Java SE 25: chapter 15, Expressions (Oracle)
- The Java Language Specification, Java SE 25: 15.7 Evaluation Order (Oracle)
- The Java Language Specification, Java SE 25: 15.14.2 Postfix Increment Operator ++ (Oracle)
- The Java Language Specification, Java SE 25: 15.23 Conditional-And Operator && (Oracle)
- The Java Language Specification, Java SE 25: 15.25 Conditional Operator ? : (Oracle)
- The Java Language Specification, Java SE 25: 15.26.1 Simple Assignment Operator = (Oracle)
- The Java Tutorials: Operators (operator precedence) (Oracle)
- JEP 358: Helpful NullPointerExceptions (OpenJDK)
Report a problem with this lesson
Kept only in this browser. Your Learn progress