Layers of Logic

3.1

Operators

Calculate values, compare them, short-circuit boolean expressions, and manipulate fixed-width bit patterns.

Core20 min read5 exercises
01

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.

02

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 / 2 is 3, not 3.5.
  • b += 300 compiles when b = b + 300 does not.
  • Reordering an && condition can cause a crash.
  • >> and >>> move bits in different ways.
  • i++ and ++i return different values when they appear inside a larger expression.
03

The idea

One useful way to group operators is by the value they return.

Operator familyResult
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.
The result tells you what you can do next. An if statement needs a boolean, for example.

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.

04

Under the hood

Going deeper

1. Division and remainder with whole numbers

System.out.println(7 / 2);      // 3
System.out.println(7 % 2);      // 1

When 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) == a

For 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 values

2. Compound assignment adds a cast

byte b = 10;
b = b + 300;      // ERROR: incompatible types: possible lossy conversion

b + 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
1

Rewriting 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 6
int j = 5;
System.out.println(++j);   // j becomes 6, then prints 6
i++ (post-increment)++i (pre-increment)
OrderReturn the old value, then add one.Add one, then return the new value.
Starting at 5The expression is 5; i ends at 6.The expression is 6; i ends at 6.
When it mattersWhen the expression value is used.When the expression value is used.
When it does notOn 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 && anything is false, so Java does not evaluate anything.
  • true || anything is true, so Java does not evaluate anything.
if (user != null && user.getName().isEmpty()) { }   // safe
if (user.getName().isEmpty() && user != null) { }   // fails when user is null

Java 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:

1100
12
12 in binary
1010
10
10 in binary
OperatorResult for 12 and 10
& AND1 only where both bits are 1.1000 = 8
| OR1 where either bit is 1.1110 = 14
^ XOR1 where the bits differ.0110 = 6
~ NOTFlip 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);    // 5

When 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);   // 1

The 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 here

Negative 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 leftCopies of the sign bit.Zeros.
Negative valueIt remains negative.It can become a large positive value.
-20 shifted right by 2-51073741819
Use it whenThe 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

  1. Higher precedence firstUnary operators (! ~ ++ --), then * / %, then + -, shifts, comparisons, logical operators, and assignment last.
  2. 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)    // correct

Write the parentheses every time. They state the test clearly.

05

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.

06

Check yourself

Answer from memory before opening the explanation. Then compare your rule with the answer and revisit the example if they differ.

  1. `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 + 300 is an int. Plain assignment needs you to say that narrowing it back to a byte is intentional.

    Compound assignment includes that narrowing cast for you. b += 300 behaves like b = (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.

  2. 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. When user is null, the first version stops after the left side is false. It never calls getName().

    The reversed version calls getName() first. Calling a method on null throws a NullPointerException.

  3. 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.

07

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

The Layers of Logic VS Code extension runs the checks for exercises marked checked. For a manual exercise, run the program and compare its behaviour with the stated requirements and sample output.
A

Remainder with Negative Operands

Warm up·15 min·15 points

checkedex-3-1-a

The 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
Modulo.java
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
The remainder takes the sign of the left operand. 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.
What this is really testing

Whether you know that % takes the sign of the left side. The classic is-it-even check is wrong for negative numbers, and it is wrong in production code all over the world.

B

The Hidden Cast

Real work·20 min·20 points

checkedex-3-1-b

Two 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
Hidden.java
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
So the value comes out as 54: 310 truncated to eight bits and read as signed. Convenient, and it means a byte can silently lose data in code that has no cast anywhere in it.
What this is really testing

Whether you believe that += is a pure shorthand. It is not, and the difference is a silent data loss that the compiler would have caught in the longer form.

C

Prove the Short Circuit

Real work·20 min·20 points

checkedex-3-1-c

You 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
Circuit.java
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
Recording the label inside 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.
What this is really testing

Whether short circuiting is a fact you read or a mechanism you have watched work. Half of Java's null safety rests on it, so watching it is worth twenty minutes.

D

Two Right Shifts

Real work·25 min·25 points

checkedex-3-1-d

Two 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
Shifts.java
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
They agree on every positive number, which is why the difference is easy to miss until a negative arrives.
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.
What this is really testing

Whether you can connect the two shift operators back to the sign bit from Section 2.2. Without that, the difference looks arbitrary. With it, it is obvious.

E

Registry Flags in One int

Hard·35 min·30 points·The Registry

checkedex-3-1-e

Your 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
Flags.java
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
Powers of two mean each flag owns exactly one bit and no two overlap. Use 3 for a flag and it shares bits with 1 and 2, so turning it off would turn those off as well.
What this is really testing

Whether you can use bitwise operators for what they are really for, which is packing many yes-or-no answers into one number. This is how permissions, feature switches and file modes are stored everywhere.

08

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

Short circuit evaluation will return in 8.3 - The Object Class: `equals`, `hashCode`, `toString`