Operators and precedence
Most operators do exactly what you expect, which is why the four that do not are worth a lesson of their own. Each one here has produced a real bug in real code, and three of them compile without a murmur.
+ is two operators wearing one symbol
It adds numbers, and it joins strings. Which one you get depends on the operands, and it is evaluated strictly left to right:
System.out.println(1 + 2 + " tiffins");
System.out.println("tiffins: " + 1 + 2);
System.out.println("total: " + 1 + 2 * 3);
3 tiffins
tiffins: 12
total: 16
Line one adds 1 and 2 first, then joins. Line two joins "tiffins: " with 1,
producing a string, so the 2 is joined too — 12, not 3. Line three shows
that precedence still applies inside: 2 * 3 runs before either join.
The moment one side is a String, everything after it becomes string joining.
Wrap arithmetic in brackets when you mix them:
System.out.println("total: " + (1 + 2 * 3));
Precedence, in full
Keep this. It answers most "why did that happen" questions about an expression. Higher rows bind tighter.
| Level | Operators | Associativity |
|---|---|---|
| 1 | x++ x-- (postfix) |
left |
| 2 | ++x --x +x -x ! ~ |
right |
| 3 | (type) cast, new |
right |
| 4 | * / % |
left |
| 5 | + - |
left |
| 6 | << >> >>> |
left |
| 7 | < <= > >= instanceof |
left |
| 8 | == != |
left |
| 9 | & |
left |
| 10 | ^ |
left |
| 11 | | |
left |
| 12 | && |
left |
| 13 | || |
left |
| 14 | ? : |
right |
| 15 | = += -= *= /= %= and the rest |
right |
Two consequences people trip over. && binds tighter than ||, so
a || b && c is a || (b && c). And - is left-associative, so 10 - 4 - 3 is
3, not 9.
Do not memorise this. Memorise * before +, and use brackets for
everything else. Brackets cost nothing and a reviewer never has to check.
++ and --, and the assignment that undoes itself
Postfix returns the old value and then increments. Prefix increments and then returns the new value.
int i = 5;
System.out.println(i++); // prints 5
System.out.println(i); // prints 6
int j = 5;
System.out.println(++j); // prints 6
System.out.println(j); // prints 6
On a line of its own, i++ and ++i are identical, and that is where you should
keep them. Inside a larger expression they reward cleverness with bugs.
The classic:
int k = 5;
k = k++;
System.out.println(k);
5
k++ returns 5 and sets k to 6 — and then the assignment writes the returned 5
back over it. The increment is real and is immediately destroyed. Every reviewer
has seen this, usually written as count = count++ inside a loop that then never
terminates.
Use ++ as a statement, never inside an expression you also assign from.
Short-circuit, and the idiom it makes possible
&& evaluates its right side only if the left side is true. || evaluates its
right side only if the left side is false.
static boolean expensive() {
calls++;
return true;
}
boolean r1 = false && expensive(); // calls: 0
boolean r2 = false & expensive(); // calls: 1
& and | are the non-short-circuiting versions. They always evaluate both
sides, and on booleans that is almost never what you want.
Short-circuiting is not merely an optimisation. It is what makes this safe:
String name = null;
System.out.println(name != null && name.length() > 3);
false
name.length() is never called, because the left side was already false.
Change that && to & and the same line throws NullPointerException. This
null-guard is the single most common use of && in Java, and you will write it
several times a day until module 7 shows you how to need it less.
The mirror-image form uses ||:
if (value == null || value.isBlank()) { ... }
Order matters in both. name.length() > 3 && name != null is a guard that guards
nothing.
The ternary, and the type it quietly chooses
String label = count == 1 ? "tiffin" : "tiffins";
Readable, and worth using for small either-or values. Two things to know.
First, it is an expression, not a statement — it produces a value, so it
cannot stand on its own line. If both branches are actions rather than values,
you want an if.
Second, and this is the trap: both branches are forced into one common type.
Object o = true ? 1 : 2.0;
System.out.println(o);
System.out.println(o.getClass().getSimpleName());
1.0
Double
The condition was true, so the answer should be the int 1. It printed
1.0, a Double, because the two branches were unified to double before
either was chosen. Keep both branches the same type. The version of this that
actually hurts involves an Integer and an int, where unification forces
unboxing and a null branch throws NullPointerException on a line with no
visible method call.
Bitwise and shifts
You will not need these often. You will need to recognise them.
| Operator | Meaning | Example |
|---|---|---|
& |
Bitwise AND | 12 & 10 is 8 |
| |
Bitwise OR | 12 | 10 is 14 |
^ |
Bitwise XOR | 12 ^ 10 is 6 |
~ |
Bitwise NOT | ~12 is -13 |
<< |
Shift left, fill with zeros | 1 << 10 is 1024 |
>> |
Shift right, keep the sign | -8 >> 1 is -4 |
>>> |
Shift right, fill with zeros | -8 >>> 1 is 2147483644 |
>>> is the one Java has and most languages do not. The difference only shows up
on negative numbers, and it shows up dramatically. Shifts left and right are fast
multiplication and division by powers of two, but write * 2 — the compiler
knows the trick and the reader does not have to.
Where you will genuinely meet bitwise operators is flag sets, hashing code, and anything reading a binary file format.
Math, worth having on one page
| Call | Gives | Note |
|---|---|---|
Math.max(3, 9) |
9 |
|
Math.min(3, 9) |
3 |
|
Math.abs(-7) |
7 |
|
Math.pow(2, 10) |
1024.0 |
Always a double |
Math.sqrt(144) |
12.0 |
|
Math.round(2.4) |
2 |
|
Math.round(2.5) |
3 |
|
Math.round(-2.5) |
-2 |
Not -3 — halves go towards positive infinity |
Math.ceil(2.1) |
3.0 |
|
Math.floor(2.9) |
2.0 |
|
Math.floorDiv(-7, 2) |
-4 |
-7 / 2 is -3 |
Math.floorMod(-7, 2) |
1 |
-7 % 2 is -1 |
Math.random() |
0.0 up to but excluding 1.0 |
Prefer java.util.Random |
Math.round(-2.5) giving -2 catches people who expect symmetry. The rule is
"add 0.5 and take the floor", which leans positive.
And one carried over from the last lesson: compound assignment (+=, -=, *=)
performs a hidden narrowing cast, so byte b = 10; b += 300; compiles and gives
54. The plain b = b + 300 does not compile. Same trap, worth seeing twice.
Check your work
What does "tiffins: " + 1 + 2 print, and why? tiffins: 12. + is
left-associative, the first operation joins a string with 1 producing a string,
and the 2 is then joined too. Bracket the arithmetic: + (1 + 2).
What does k = k++; leave in k? The original value. k++ returns the old
value and increments, then the assignment writes the old value back over the
increment.
Why does name != null && name.length() > 3 not throw when name is null?
&& short-circuits: the right side is never evaluated once the left is false.
Writing & instead would throw, as would reversing the order.
What is the type of true ? 1 : 2.0? double, so the expression is 1.0.
Both branches are unified to a common type before one is selected. Keep the two
branches the same type.
What is -8 >> 1 and -8 >>> 1? -4 and 2147483644. >> preserves the
sign bit; >>> shifts zeros in from the left and turns a negative into a large
positive.
What is 10 - 4 - 3? 3. Subtraction is left-associative.
What is true || true && false? true. && binds tighter than ||, so it
reads as true || (true && false).
What is Math.round(-2.5)? -2. Halfway values round towards positive
infinity.
Practice 1, the five expressions.
1 + 2 + "3" + 4 + 5 -> 3345
10 % 3 * 2 -> 2
1 + 2 * 3 - 4 / 2 -> 5
true || false && false -> true
5 / 2 * 2.0 -> 4.0
The first adds 1 and 2, then joins everything after the string. The last is the
one worth dwelling on: 5 / 2 is integer division giving 2, and only then is
2.0 involved. Writing 5 / 2.0 * 2 gives 5.0.
Practice 4, the safe average.
static String average(int total, int count) {
if (count == 0) {
return "no data";
}
return "%.2f".formatted((double) total / count);
}
The guard has to come first, and the cast has to be on one operand before the
division, not on the result — (double) (total / count) would divide in integers
and then widen a value that has already lost its fraction.
Practice
-
Predict, then run. Write down your answer for each of these before compiling. Then run them.
System.out.println(1 + 2 + "3" + 4 + 5); System.out.println(10 % 3 * 2); System.out.println(1 + 2 * 3 - 4 / 2); System.out.println(true || false && false); System.out.println(5 / 2 * 2.0);The last one is the interesting one:
5 / 2happens in integers first. -
Reproduce
k = k++. Write it, print the result, then rewrite it as three separate statements that make the sequence visible. Then write a loop usingcount = count++as its increment and watch it never finish. Stop it withCtrl+C. -
Prove short-circuiting. Write a method that prints something and returns
true, and call it on the right of&&withfalseon the left. Confirm nothing prints. Change&&to&and confirm it does. -
Write a safe average. Given a total and a count, return the average to two decimal places, or the text
"no data"when the count is zero. Do it without an exception, and be careful where the cast goes. -
Harder — a delivery charge. A tiffin delivery is free above Rs 500, Rs 20 within 3 km, and Rs 20 plus Rs 8 per additional kilometre beyond that. Write it as a single expression using nested ternaries. Then rewrite it as an
if/elsechain, and decide honestly which one you would rather find in a pull request. There is a right answer and it is not the clever one.
Next: control flow — if, the modern switch, and the loop that runs one time
too many.
Stuck on this lesson?
Being stuck is part of it — but being stuck alone for three days is not. Our internship programme pairs this curriculum with code review and one-to-one help from working developers, and it is free.
About the internship