Module 21 · Types and memory · Lesson 1 of 4
Value types vs reference types
Whether assigning a variable copies the data or shares it. Value and reference semantics across languages, why most languages pass references by value, what boxing costs, and how immutability makes sharing safe.
Why it matters
"I changed it in one place and it changed somewhere else" and "I changed it and nothing happened" are two of the most common bugs in any language, and both come from not knowing whether a variable holds the data itself or a reference to data stored elsewhere. The answer also drives performance: millions of small reference objects cost memory, allocation and garbage-collection time that value types avoid.
The idea
A value type variable holds the data directly; assignment and parameter passing copy the data, so each copy is independent. A reference type variable holds a reference (an address) to an object on the heap; assignment copies the reference, so several variables can point at, and change, the same object. That sharing is called aliasing.
Across languages
| Language | Value semantics | Reference semantics | Notes |
|---|---|---|---|
| Java | Primitives: int, long, double, boolean… | Every object, including arrays and String | Value classes are coming to the JVM; records are still references |
| C# | struct, enum, primitives | class, arrays, string | Structs copy on assignment; large mutable structs are a trap |
| Go | Everything by default, including structs and arrays | Pointers, and the hidden pointers inside slices, maps and channels | Passing a slice copies its header, not its elements |
| Python | None; every name refers to an object | All objects | Immutable types (int, str, tuple) behave like values because they cannot change |
| JavaScript | Primitives: number, string, boolean, bigint | Objects, arrays, functions | const prevents rebinding, not mutation |
| C++, Rust | Everything by default | Explicit pointers and references | Copies, moves and borrows are explicit and checked |
Pass-by-value of a reference
Java, Python, JavaScript and C# (without ref) are all pass-by-value. For reference types, the value passed is the reference. So a method can mutate the object the caller passed in, because both hold a reference to the same object, but it cannot make the caller's variable point somewhere else, because it only has a copy of the reference.
void mutate(List<Integer> xs) { xs.add(4); } // caller sees the 4
void reassign(List<Integer> xs) { xs = new ArrayList<>(); } // caller's list is unchanged
List<Integer> nums = new ArrayList<>(List.of(1, 2, 3));
mutate(nums); // nums is [1, 2, 3, 4]
reassign(nums); // nums is still [1, 2, 3, 4]
True pass-by-reference, where the callee can rebind the caller's variable, exists in C# (ref, out), C++ (&) and a few others, and is used sparingly.
Where the bytes live, roughly
As a simplification: local value-type variables live in the function's stack frame and disappear when it returns; reference-type objects live on the heap and live until nothing references them. Real runtimes blur this (escape analysis keeps some objects off the heap; value types inside objects live on the heap with them), but the model explains the costs. Every heap object needs an allocation, a header of 12 to 16 bytes, a pointer to reach it, and eventual garbage collection. An array of a million long values is 8 MB in one block; a list of a million boxed Long objects is several times larger, scattered across the heap, and slower to iterate.
Boxing
Collections and generics in Java hold references, so storing a primitive wraps it in an object, called boxing (int becomes Integer). It is automatic and invisible, and in hot loops it allocates heavily. Boxed values also compare by reference with ==, so two Integer objects holding 1000 can be unequal under == while equal under equals. Primitive-specialised collections and arrays avoid both problems.
Immutability makes sharing safe
Aliasing is only dangerous when the shared object can change. Immutable objects (Java's String, records with immutable fields, Python tuples, frozen data classes) can be shared freely between variables and threads, because nobody can change them. That is why value-like domain types (money, dates, identifiers) should be immutable even in reference-type languages: they get value semantics without copying.
Trade-offs
Value types avoid aliasing bugs and heap overhead and are cheap for small data, but large value types are expensive to copy and mutable ones confuse people when a modified copy does not affect the original. Reference types make sharing and large structures cheap and pay with aliasing risks and allocation cost. Immutability gives reference types value-like safety at the price of creating new objects on every change.
In an interview
Raise it when designing data that flows between components: "Money is an immutable value object, so passing it around never risks one caller changing another's amount." For performance discussions: "The hot path stores a million coordinates in two primitive arrays rather than a million Point objects, which removes allocation and keeps the data contiguous for the cache."
Check yourself
In Java, a method receives a List, calls list.clear(), then sets list = null. What does the caller's variable hold afterwards?