Skip to content
Part IA Michaelmas Term

Assignment, Aliasing and Equality

Assignment Copies the Value

For primitives, assignment copies the numeric value. The two variables are independent:

int a = 10;
int b = a;
b = 20;
System.out.println(a);  // 10 — unaffected

For reference types, assignment copies the reference. Both variables now point at the same object:

int[] x = {1, 2, 3};
int[] y = x;
y[0] = 99;
System.out.println(x[0]);  // 99 — affected because x and y point at the same array

This is the box-and-arrow model in action: x and y are two boxes, each containing an arrow, and both arrows point to the same heap object.

Aliasing

When two references point at the same object, they are aliases — different names for the same entity. Aliasing means:

  • Mutating through one reference is visible through the other.
  • Reassigning one reference does not affect the other (it only changes which object that one arrow points to).
List<String> list1 = new ArrayList<>();
List<String> list2 = list1;        // alias
list1.add("hello");
System.out.println(list2.size());  // 1 — mutation visible through alias
list2 = new ArrayList<>();         // list2 now points elsewhere
System.out.println(list1.size());  // still 1 — list1 unaffected

Aliasing can be a source of bugs when a developer does not realise that multiple parts of the program share a reference to the same mutable object. Defensive copying is one mitigation: if you are passed a mutable object and do not want the caller to be able to modify it, copy it:

public MyClass(List<String> items) {
    this.items = new ArrayList<>(items);  // defensive copy
}

Identity vs Equality: == and .equals()

The == operator on reference types compares identity — do these two references point at the exact same object in memory?

The .equals() method compares logical equality — do these two objects represent the same value?

String a = new String("hello");
String b = new String("hello");
System.out.println(a == b);       // false — different objects
System.out.println(a.equals(b));  // true — same content

For reference types, == should almost never be used unless you genuinely want to check if two references are to the same object. Use .equals() for value comparison.

Classes that override .equals() must also override .hashCode() to maintain the general contract: equal objects must have equal hash codes. Failing to do this causes subtle bugs when objects are used in hash-based collections (HashSet, HashMap).

The Integer Cache Trap

Java caches Integer objects for values in the range [128,127][-128, 127]. This means Integer.valueOf(n) (and autoboxing, which uses valueOf) returns a cached instance for small values:

Integer a = 100;    // autoboxing: Integer.valueOf(100)
Integer b = 100;
System.out.println(a == b);  // true — same cached object

Integer x = 200;    // outside cache range
Integer y = 200;
System.out.println(x == y);  // false — different Integer objects
System.out.println(x.equals(y));  // true — same value

The trap: == happens to work for small Integer values, which can give a false sense of correctness during testing. Always use .equals() for wrapper and String comparison. The cache range is implementation-dependent but [128,127][-128, 127] is required by the JLS.

The same caching logic applies to Short, Byte, Long, and Character for values in [128,127][-128, 127]. Boolean caches both TRUE and FALSE.

String Interning

String literals in Java are automatically interned — the JVM maintains a pool of unique string constants. All string literals with the same character sequence refer to the same String object:

String a = "hello";
String b = "hello";
System.out.println(a == b);  // true — same interned String

String c = new String("hello");
System.out.println(a == c);  // false — c is a different object
System.out.println(a.equals(c));  // true — same content

You can manually intern a string with String.intern(), which returns a canonical representation from the pool. However, overusing intern() in performance-sensitive code can bloat the string pool and degrade performance.

The takeaway: always use .equals() for String content comparison, regardless of how the strings were created.

Default .equals() in Object

The Object class provides a default implementation of .equals() that behaves identically to == — it compares object identity (memory addresses). Every class you write that needs value-based equality must override .equals() (and .hashCode()):

class Point {
    private final int x, y;

    public Point(int x, int y) { this.x = x; this.y = y; }

    @Override
    public boolean equals(Object other) {
        if (this == other) return true;
        if (!(other instanceof Point p)) return false;
        return this.x == p.x && this.y == p.y;
    }

    @Override
    public int hashCode() {
        return Objects.hash(x, y);
    }
}

The general contract of .equals() (from Object’s Javadoc) specifies that the method must be:

  1. Reflexive: x.equals(x) is true.
  2. Symmetric: x.equals(y) iff y.equals(x).
  3. Transitive: if x.equals(y) and y.equals(z), then x.equals(z).
  4. Consistent: multiple calls return the same result unless the objects are mutated.
  5. Null-safe: x.equals(null) is false for any non-null x.

This contract is famously tricky to satisfy when inheritance is involved — a point explored in the OCP/LSP notes.