Appearance
Reference Types, Wrappers, and How Java Really Manages Memory
The previous chapter covered primitives: an int variable holds the number itself. Java has a second category of data types called reference types, and they work completely differently. Understanding this difference is not optional. It shows up in interviews constantly, it explains bugs that seem inexplicable at first, and it is the foundation for understanding everything from collections to threading to garbage collection.
This chapter covers the four reference types (class, String, interface, array), wrapper classes, autoboxing and unboxing, and constants.
What Makes Something a Reference Type
Let us start with what a reference type actually is, because the name is descriptive once you understand it.
When you declare a primitive variable like int a = 10, the variable a directly contains the value 10. The variable and the value are the same thing, living in the same place in memory.
Reference types are different. When you create an object, that object goes into a region of memory called the Heap. The variable you declared does not hold the object itself. It holds the memory address where the object lives. That address is the reference.
Think of it like a TV remote control and a television. The TV is the actual object sitting on your shelf. The remote control is not the TV. The remote just has information that lets you interact with the TV. If you hand someone else a copy of the remote, you both now control the same TV. Changes one person makes affect what the other person sees, because there is only one television.
In Java, the object on the Heap is the TV. The variable holding its address is the remote. You can have many variables all pointing to the same single object.
java
// Creating a class (a blueprint for objects)
class Employee {
int employeeId;
String name;
}
// Creating an object
Employee emp = new Employee();
emp.employeeId = 10;
// The memory picture:
// Stack: emp = [address: 0x7f4a2c00] <-- the variable holds an address
// Heap: at 0x7f4a2c00 --> { employeeId=10, name=null } <-- the actual objectThe variable emp is on the Stack. The Employee object is on the Heap. The variable only holds the address of the object. That is why this category is called reference types: the variable is a reference to the actual memory.
Now watch what happens when you assign one reference variable to another:
java
Employee obj2 = emp; // copies the address, NOT the objectNow both emp and obj2 hold the same address. They both point to the same Employee object on the Heap. There is still only one object.
java
obj2.employeeId = 30;
System.out.println(emp.employeeId); // prints 30 -- same object, changed through obj2This surprises beginners. But once you understand that both variables are just remote controls for the same TV, it makes complete sense. Changing the object through one reference changes it for everyone holding a reference to it.
Java Is ALWAYS Pass by Value
This is one of the most commonly asked Java interview questions, and a significant number of experienced developers still get it wrong.
People often say "Java passes objects by reference." This is incorrect. Java is strictly pass by value, always, with no exceptions. The confusion comes from not understanding what value is being copied.
When you pass a primitive to a method, Java copies the actual value. The method gets its own copy and can do whatever it wants with it without affecting the original.
java
static void modifyPrimitive(int x) {
x = 20; // only modifies the local copy in this method
}
int a = 10;
modifyPrimitive(a);
System.out.println(a); // still 10 -- the original was never touchedWhen you pass an object to a method, Java copies the reference (the address). The method gets a copy of the address. But here is the key: a copy of the address still points to the same object on the Heap.
java
static void modify(Employee e) {
e.employeeId = 20; // changes the actual object through the copied address
}
Employee emp = new Employee();
emp.employeeId = 10;
modify(emp);
System.out.println(emp.employeeId); // prints 20 -- the object was mutatedSo it looks like pass by reference because mutations are visible. But watch what happens when you try to reassign the parameter:
java
static void tryToReplace(Employee e) {
e = new Employee(); // only reassigns the LOCAL copy of the address
e.employeeId = 99;
}
Employee emp = new Employee();
emp.employeeId = 10;
tryToReplace(emp);
System.out.println(emp.employeeId); // still 10 -- caller's variable was NOT reboundThe method got a copy of the address. Reassigning that copy to point somewhere else does not affect the caller's variable. If Java were truly pass by reference, this reassignment would be visible to the caller. It is not. That is the proof that Java is pass by value.
The complete answer for any interview: "Java always passes a copy of the value. For primitives, that value is the data itself. For objects, that value is the memory address (reference). Mutations through a copied address affect the same Heap object and are visible to the caller. But reassigning the parameter variable does not affect the caller's reference. Java has no true pass by reference."
Strings: A Reference Type with Special Powers
String is a reference type in Java, not a primitive. Even though it looks like one and you use it constantly, there are important differences from primitives and important differences from ordinary reference types.
The most important one is the String Constant Pool.
The String Constant Pool
Inside the Heap, there is a special area called the String Constant Pool (sometimes called the String Pool or SCP). When you write a string literal in your code, the JVM does something clever before creating anything:
- It checks whether an identical string already exists in the pool.
- If it does, your variable gets a reference to that existing pooled object. No new object is created.
- If it does not, the JVM creates the string in the pool and your variable references it.
java
String s1 = "hello"; // "hello" created in the String Constant Pool
String s2 = "hello"; // "hello" already in pool -- s2 gets the SAME reference as s1Now s1 and s2 both point to the exact same object. There is only one "hello" in memory, shared by both variables.
This optimization saves memory and improves performance. In a large application, the same strings appear thousands of times. Without the pool, you would have thousands of duplicate objects eating up Heap space.
What new String() Does Differently
When you use the new keyword with String, you always get a brand new object outside the pool:
java
String s3 = new String("hello"); // creates a NEW object on the Heap, OUTSIDE the poolNow s1 and s3 both contain the text "hello", but they are two completely separate objects in different locations in memory. s3 is not in the String Constant Pool. It is just an ordinary Heap object.
The == vs .equals() Distinction
This is where the pool creates one of Java's most common bugs and interview questions.
The == operator, when used on reference types, compares memory addresses. It asks: "Are these two variables pointing to the exact same object?"
The .equals() method on String compares the actual character content. It asks: "Do these two strings contain the same text?"
java
String s1 = "hello";
String s2 = "hello";
String s3 = new String("hello");
// == compares addresses (object identity)
System.out.println(s1 == s2); // true -- same pooled object, same address
System.out.println(s1 == s3); // false -- s3 is a different object at a different address
// .equals() compares content
System.out.println(s1.equals(s2)); // true -- both contain "hello"
System.out.println(s1.equals(s3)); // true -- both contain "hello"Never use == to compare String values in real code. Always use .equals(). The == result depends on whether the string came from the pool or from new String(), which is an implementation detail you should not rely on.
Why Strings Are Immutable
Once a String object is created, it cannot be changed.
When you write this:
java
String s1 = "hello";
String s2 = "hello";
s1 = "hello world";You have not modified the string "hello". You have created a new string "hello world" (or found it in the pool if it already exists) and pointed s1 at it. The original "hello" string is unchanged. s2 still safely references "hello".
Now think about why this matters for the pool. The pool works because multiple variables share the same object. If strings were mutable, one variable could change "hello" to "goodbye" and every other variable pointing to the same pooled object would suddenly see "goodbye". That would be catastrophic. Immutability is what makes sharing safe.
Interface References
An interface is also a reference type. You cannot create an instance of an interface directly, but you can declare a variable of an interface type and store any object whose class implements that interface.
java
interface Person {
String profession();
}
class Engineer implements Person {
public String profession() {
return "Software Engineer";
}
}
class Teacher implements Person {
public String profession() {
return "Teacher";
}
}
// Interface reference holding a child class object -- valid
Person p1 = new Engineer();
Person p2 = new Teacher();
// This would be a compile error -- cannot instantiate an interface
// Person p3 = new Person();
System.out.println(p1.profession()); // Software Engineer
System.out.println(p2.profession()); // TeacherThe variable p1 is typed as Person, but the actual object is an Engineer. When you call p1.profession(), Java calls Engineer's implementation at runtime. This is polymorphism. You will explore this much more deeply when you study interfaces and inheritance.
You can also store an object in a variable of its own type:
java
Engineer e = new Engineer(); // storing in same class type
Person p = e; // also valid, since Engineer implements PersonBoth variables point to the same Engineer object. The type of the variable determines which methods you can call. Through a Person reference, you can only call methods declared in Person. Through an Engineer reference, you can call all methods on Engineer.
Arrays Are Reference Types Too
Arrays in Java are objects. When you create an array, the array object is allocated on the Heap and your array variable holds a reference to it, just like any other reference type.
java
// 1D array
int[] arr = new int[5]; // creates an int array of 5 elements, all defaulting to 0
arr[3] = 40;
// Array literal -- size is inferred from the initializer
int[] numbers = {30, 20, 10, 40, 50};
// 2D array
int[][] matrix = new int[5][4]; // 5 rows, 4 columns, all zeros
matrix[2][2] = 20; // set row 2, column 2 to 20
// 2D array literal
int[][] grid = { {1, 5, 7}, {4, 2, 3} };
// grid[1][2] == 3 (row index 1, column index 2)The square brackets can go before or after the variable name. Both int[] arr and int arr[] are legal Java. The first form is preferred by convention.
Because arrays are objects on the Heap, assigning one array variable to another copies the reference, not the array:
java
int[] a = {1, 2, 3};
int[] b = a; // b points to the same array object
b[0] = 99;
System.out.println(a[0]); // 99 -- same array, changed through bThis is the same remote control behavior you saw with Employee objects.
Wrapper Classes: Giving Primitives Object Superpowers
Primitives are fast and efficient, but they have limitations. The most painful limitation appears when you try to use them with Java's collection framework. You cannot do this:
java
List<int> numbers = new ArrayList<>(); // COMPILE ERROR -- int is not an object typeJava's generic types (List<T>, Map<K, V>, Set<T>) require objects. Primitives are not objects. They cannot be used as generic type parameters.
To solve this, Java provides a wrapper class for each primitive type. A wrapper class is an ordinary Java class that encapsulates a primitive value inside an immutable Heap object, making it compatible with collections and generics.
| Primitive | Wrapper class |
|---|---|
byte | Byte |
short | Short |
int | Integer |
long | Long |
float | Float |
double | Double |
char | Character |
boolean | Boolean |
Most wrapper names are the capitalized primitive name. The exceptions are int → Integer and char → Character.
The main reason wrappers exist is the one above: collections such as ArrayList and HashMap work only with objects, so to keep int values in a list you use List<Integer>.
Wrapper objects are immutable, just like String. So passing an Integer to a method does not let the method change the caller's value: assigning a new value inside the method only points the parameter at a different Integer object.
java
static void modify(Integer x) {
x = 20; // x now points to a different Integer object
}
Integer a = 10;
modify(a);
System.out.println(a); // 10Autoboxing and Unboxing
Writing Integer.valueOf(10) every time you wanted to add a number to a list would be exhausting. Java solves this with autoboxing and unboxing.
Autoboxing is the automatic conversion from a primitive to its wrapper object. Unboxing is the automatic conversion from a wrapper object back to its primitive. The compiler inserts these conversions for you invisibly.
java
Integer a = 10; // AUTOBOXING: compiler converts int 10 to Integer.valueOf(10)
int x = a; // UNBOXING: compiler extracts the int value from the Integer object
List<Integer> list = new ArrayList<>();
list.add(5); // AUTOBOXING: 5 (int primitive) becomes Integer(5) object
int first = list.get(0); // UNBOXING: Integer(5) becomes int 5Autoboxing and unboxing happen transparently. You write natural code and the compiler handles the conversions.
Constants: When a Value Should Never Change
A constant in Java is a variable whose value is set once and never changes. You declare constants using two keywords together: static and final, plus the convention of ALL_CAPS naming.
java
class MathConstants {
static final double PI = 3.14159265358979;
static final int MAX_CONNECTIONS = 100;
static final String DEFAULT_HOST = "localhost";
}static means there is one copy of this variable for the entire class, not one per object. All instances of the class share it. You do not need to create an object to access it.
final means the value cannot be reassigned after it is initialized. Any attempt to reassign a final variable is a compile error.
java
static final int EMPLOYEE_ID = 10;
// Later in code:
EMPLOYEE_ID = 20; // COMPILE ERROR -- cannot assign a value to a final variableTogether, static and final create a true constant: one copy for the whole program, and that copy never changes.
Without static, a final variable would still be unchangeable, but each object would have its own copy. That is useful for instance constants but wasteful for program wide constants. Adding static ensures you are not creating redundant copies.
Without final, a static variable would be shared but mutable. Any object could change it and corrupt the shared value for everyone. Adding final prevents that.
The ALL_CAPS naming with underscores separating words (like MAX_VALUE, DEFAULT_HOST) is a Java convention that signals to readers that this is a constant, not a regular variable.
Check yourself
- What are the four reference data types covered here?
Employee b = a; b.employeeId = 30;What isa.employeeIdnow, and why?- Is Java pass by value or pass by reference? How does a method still change an object you pass to it?
String s1 = "hi"; String s2 = "hi"; String s3 = new String("hi");What dos1 == s2,s1 == s3ands1.equals(s3)print?- What does "strings are immutable" mean?
- Why can you not write
new Person()whenPersonis an interface, yetPerson p = new Engineer();works? - What are autoboxing and unboxing, and why do wrapper classes exist?
- How do you declare a constant?
Answers
- Class, String, interface, and array.
- 30. Assigning a reference copies the address, so
aandbpoint to the same object. - Always by value. For an object the value copied is the reference, so the method works on the same object in the Heap and its changes are visible to the caller.
true,false,true. Equal literals share one object in the String Constant Pool;new Stringcreates a separate Heap object.==compares references,equalscompares content.- A String object never changes.
s1 = "hello world"makess1point to a different String; the old"hello"is untouched. - An interface cannot be instantiated. A variable of the interface type can hold a reference to an object of any class that implements it.
- Autoboxing converts a primitive to its wrapper (
Integer a = 10); unboxing converts back (int x = a). Wrappers exist because collections work only with objects. static final, named in capitals:static final int EMPLOYEE_ID = 10;.staticgives one shared copy;finalstops it from being changed.