Understanding References and Pass-by-Value in Java

May 1, 2025

javareferencespointersarraysarraylist

Understanding References and Pass-by-Value in Java

Java’s handling of objects, arrays, and collections can be confusing for beginners and even experienced programmers. This tutorial clarifies how Java manages memory, passes arguments to methods, and why understanding references is crucial for effective programming.

Java Memory Model: Primitives vs. Objects

In Java, memory is divided into two main areas:

  1. Stack: Stores primitive values and references to objects
  2. Heap: Stores the actual objects

Primitive Types

Primitive types (int, boolean, char, etc.) store their values directly in memory:

int x = 10;
boolean flag = true;
char letter = 'A';

When you assign a primitive to another variable, Java creates a copy of the value:

int x = 10;
int y = x;  // y gets a copy of x's value
x = 20;     // changing x doesn't affect y
System.out.println(y);  // Still prints 10

Reference Types

Reference types (objects, arrays, etc.) store a reference (memory address) to the actual object:

String name = "Java";  // 'name' holds a reference to the String object
Person person = new Person("Alice");  // 'person' holds a reference to the Person object

Primitive vs Reference Variables

Stack
int x = 10
10
Stack
Person p
0x1234
Heap
Person Object (0x1234)
name: "Alice"
age: 30

Java is Always Pass-by-Value

A common misconception is that Java uses “pass-by-reference” for objects. In reality, Java always uses pass-by-value, but the value being passed is a reference for objects.

Pass-by-Value with Primitives

When you pass a primitive to a method, Java passes a copy of the value:

public static void main(String[] args) {
    int number = 10;
    modifyValue(number);
    System.out.println(number);  // Still prints 10
}

public static void modifyValue(int x) {
    x = 20;  // Modifies the local copy, not the original
}

Pass-by-Value with References

When you pass an object to a method, Java passes a copy of the reference:

public static void main(String[] args) {
    Person person = new Person("Alice");
    modifyPerson(person);
    System.out.println(person.getName());  // Prints "Bob"
}

public static void modifyPerson(Person p) {
    p.setName("Bob");  // Modifies the object that p refers to
}

Pass-by-Value with References

main() Stack Frame
Person person
0x1234
modifyPerson() Stack Frame
Person p
0x1234
Heap
Person Object (0x1234)
name: "Bob" (changed)

Reassigning References

If you reassign the reference inside a method, it doesn’t affect the original reference:

public static void main(String[] args) {
    Person person = new Person("Alice");
    replacePerson(person);
    System.out.println(person.getName());  // Still prints "Alice"
}

public static void replacePerson(Person p) {
    p = new Person("Bob");  // p now points to a new object
    // The original reference is unchanged
}

Reassigning References

main() Stack Frame
Person person
0x1234
replacePerson() Stack Frame
Person p
0x5678
Heap
Person Object (0x1234)
name: "Alice" (unchanged)
Person Object (0x5678)
name: "Bob" (new object)

Arrays and References

Arrays in Java are objects, so they behave like reference types:

int[] numbers = {1, 2, 3, 4, 5};

Here, numbers is a reference to an array object in the heap.

Modifying Array Elements

When you pass an array to a method, you can modify its elements:

public static void main(String[] args) {
    int[] numbers = {1, 2, 3, 4, 5};
    modifyArray(numbers);
    System.out.println(numbers[0]);  // Prints 99
}

public static void modifyArray(int[] arr) {
    arr[0] = 99;  // Modifies the original array
}

Reassigning Arrays

If you reassign the array reference inside a method, it doesn’t affect the original reference:

public static void main(String[] args) {
    int[] numbers = {1, 2, 3, 4, 5};
    replaceArray(numbers);
    System.out.println(numbers[0]);  // Still prints 1
}

public static void replaceArray(int[] arr) {
    arr = new int[]{99, 98, 97};  // arr now points to a new array
    // The original reference is unchanged
}

ArrayLists and References

ArrayLists behave similarly to arrays since they are also objects:

ArrayList<String> names = new ArrayList<>();
names.add("Alice");
names.add("Bob");

Modifying ArrayList Elements

When you pass an ArrayList to a method, you can modify its elements:

public static void main(String[] args) {
    ArrayList<String> names = new ArrayList<>();
    names.add("Alice");
    names.add("Bob");
    
    modifyList(names);
    System.out.println(names.get(0));  // Prints "Charlie"
}

public static void modifyList(ArrayList<String> list) {
    list.set(0, "Charlie");  // Modifies the original list
    list.add("David");       // Adds to the original list
}

Common Misconceptions

A common mistake is thinking that creating a new ArrayList inside a method will affect the original:

public static void main(String[] args) {
    ArrayList<String> names = new ArrayList<>();
    names.add("Alice");
    names.add("Bob");
    
    replaceList(names);
    System.out.println(names.size());  // Still prints 2
}

public static void replaceList(ArrayList<String> list) {
    list = new ArrayList<>();  // list now points to a new ArrayList
    list.add("Charlie");       // Adds to the new list, not the original
    // The original reference is unchanged
}

Practical Examples

Example 1: Swapping Values

public static void main(String[] args) {
    int a = 5;
    int b = 10;
    
    // This won't work
    swap(a, b);
    System.out.println("a = " + a + ", b = " + b);  // Still a = 5, b = 10
    
    // This will work
    int[] values = {a, b};
    swapInArray(values);
    a = values[0];
    b = values[1];
    System.out.println("a = " + a + ", b = " + b);  // Now a = 10, b = 5
}

public static void swap(int x, int y) {
    int temp = x;
    x = y;
    y = temp;
    // Changes are local to this method
}

public static void swapInArray(int[] arr) {
    int temp = arr[0];
    arr[0] = arr[1];
    arr[1] = temp;
    // Changes affect the original array
}

Example 2: Modifying a Complex Object

class Student {
    private String name;
    private int[] grades;
    
    // Constructor and getters/setters...
}

public static void main(String[] args) {
    Student student = new Student("Alice", new int[]{85, 90, 78});
    
    updateGrades(student);
    // The student's grades are updated
    
    replaceGrades(student);
    // The student's grades remain the same
}

public static void updateGrades(Student s) {
    s.getGrades()[0] = 95;  // Modifies the original grades array
}

public static void replaceGrades(Student s) {
    int[] newGrades = {100, 100, 100};
    s.setGrades(newGrades);  // Updates the reference in the student object
}

String: A Special Case

Strings in Java are immutable, which means once created, they cannot be changed:

public static void main(String[] args) {
    String name = "Alice";
    modifyString(name);
    System.out.println(name);  // Still prints "Alice"
}

public static void modifyString(String s) {
    s = s + " Smith";  // Creates a new String, doesn't modify the original
}

This is because String methods that appear to “modify” the string actually return a new String object.

Conclusion

Understanding how Java handles references is crucial for effective programming:

  1. Java is always pass-by-value
  2. For objects, the value passed is a copy of the reference
  3. You can modify an object’s state through its reference
  4. Reassigning a reference inside a method doesn’t affect the original reference
  5. Arrays and ArrayLists follow the same rules as other objects

By mastering these concepts, you’ll avoid common pitfalls and write more predictable Java code, especially when working with arrays, collections, and complex objects.