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 . 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 is required by the JLS.
The same caching logic applies to Short, Byte, Long, and Character for values in . 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:
- Reflexive:
x.equals(x)istrue. - Symmetric:
x.equals(y)iffy.equals(x). - Transitive: if
x.equals(y)andy.equals(z), thenx.equals(z). - Consistent: multiple calls return the same result unless the objects are mutated.
- Null-safe:
x.equals(null)isfalsefor any non-nullx.
This contract is famously tricky to satisfy when inheritance is involved — a point explored in the OCP/LSP notes.