3.1
Operators
Calculate values, compare them, short-circuit boolean expressions, and manipulate fixed-width bit patterns.
Previously on
Phase II introduced the primitive types and showed how numbers are stored. It also gave you a rule that matters throughout this lesson: arithmetic on byte, short, and char happens as an int.
The problem
Declaring variables is only the beginning. A program has to calculate values and make choices.
Operators do that work. You do not need to memorise every symbol at once. Start with the rules that create real mistakes:
7 / 2is3, not3.5.b += 300compiles whenb = b + 300does not.- Reordering an
&&condition can cause a crash. >>and>>>move bits in different ways.i++and++ireturn different values when they appear inside a larger expression.
The idea
One useful way to group operators is by the value they return.
| Operator family | Result | |
|---|---|---|
| Arithmetic: + - * / % | Numbers go in. | A number comes out. |
| Comparison: == != < > <= >= | Values go in. | A boolean comes out. |
| Logical: && || ! | Booleans go in. | A boolean comes out. |
| Bitwise: & | ^ ~ << >> >>> | Numbers go in. | A number comes out, changed bit by bit. |
| Assignment: = += -= *= /= %= | A value goes in. | It is stored in a variable. |
That is why if (5) does not compile in Java. The number 5 is not a boolean. Java asks you to write the comparison you mean.
Under the hood
Going deeper1. Division and remainder with whole numbers
System.out.println(7 / 2); // 3
System.out.println(7 % 2); // 1When both values are whole numbers, Java drops the fractional part. % gives the remainder.
Negative values follow two rules:
System.out.println(-7 / 2); // -3, not -4
System.out.println(-7 % 3); // -1, not 2
System.out.println(7 % -3); // 1- Division moves towards zero. It does not use
Math.floor. - The remainder has the same sign as the left value.
For every non-zero divisor, Java preserves this identity:
(a / b) * b + (a % b) == aFor a = -7 and b = 3, division gives -2 and remainder gives -1. Substitution gives (-2 * 3) + -1, which is -7. The remainder magnitude is always smaller than the divisor magnitude.
Python gives a different result for -7 % 3: 2. In Java it is -1. This matters when you check whether a number is odd:
if (n % 2 == 1) { } // wrong for negative odd values
if (n % 2 != 0) { } // works for positive and negative values2. Compound assignment adds a cast
byte b = 10;
b = b + 300; // ERROR: incompatible types: possible lossy conversionb + 300 is an int. Java will not put that int back into a byte unless you make the narrowing explicit.
Now compare it with this:
byte b = 10;
b += 300; // compiles
System.out.println(b); // 54+= is not the same as b = b + 300. For this local variable, it has the same value effect as:
b = (byte) (b + 300);Java adds the cast. The value 310 is narrowed to eight bits, leaving 54.
There is another difference when the left side is an expression. A compound assignment evaluates that left side once.
public class CompoundDemo {
static int calls;
static int index() {
calls++;
return 0;
}
public static void main(String[] args) {
int[] values = {10};
values[index()] += 5;
System.out.println(values[0]);
System.out.println(calls);
}
}Output:
15
1Rewriting that line as values[index()] = values[index()] + 5 calls index() twice. The formal rule is close to E1 = (T) (E1 op E2), except that E1 is evaluated only once.
3. Pre-increment and post-increment
Both forms add one. They differ in the value the expression returns.
int i = 5;
System.out.println(i++); // prints 5, then i becomes 6int j = 5;
System.out.println(++j); // j becomes 6, then prints 6| i++ (post-increment) | ++i (pre-increment) | |
|---|---|---|
| Order | Return the old value, then add one. | Add one, then return the new value. |
| Starting at 5 | The expression is 5; i ends at 6. | The expression is 6; i ends at 6. |
| When it matters | When the expression value is used. | When the expression value is used. |
| When it does not | On a line by itself, including a for loop update. | The same. |
In a loop update, i++; and ++i; have the same effect. Avoid mixing either form into larger expressions. The result is valid Java, but it is harder to read and review.
4. Short-circuit evaluation
&& and || stop when the result is already known.
false && anythingis false, so Java does not evaluateanything.true || anythingis true, so Java does not evaluateanything.
if (user != null && user.getName().isEmpty()) { } // safe
if (user.getName().isEmpty() && user != null) { } // fails when user is nullJava also has & and | for booleans. They evaluate both sides every time.
5. Bitwise operators
Bitwise operators treat a number as a row of bits. They work on each position separately.
Take 12 and 10:
| Operator | Result for 12 and 10 | |
|---|---|---|
| & AND | 1 only where both bits are 1. | 1000 = 8 |
| | OR | 1 where either bit is 1. | 1110 = 14 |
| ^ XOR | 1 where the bits differ. | 0110 = 6 |
| ~ NOT | Flip every bit of 12. | -13 |
~12 is -13 because Java flips all 32 bits, then reads the result as a two’s-complement int. A useful shortcut is ~x = -x - 1.
6. Shift operators
<< moves bits left. >> and >>> move them right.
System.out.println(20 << 2); // 80
System.out.println(20 >> 2); // 5When no significant bits are discarded, shifting a positive value left by n has the same result as multiplying by 2^n. Bits that leave the fixed 32-bit or 64-bit width are discarded, so overflow can break that arithmetic shortcut.
System.out.println(1 << 30); // 1073741824
System.out.println(1 << 31); // -2147483648
System.out.println(1 << 32); // 1The last line is not a 33-bit result. For an int, Java uses only the low five bits of the shift distance, so the effective distance is distance & 31. 32 & 31 is 0. For a long, Java uses the low six bits, which is distance & 63.
Signed right shift is also not the same as division for every negative odd number:
System.out.println(-3 / 2); // -1, division truncates toward zero
System.out.println(-3 >> 1); // -2, sign-extending shift rounds downward hereNegative values show the difference between the two right shifts:
System.out.println(-20 >> 2); // -5
System.out.println(-20 >>> 2); // 1073741819| >> signed shift | >>> unsigned shift | |
|---|---|---|
| Bits entering from the left | Copies of the sign bit. | Zeros. |
| Negative value | It remains negative. | It can become a large positive value. |
| -20 shifted right by 2 | -5 | 1073741819 |
| Use it when | The value represents a number. | The value represents bits. |
>> copies the sign bit. >>> fills with zeros. When an int represents flags or part of a hash, that distinction matters.
7. The ternary operator
The ternary operator chooses one of two values.
int max = (a > b) ? a : b;Read it as: condition, value when true, value when false. It works well for a short choice. Nested ternaries are hard to read, so use an if instead.
8. Precedence and associativity
2 + 3 * 4 is 14 because * binds more tightly than +.
The complete precedence table has about fifteen levels. Remember the common order, then use parentheses when there is any doubt:
The order worth knowing
- Higher precedence firstUnary operators (
! ~ ++ --), then* / %, then+ -, shifts, comparisons, logical operators, and assignment last. - Use parentheses for the restThey make the grouping clear and prevent a reader from having to remember the table.
One common surprise is that bitwise & has lower precedence than ==:
if (flags & MASK == 0) // parsed as flags & (MASK == 0), so it is a type error
if ((flags & MASK) == 0) // correctWrite the parentheses every time. They state the test clearly.
What it costs
Short-circuiting means the right side of an && may not run. Keep side effects out of conditions, or the program can behave differently for different inputs.
arr[i++] = i++; is legal Java. It is also difficult to reason about. Increment operators work best in loop headers.
Bit masking can be useful in code that is already working with bits. In ordinary code, % is clearer than x & (n - 1). Modern JVMs can often optimise the readable version anyway.
Compound assignment deserves extra care because it can hide a narrowing cast. Use it when you understand the type change, not only because it is shorter.
Check yourself
Answer from memory before opening the explanation. Then compare your rule with the answer and revisit the example if they differ.
`byte b = 10; b = b + 300;` does not compile, but `byte b = 10; b += 300;` compiles and gives 54. Why?
Show the answer
In both expressions,
b + 300is anint. Plain assignment needs you to say that narrowing it back to abyteis intentional.Compound assignment includes that narrowing cast for you.
b += 300behaves likeb = (byte) (b + 300). The cast keeps the low eight bits of 310, which is 54. The compiler rejects the first form but accepts the second.Why can changing `user != null && user.getName().isEmpty()` to `user.getName().isEmpty() && user != null` make the program crash?
Show the answer
Java evaluates
&&from left to right and stops as soon as the answer is known. Whenuseris null, the first version stops after the left side is false. It never callsgetName().The reversed version calls
getName()first. Calling a method on null throws aNullPointerException.When do `>>` and `>>>` give different results?
Show the answer
They differ for negative values.
>>copies the sign bit from the left, so a negative value stays negative.>>>fills the left side with zeros, so that same bit pattern can become a large positive number.Use
>>when sign extension is the operation you need. Use>>>when zeros must enter from the left, for example while mixing bits. Neither is a universal replacement for division because overflow and negative rounding can change the result.
Exercises
Write each solution in VS Code, predict its result, and then run the checks. Reading an example does not replace implementing it.
5 exercises110 pointsabout 115 minutes
Remainder with Negative Operands
ex-3-1-aThe isOddBroken method looks completely reasonable. It is wrong, and it is wrong in a way that never crashes.
Predict the output for all six inputs before you run it. Then find every case where the broken version disagrees with the fixed one.
Once you see it, write down the rule in one sentence. That sentence is worth more than the exercise.
If you have written Python before, this one matters extra. Python and Java genuinely disagree about what -7 % 3 means, and carrying the Python answer into Java will cost you an afternoon someday.
What to write
These exact signatures. The checks call them by name, so a different name or a different parameter type will not build.
class Modulo {
static boolean isOddBroken(int n)
static boolean isOddFixed(int n)
static int remainder(int a, int b)
static boolean signFollowsLeft(int a, int b)
static int positiveMod(int a, int b)
}Keep isOddBroken broken, written as n % 2 == 1. positiveMod must never return a negative, whatever sign the inputs have.
What your program must do
- Find the inputs where the two versions disagree
- Work out which operand decides the sign of a remainder
- Write a version that is never negative
- Say why the broken version passes almost every test anyone writes
public class Modulo {
// Leave this one broken. It is the point of the exercise.
static boolean isOddBroken(int n) { return n % 2 == 1; }
// TODO: a version that works for negatives too
static boolean isOddFixed(int n) { return false; }
static int remainder(int a, int b) { return 0; } // TODO
// Does the remainder take its sign from the LEFT operand? Find out.
static boolean signFollowsLeft(int a, int b) { return false; } // TODO
// Never negative, whatever the inputs. Useful for a bucket index.
static int positiveMod(int a, int b) { return 0; } // TODO
public static void main(String[] args) {
// TODO: print both versions for 7, 8, -7, -8, 0 and -1, plus n % 2 itself
}
}
Hint 1
-7 % 2 is -1 in Java, not 1. So n % 2 == 1 answers false for every negative odd number.Hint 2
7 % -2 is 1 and -7 % 2 is -1.Hint 3almost the answer
((a % b) + b) % b is the always-positive version. The extra + b pushes a negative remainder into range, and the second % b brings a positive one back down.The Hidden Cast
ex-3-1-bTwo lines that look like the same thing. One does not compile. The other compiles and gives you a wrong number.
Find out which is which, then work out the printed value of a using bit patterns, not by running it.
Then look at the other three cases. The char one is actually useful, and explains something from Section 2.3 that may have seemed inconsistent: c + 1 is an int, so why does c += 1 work?
Finish by writing out the full expansion of += on paper. Once you have written it once, you will never be surprised by it again.
What to write
These exact signatures. The checks call them by name, so a different name or a different parameter type will not build.
class Hidden {
static byte compoundOnByte()
static int whatPlainAdditionWouldGive()
static char compoundOnChar()
static short compoundOnShort()
static int compoundDivide()
static boolean plainAdditionCompiles()
}compoundOnByte declares a byte of 10 and does a += 300. whatPlainAdditionWouldGive returns the same arithmetic as an int, since the byte version of it will not compile. plainAdditionCompiles is you recording what you found.
What your program must do
- Try both forms of the byte addition and record which one compiles
- Predict the value each compound assignment produces before running
- Say what += is doing that + is not
- Say why this is a feature and also a way to lose data quietly
public class Hidden {
// byte a = 10; a += 300; Does this compile? What comes out?
static byte compoundOnByte() { return 0; } // TODO
// byte a = 10; a = a + 300; Try writing that. Then return the same
// arithmetic as an int, since the byte version will not build.
static int whatPlainAdditionWouldGive() { return 0; } // TODO
static char compoundOnChar() { return 0; } // TODO: char c = 'A'; c += 1;
static short compoundOnShort() { return 0; } // TODO: short s = 30000; s += 30000;
static int compoundDivide() { return 0; } // TODO: int i = 5; i /= 2;
// Did a = a + 300 compile? Record what you found.
static boolean plainAdditionCompiles() { return true; } // TODO
public static void main(String[] args) {
// TODO: print all of them, and predict each one first
}
}
Hint 1
a = a + 300 is rejected because a + 300 is an int and an int does not fit in a byte without a cast.Hint 2
a += 300 compiles because the specification says compound assignment includes an implicit cast back to the left hand type. It is doing a = (byte)(a + 300) for you.Hint 3almost the answer
Prove the Short Circuit
ex-3-1-cYou have been told that && stops early. Watch it stop.
The check method prints a line every time it runs, so you can see exactly which sides get evaluated. Run it and compare && against &. The results match. The printed lines do not.
Then write the thing this actually protects: a null check. Do it in the safe order and show it works. Then flip the two sides and show it throwing a NullPointerException.
That flip is the entire lesson. && is not a faster &. It is an operator whose ordering guarantee real code depends on for safety.
What to write
These exact signatures. The checks call them by name, so a different name or a different parameter type will not build.
class Circuit {
static boolean check(String label, boolean value)
static List<String> andShortCircuit()
static List<String> andEager()
static List<String> orShortCircuit()
static boolean safeLengthCheck(String s)
static String unsafeLengthCheck(String s)
}check records its label in a list so the checks can see which sides were evaluated. The three list methods clear the record first and return what was evaluated, in order.
What your program must do
- Show that && skips its right side and & does not
- Show the same for the or operators
- Write the null check both ways round and run each with null
- Say why the unsafe version passes every test that does not use null
import java.util.*;
public class Circuit {
static final List<String> evaluated = new ArrayList<>();
// Record that this side was evaluated, then return the value.
static boolean check(String label, boolean value) {
return value; // TODO: record the label first
}
// Each one clears the record, runs the expression, and returns what was evaluated.
static List<String> andShortCircuit() { return List.of(); } // TODO: false && true
static List<String> andEager() { return List.of(); } // TODO: false & true
static List<String> orShortCircuit() { return List.of(); } // TODO: true || true
// Null check first, then use it.
static boolean safeLengthCheck(String s) { return false; } // TODO
// The other way round. Return "ok" or the name of what came out.
static String unsafeLengthCheck(String s) { return "ok"; } // TODO
public static void main(String[] args) {
// TODO: print which sides were evaluated for each of the three
// TODO: try both null checks with null and with a real string
}
}
Hint 1
check is what makes this visible. Without it you are guessing about evaluation order.Hint 2
&& and || stop as soon as the answer is known. & and | always evaluate both sides, which matters when a side has an effect.Hint 3almost the answer
s != null && s.length() > 3 is safe because the left side stops it. Reverse them and length() runs first, which throws on null and works perfectly for every other input.Two Right Shifts
ex-3-1-dTwo operators that look almost identical. Work out why Java needs both.
Predict every result for -20 at the bit level before you run anything. Write the 32 bits out on paper, shift them by hand, and say what comes in on the left.
Then answer the two questions that make it click. Why does >> behave like division even for negative numbers? And why does -1 >>> 28 give exactly 15?
Use the two answers to state when sign extension and zero fill produce different results. Phase X applies zero-fill shifts while mixing hash bits.
What to write
These exact signatures. The checks call them by name, so a different name or a different parameter type will not build.
class Shifts {
static String bits(int v)
static int arithmeticShift(int v, int by)
static int logicalShift(int v, int by)
static boolean shiftsAgree(int v, int by)
static int halveByShift(int v)
static int doubleByShift(int v)
}bits always returns exactly 32 characters. arithmeticShift uses the two arrow operator and logicalShift uses the three arrow one.
What your program must do
- Print the bit pattern and both right shifts for four values
- Find the inputs where the two shifts disagree
- Say what each one fills the vacated bits with
- Say what halving by shift does to a negative number
public class Shifts {
// Exactly 32 characters, padded with leading zeros.
static String bits(int v) { return ""; } // TODO
static int arithmeticShift(int v, int by) { return 0; } // TODO: >>
static int logicalShift(int v, int by) { return 0; } // TODO: >>>
static boolean shiftsAgree(int v, int by) { return true; } // TODO
static int halveByShift(int v) { return 0; } // TODO
static int doubleByShift(int v) { return 0; } // TODO
public static void main(String[] args) {
// TODO: print the bits and both shifts for 20, -20, -1 and Integer.MIN_VALUE
// PREDICT the negative cases before running
}
}
Hint 1
>> copies the sign bit into the top, so a negative number stays negative. >>> always fills with zeros, so a negative number becomes a large positive one.Hint 2
Hint 3almost the answer
>> rounds towards negative infinity, not towards zero. -1 >> 1 is -1, not 0, which is a real difference from dividing by two.Registry Flags in One int
ex-3-1-eYour Registry unit has capabilities. You could give it four boolean fields. Instead, put all four in a single int, one bit each.
This is not showing off. It is how file permissions work in every operating system, how feature switches are stored in large systems, and how Java’s own Modifier class reports whether a method is public, static or final.
Implement all four operations: turn on, test, turn off, and flip. Each one is a single bitwise operator, and working out which is the exercise.
Watch the brackets on your test. & binds looser than !=, so leaving them out will not compile, and the error message will not make the reason clear.
Finish by printing the capabilities as a readable list, so a human can see what the number means.
What to write
These exact signatures. The checks call them by name, so a different name or a different parameter type will not build.
class Flags {
static int turnOn(int flags, int which)
static int turnOff(int flags, int which)
static int flip(int flags, int which)
static boolean has(int flags, int which)
static int countSet(int flags)
static String describe(int flags)
}Every method takes the current flags and returns the new ones, changing nothing else. describe lists the set capabilities separated by single spaces, in the order fly swim cloak heal, and returns an empty string for none.
What your program must do
- Turn flags on and off without disturbing the others
- Turn two on with a single call
- Show that flipping twice returns to the start
- Say why the constants have to be powers of two
public class Flags {
// Each capability is one bit. Powers of two, so no two overlap.
static final int CAN_FLY = 1; // 0001
static final int CAN_SWIM = 2; // 0010
static final int CAN_CLOAK = 4; // 0100
static final int CAN_HEAL = 8; // 1000
// Each one returns the NEW flags and disturbs nothing else.
static int turnOn(int flags, int which) { return flags; } // TODO
static int turnOff(int flags, int which) { return flags; } // TODO
static int flip(int flags, int which) { return flags; } // TODO
static boolean has(int flags, int which) { return false; } // TODO
static int countSet(int flags) { return 0; } // TODO
// "fly swim cloak heal", in that order, single spaces, empty for none.
static String describe(int flags) { return ""; } // TODO
public static void main(String[] args) {
// TODO: turn on fly and cloak with ONE call, check swim, turn off cloak,
// flip fly, and print the capabilities at each step
}
}
Hint 1
| turns bits on. & ~which turns them off. ^ flips them. & which tests them.Hint 2
turnOn(flags, CAN_FLY | CAN_CLOAK) does two at once, because the or of two flags is a pattern with both bits set.Hint 3almost the answer
After the credits
Short-circuiting appears in Section 3.2, where conditions become the main topic. Later, it protects the null checks in a correct equals() method.
Bitwise operators wait until Phase X. There you will see HashMap use & to turn a hash into an array position and >>> to mix its high bits into its low bits.
Those lines are much easier to read once the bit rules are familiar.
Threads you opened in this section
- Bitwise operatorsFlag sets used to be built from bits before enums existed.8.4 - Enums
- Bitwise operatorsHashMap picks a bucket with one & operation. It is the fastest modulo there is.Phase X. The Collections Framework
- Short circuit evaluationOptional exists so you stop writing these checks by hand.11.5 - Optional, and the Habit It Is Trying to Break
Short circuit evaluation will return in 8.3 - The Object Class: `equals`, `hashCode`, `toString`