Understanding References and Pass-by-Value in Java

  • java
  • references
  • pointers
  • arrays
  • arraylist

Sources

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:

Main.java
JAVA
int x = 10;
boolean flag = true;
char letter = 'A';

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

Main.java
JAVA
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:

Main.java
JAVA
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 and reference variables
Stack
int score10
User user@A1
→
Heap
@A1 · Username: “Mina”level: 3

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:

Main.java
JAVA
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:

Main.java
JAVA
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
}
A copied reference still reaches the same object
Caller frame
user@A1
Method frame
parameter@A1
Heap
@A1 · Username: “Mina”shared object

Reassigning References

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

Main.java
JAVA
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
}
Reassignment changes only the local parameter
Caller frame
user@A1
Method frame
parameter@B2
Heap
@A1 · Username: “Mina”
@B2 · Username: “Noah”

Arrays and References

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

Main.java
JAVA
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:

Main.java
JAVA
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:

Main.java
JAVA
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:

Main.java
JAVA
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:

Main.java
JAVA
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:

Main.java
JAVA
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

Main.java
JAVA
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

Main.java
JAVA
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:

Main.java
JAVA
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.