Primitive versus Reference Types
The Two Kinds of Type
Every type in Java falls into one of two categories: primitive or reference. Understanding the distinction is essential for reasoning about assignment, parameter passing, equality comparison, and null handling.
Primitive Types
Primitives represent simple, atomic values. They are:
- Fixed-size: each primitive type occupies a known number of bytes, specified by the language specification and independent of platform
- Stored directly by value: a variable of primitive type holds the actual value, not a pointer to it
- Never null: a primitive variable always contains a value (even if uninitialised, it gets a default zero value as a field)
- Live on the stack or inline in objects: primitives are not heap-allocated unless they are fields of a heap-allocated object
The Eight Primitive Types
| Type | Size | Range | Default | Wrapper Class |
|---|---|---|---|---|
byte | 8 bits | to | 0 | Byte |
short | 16 bits | to | 0 | Short |
int | 32 bits | to (~ billion) | 0 | Integer |
long | 64 bits | to | 0L | Long |
float | 32 bits | IEEE 754 single-precision (~7 significant digits) | 0.0f | Float |
double | 64 bits | IEEE 754 double-precision (~15 significant digits) | 0.0d | Double |
boolean | not precisely defined (JVM-dependent) | true or false | false | Boolean |
char | 16 bits | Unicode characters U+0000 to U+FFFF | \u0000 | Character |
The sizes in the table are fixed by the Java Language Specification. An int is always 32 bits, regardless of whether the JVM runs on a 32-bit or 64-bit platform. This is a key difference from C, where int may be 16, 32, or 64 bits depending on the compiler and target.
Literals
- Integer literals (
42) areintby default; appendLforlong(42L) - Floating-point literals (
3.14) aredoubleby default; appendfforfloat(3.14f) charliterals are enclosed in single quotes ('A','\n','\u0041')booleanliterals aretrueandfalse
Reference Types
Reference types include classes, arrays, interfaces, enums, and String. A variable of reference type holds a reference — a handle or pointer to an object allocated on the heap. The variable itself does not contain the object’s data; it contains the memory address (abstractly) of where the object lives.
String s = new String("hello");
The variable s is a reference. The actual string object lives on the heap. The reference is what gets stored in the variable.
Key Properties of Reference Types
- Assignment copies the reference, not the object:
Point p1 = new Point(3, 4);
Point p2 = p1;
p2 now refers to exactly the same object as p1. Modifying p2’s state will be visible through p1. This is called aliasing.
-
Can be null: a reference variable can hold
null, meaning “no object.” Attempting to call a method onnullthrows aNullPointerExceptionat runtime. -
Equality has two meanings:
==compares references — are these the same object in memory?.equals()compares values — do these objects represent the same logical value? (Behaviour depends on whether the class overridesequals.)
-
Live on the heap: the object itself is always heap-allocated. The reference variable may live on the stack (if it is a local variable) or be a field of another heap object.
Memory Layout
- Stack: holds local variables (primitives and references) and method call frames. Fast allocation/deallocation (just move the stack pointer). Each thread has its own stack.
- Heap: holds all objects and arrays. Managed by the garbage collector. Slower allocation, but objects can outlive the method that created them.
A local variable of type int stores the integer value directly on the stack. A local variable of type Point stores a reference on the stack; the Point object’s fields (x and y, which are primitives) live inside the object on the heap.
Why the Distinction Matters
Parameter Passing
Java is pass-by-value, always. For primitives, the value is the number itself — the method receives a copy, and mutations to the parameter do not affect the caller’s variable. For references, the value is the reference — the method receives a copy of the reference, so it can mutate the object’s state, but it cannot make the caller’s variable point to a different object.
void increment(int x) { x = x + 1; } // does nothing to caller's variable
void move(Point p) { p.x = 10; } // mutates the caller's object
void reassign(Point p) { p = new Point(0, 0); } // does nothing to caller's reference
Null Handling
Only reference types can be null. Primitives are never null. This means:
Integer boxed = null; // OK — Integer is a reference type
int primitive = null; // Compile error — int cannot be null
Auto-unboxing a null Integer to int throws a NullPointerException.
String: A Special Reference Type
String is a reference type — it is a class — but it has several special behaviours:
- Immutability: once created, a
Stringobject’s character sequence cannot change. Methods liketoUpperCase()andsubstring()return new strings rather than modifying the original. - String pool: string literals (e.g.
"hello") are interned — the JVM maintains a pool of unique string literals.String s1 = "hello"; String s2 = "hello";results ins1 == s2beingtruebecause they reference the same interned object. - Concatenation operator: the
+operator is overloaded for strings. Any non-string operand concatenated with a string is converted via itstoString()method. - StringBuilder: for repeated concatenation in loops, use
StringBuilderto avoid creating many intermediateStringobjects.
String s1 = "hello";
String s2 = "hello";
String s3 = new String("hello");
s1 == s2; // true — same interned object
s1 == s3; // false — s3 is a new object, not from the pool
s1.equals(s3); // true — same character sequence