Value vs Reference Semantics, Copying & Aliasing

What assignment and argument passing actually do, why "Java passes objects by reference" is false, how aliasing turns into bugs, and the memory-layout consequences that make this a performance decision as much as a correctness one.

Two things a language must decide about every type: what b = a copies, and what f(a) gives the callee. The answers determine whether a mutation is visible elsewhere, whether == means "same thing" or "same contents", and how the values are laid out in memory. Almost every "the object changed underneath me" bug and a surprising amount of cache-miss cost trace back to this one decision.

Untangling the two questions #

These are separate and constantly conflated:

  1. Value type vs reference type — a property of the type. Does a variable of this type hold the data, or a handle to data elsewhere?
  2. Pass-by-value vs pass-by-reference — a property of the calling convention. Does the callee get a copy of the caller's variable, or an alias to the variable itself?

Aliasing is the actual hazard #

Reference semantics means two names can denote one object. That is the whole point and also the whole problem: the object can change without either holder being involved.

java
class Reservation {
    private final Date start;                 // final, but Date is mutable
    Reservation(Date start) { this.start = start; }
    Date getStart() { return start; }         // hands out the internal object
}

Date d = new Date();
Reservation r = new Reservation(d);
d.setTime(0);          // the caller still holds a reference — invariant broken from outside
r.getStart().setTime(0); // and so does anyone who ever called the getter

The classical fixes are defensive copy on the way in and on the way out, which is correct and costs an allocation per crossing, or — far better — making the field type immutable so there is nothing to copy. java.time.Instant exists because java.util.Date got this wrong for twenty years. See immutability and value objects.

Equality follows from the same decision #

Identity equality Structural equality
Question are these the same object? do these hold the same data?
Java == equals (must override with hashCode)
C# ReferenceEquals, == on classes Equals, == on records/structs
Python is == (__eq__)
Rust pointer comparison (rare, explicit) PartialEq / #[derive(PartialEq)]

The equals/hashCode contract is the one to state precisely, because collections silently depend on it: equal objects must have equal hash codes; the reverse need not hold. Overriding one without the other produces a type that works in a List and fails in a HashSet, which is exactly the kind of bug that survives code review.

The layout consequence #

This is where the decision stops being philosophical. A reference type in a managed runtime means the array holds pointers, and iterating it means chasing them:

text
Object[]:   [ptr]→ hdr|fields    [ptr]→ hdr|fields    [ptr]→ hdr|fields
            (scattered by allocation order, one cache miss per element)

value[]:    [fields][fields][fields][fields]
            (contiguous, prefetcher-friendly, one miss per cache line)

A Long on the JVM is 16 bytes of object header plus 8 bytes of payload plus an 8-byte reference to reach it — roughly 4× the footprint of a long, and the payload is wherever the allocator put it. On a modern machine an L1 hit is ~1 ns and a main-memory reference ~80–100 ns (CPU caches), so a pointer-chasing traversal of a boxed collection can run an order of magnitude slower than the equivalent flat array while executing the same logic.

Boxing is the automatic conversion from a primitive to its reference form, and it is where this cost appears silently — Map<Integer, Long>, a List<Double> in a hot loop, an int autoboxed to satisfy a generic parameter. Java caches boxed Integer values in −128..127 only, which is also why Integer a = 1000, b = 1000; a == b is false while the same code with 100 is true.

Where the languages are now #

Language Value types Notes
C# struct, record struct, readonly struct user-definable since 1.0; in/ref readonly avoid the copy cost of large structs
Rust everything by default Copy = bitwise duplicate, otherwise move; aliasing is checked by the borrow checker, not by convention
Go struct is a value slices/maps/channels are reference-like headers; copying a struct containing a slice shares the backing array
Swift struct/enum are values copy-on-write in the standard library gives value semantics at reference cost
Java primitives only, until now see below

Sources

  1. docsJEP 401: Value Classes and Objects (OpenJDK, Project Valhalla)
  2. docsProject Valhalla
  3. docsThe Java Language Specification — Method Invocation (argument passing)
  4. bookEffective Java, 3rd Edition
  5. docsRust Book — Ownership, Move and Copy semantics

Searches titles, aliases, headings, summaries, and full text.↑↓ move ↵ open esc close