Primitive and Reference Types
Explore how Java distinguishes between primitive types that store actual values and reference types that store memory addresses. Understand how this affects variable assignment, memory management on stack versus heap, and why null references cause runtime errors. This lesson helps you predict data behavior and manage Java variables effectively.
We'll cover the following...
Imagine you are sharing a document with a colleague. You have two choices: you can email them a PDF attachment, or you can send them a link to a shared online doc. This choice changes everything. If you email the PDF, you are sending a separate copy; if they highlight text on their version, your original file remains clean. But if you send the link, you are giving them access to the live document; if they delete a paragraph, it disappears from your screen, too.
Java enforces this exact distinction with every variable you create. Understanding this difference between holding a value and holding a reference is the key to predicting how your data behaves and preventing accidental side effects in your code.
The two categories of types
Java enforces a strict rule: every variable we declare must be either a primitive type or a reference type.
Primitive types: These are the atoms of Java. They represent simple values like a number (
20) or a character ('A'). When we create a primitive variable, we are creating a container that holds the value directly.Reference types: These handle complex data, such as strings, arrays, or custom objects. When we create a reference variable, it does not store the data itself. Instead, it stores a reference to the memory location where the data resides.
Primitive types: Storing values directly
Java provides exactly eight primitive types. These are built into the language and are reserved for simple data. Because they are simple, Java handles them efficiently.
Type | Description | Size |
| Very small integer | 8-bit |
| Small integer | 16-bit |
| Standard integer | 32-bit |
| Large integer | 64-bit |
| Decimal number (standard precision) | 32-bit |
| Decimal number (high precision) | 64-bit |
|
| ~1 bit (virtual) |
| Single Unicode character | 16-bit |
When we declare a primitive, based on its type, Java reserves a fixed amount of memory and places the value directly inside that slot.
Here is a quick example showing primitive assignments and prints.
Line 3: We declare an
int. Java reserves 32 bits of memory and stores the binary representation of8000directly in it.Line 4: We declare a
double. Java reserves 64 bits and stores the floating-point value19.99directly in it.Line 5: We declare a
boolean. Java reserves memory to store the single logic statetrue.Line 6: We declare a
char. Java reserves 16 bits to store the Unicode value for the character'A'.
Note: In the code above, we use the + symbol inside System.out.println. When used with text, this operator does not perform math; instead, it joins the text and the number together into a single sentence. This is called concatenation.
Reference types: Storing addresses
Reference types include everything that is not one of the eight primitives. This includes classes (like String), interfaces, and arrays.
Unlike primitives, objects can be large and complex. Java does not store these huge objects directly inside the variable. Instead, the variable stores a memory address (a reference) pointing to the object.
Think of the variable as a piece of paper with a distinct location written on it (like “Shelf A, Bin 4”). The actual data lives at that location.
We will focus on String as our main example. Even though a string looks like a simple value, it is actually a reference type.
Line 5: Java stores the text
"Hello Java"in the Heap (the large memory pool). Java creates the variablegreetingon the Stack. It does not put the text insidegreeting. Instead, it puts the address of the text insidegreeting.
Naming conventions for variables
Before we write our code, we must follow Java’s naming standards. By convention, variable names in Java use lowerCamelCase. This means the first word is lowercase, and every subsequent word starts with a capital letter (e.g., userAge, totalPrice, or accountBalance).
We should always use descriptive names that reflect the variable’s purpose to make our code readable for ourselves and others.
Memory management: Stack vs. heap
To understand why this distinction matters, we must examine how Java manages memory. Java divides memory into two main areas: the stack and the heap.
Stack: This is where method executions happen. When we run
main, a block of memory (a “stack frame”) is created. Local variables—whether they are primitives or references—live here. The stack is fast, temporary, and organized.Heap: This is a large, unstructured pool of memory used for storing objects. The heap is where the actual data for
String,Point, or any other object lives.
When we run the code Point p1 = new Point(10, 20);:
The
Pointobject (with x=10, y=20) is created on the heap.The
p1variable is created on the stack.The
p1variable on the stack stores the address (e.g.,0xFA31) of the object on the heap.
When we run int population = 8000;:
The variable
populationis created on the stack.The value
8000is stored directly insidepopulationon the stack.
Assignment behavior: Copying vs. aliasing
The difference between primitives and references becomes critical when we assign one variable to another using the = operator.
Primitive assignment: Copying values
When we assign one primitive to another, Java copies the bits from the source variable into the destination variable. Since the variable holds the value, we get a completely independent copy.
Line 4: We set
b = a. Java looks insidea, finds10, and copies10intob.Line 6: We change
b. Sincebhas its own memory slot,ais untouched.
Reference assignment: Aliasing
When we assign one reference variable to another, we are copying the address, not the data.
This creates aliasing: two variables that point to the exact same object. If we modify the object using one variable, the other variable “sees” the change immediately.
Java is always pass-by-value: when you pass an object to a method, Java copies the reference value, so both variables can refer to the same object.
To demonstrate this, we must use a mutable object (one that can be changed). Regular Strings are immutable, so we will use a StringBuilder, which is simply an editable String.
Line 4: We create a
StringBuilderobject containing “Start”.ref1holds its address.Line 7:
ref2 = ref1. We copy the address. Now bothref1andref2point to the same object in memory.Line 10: We use
ref2to append text. The object itself changes from “Start” to “Start+End”.Line 13: When we print
ref1, it looks at that same object and finds “Start+End”.
The null value
Because reference variables hold an address, it is possible for them to hold no address. This state is represented by the keyword null.
If a reference is null, it is not “zero” or “empty text.” It specifically means: this variable points to nothing. We cannot use it to access data or call methods.
Line 3: We declare
messagebut explicitly say it points nowhere.Line 5: The commented-out line shows the danger. If we try to ask
messagefor its length (.length()), Java tries to follow the address. Since there is no address, it crashes with aNullPointerException.Line 7: We can print the variable itself safely; Java will just print “null”.
Understanding null is vital because NullPointerException is the most common runtime error in Java. It simply means we tried to use a reference that wasn’t pointing to an object.
In this lesson, we established the boundary between primitives (values) and references (addresses). We saw that primitives live entirely on the Stack and copy by value, ensuring independence. We saw that references live on the Stack but point to the Heap, meaning assignment creates shared access (aliasing).