Search⌘ K
AI Features

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.

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.

Primitives store values directly; references store addresses pointing to objects
Primitives store values directly; references store addresses pointing to objects

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

byte

Very small integer

8-bit

short

Small integer

16-bit

int

Standard integer

32-bit

long

Large integer

64-bit

float

Decimal number (standard precision)

32-bit

double

Decimal number (high precision)

64-bit

boolean

true or false

~1 bit (virtual)

char

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.

Java 25
public class PrimitivesExample {
public static void main(String[] args) {
int population = 8000;
double price = 19.99;
boolean isActive = true;
char grade = 'A';
System.out.println("Population: " + population);
System.out.println("Price: " + price);
}
}
  • Line 3: We declare an int. Java reserves 32 bits of memory and stores the binary representation of 8000 directly in it.

  • Line 4: We declare a double. Java reserves 64 bits and stores the floating-point value 19.99 directly in it.

  • Line 5: We declare a boolean. Java reserves memory to store the single logic state true.

  • 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.

Java 25
public class ReferenceExample {
public static void main(String[] args) {
// 'greeting' is the reference (holds the address)
// "Hello Java" is the object (stored separately in memory)
String greeting = "Hello Java";
System.out.println(greeting);
}
}
  • Line 5: Java stores the text "Hello Java" in the Heap (the large memory pool). Java creates the variable greeting on the Stack. It does not put the text inside greeting. Instead, it puts the address of the text inside greeting.

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);:

  1. The Point object (with x=10, y=20) is created on the heap.

  2. The p1 variable is created on the stack.

  3. The p1 variable on the stack stores the address (e.g., 0xFA31) of the object on the heap.

When we run int population = 8000;:

  1. The variable population is created on the stack.

  2. The value 8000 is stored directly inside population on 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.

Java 25
public class PrimitiveCopy {
public static void main(String[] args) {
int a = 10;
int b = a; // Copies the value 10 into b
b = 55; // Changes b only
System.out.println("a: " + a); // a remains 10
System.out.println("b: " + b); // b is 55
}
}
  • Line 4: We set b = a. Java looks inside a, finds 10, and copies 10 into b.

  • Line 6: We change b. Since b has its own memory slot, a is 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.

Java 25
public class ReferenceAlias {
public static void main(String[] args) {
// Create a modifiable string containing "Start"
StringBuilder ref1 = new StringBuilder("Start");
// Copy the address from ref1 to ref2
StringBuilder ref2 = ref1;
// Change the text using ref2
ref2.append("+End");
// ref1 sees the change because it points to the same object
System.out.println("ref1: " + ref1);
}
}
  • Line 4: We create a StringBuilder object containing “Start”. ref1 holds its address.

  • Line 7: ref2 = ref1. We copy the address. Now both ref1 and ref2 point to the same object in memory.

  • Line 10: We use ref2 to 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.

Java 25
public class NullExample {
public static void main(String[] args) {
String message = null; // message holds no address
// System.out.println(message.length()); // RUNTIME ERROR
System.out.println("Message is: " + message);
}
}
  • Line 3: We declare message but explicitly say it points nowhere.

  • Line 5: The commented-out line shows the danger. If we try to ask message for its length (.length()), Java tries to follow the address. Since there is no address, it crashes with a NullPointerException.

  • 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).