Arrays and Methods
Reference semantics, memory diagrams, and the Arrays utility class
After this lesson, you will be able to:
- Pass arrays to methods and explain why the method can modify the original array
- Draw stack/heap memory diagrams showing array references
- Distinguish value semantics (primitives) from reference semantics (arrays)
- Use
Arrays.toString,Arrays.sort, andArrays.copyOffromjava.util.Arrays - Describe
ArrayListas a resizable alternative to arrays and use its basic operations
A Method That Breaks the Rules
Every method you have written so far follows a simple rule: parameters are copies. Change the parameter inside the method, and the caller’s variable is untouched. Watch what happens with arrays:
public static void addFive(int[] grades) {
for (int i = 0; i < grades.length; i++) {
grades[i] += 5;
}
}
public static void main(String[] args) {
int[] scores = {78, 85, 62, 91, 70};
addFive(scores);
System.out.println(Arrays.toString(scores));
// [83, 90, 67, 96, 75] — the original changed!
}
addFive never returned anything. It never assigned to scores. Yet the original array is different after the call. This is not a bug — it is how Java arrays work, and understanding why requires looking at memory.
Reference Semantics: Why Arrays Are Different
When you declare a primitive variable, the variable holds the value directly:
int x = 42;
Stack
┌──────────┐
│ x: 42 │
└──────────┘
When you declare an array, the variable does not hold the data. It holds a reference — an address pointing to an object on the heap:
int[] scores = {78, 85, 62, 91, 70};
Stack Heap
┌──────────────┐ ┌────┬────┬────┬────┬────┐
│ scores: ─────┼───────────►│ 78 │ 85 │ 62 │ 91 │ 70 │
└──────────────┘ └────┴────┴────┴────┴────┘
The variable scores is a small box on the stack containing an arrow. The actual data lives on the heap.
What Happens During a Method Call
When you call addFive(scores), Java copies the reference, not the array:
Stack Heap
┌──────────────────┐
│ main: │
│ scores: ───────┼──────┐
├──────────────────┤ │ ┌────┬────┬────┬────┬────┐
│ addFive: │ ├────►│ 78 │ 85 │ 62 │ 91 │ 70 │
│ grades: ───────┼──────┘ └────┴────┴────┴────┴────┘
└──────────────────┘
Both scores (in main) and grades (in addFive) point to the same array object on the heap. When addFive writes grades[i] += 5, it modifies that shared object. After the method returns, main sees the changes through scores because nothing was copied — both references point to the same memory.
This is called reference semantics. The reference is copied, but the data is shared.
Contrast: Primitives Are Independent
Compare with a method that takes an int:
public static void addFive(int n) {
n += 5;
}
int x = 10;
addFive(x);
System.out.println(x); // still 10
Stack
┌──────────────────┐
│ main: │
│ x: 10 │ ← independent copy
├──────────────────┤
│ addFive: │
│ n: 15 │ ← modified copy, discarded on return
└──────────────────┘
The method gets its own copy of 10. Changing n has no effect on x. This is value semantics.
From CSCD 110: In Python, lists behave the same way — passing a list to a function lets the function modify the original.
def add_five(lst): lst[0] += 5changes the caller’s list. Java arrays and Python lists are both reference types. The difference is that Java makes this explicit with types:intis a value,int[]is a reference. Python hides this distinction — everything is a reference, but immutable types likeintappear to behave like values.
The Grade Curve: A Complete Example
Here is a realistic program that uses methods to transform an array of grades. Every method receives the array by reference and modifies it in place.
import java.util.Arrays;
public class GradeCurve {
// Add points to every grade (cap at 100)
public static void curve(int[] grades, int points) {
for (int i = 0; i < grades.length; i++) {
grades[i] = Math.min(grades[i] + points, 100);
}
}
// Replace the lowest grade with a new value
public static void replaceLowest(int[] grades, int replacement) {
int minIndex = 0;
for (int i = 1; i < grades.length; i++) {
if (grades[i] < grades[minIndex]) {
minIndex = i;
}
}
grades[minIndex] = replacement;
}
// Return a new array with only passing grades (>= 60)
public static int[] passingOnly(int[] grades) {
int count = 0;
for (int g : grades) {
if (g >= 60) count++;
}
int[] result = new int[count];
int index = 0;
for (int g : grades) {
if (g >= 60) {
result[index] = g;
index++;
}
}
return result;
}
public static void main(String[] args) {
int[] grades = {78, 85, 42, 91, 55, 70, 63};
System.out.println("Original: " + Arrays.toString(grades));
curve(grades, 5);
System.out.println("Curved +5: " + Arrays.toString(grades));
replaceLowest(grades, 65);
System.out.println("Drop low: " + Arrays.toString(grades));
int[] passing = passingOnly(grades);
System.out.println("Passing: " + Arrays.toString(passing));
}
}
Output:
Original: [78, 85, 42, 91, 55, 70, 63]
Curved +5: [83, 90, 47, 96, 60, 75, 68]
Drop low: [83, 90, 65, 96, 60, 75, 68]
Passing: [83, 90, 65, 96, 60, 75, 68]
Notice that curve and replaceLowest modify the original array and return void. They work through side effects. passingOnly takes a different approach: it builds and returns a new array, leaving the original untouched. Both patterns are common. Use in-place modification when you want to transform existing data. Return a new array when you want to filter or reshape without destroying the original.
What does this code print?
public static void zero(int[] arr) { arr[0] = 0; }
int[] data = {5, 10, 15};
zero(data);
System.out.println(data[0]);
A Subtlety: Reassigning the Parameter
Reference semantics lets a method modify the array’s contents. But it cannot replace the caller’s variable:
public static void replace(int[] arr) {
arr = new int[] {99, 99, 99}; // reassigns local reference
}
int[] data = {1, 2, 3};
replace(data);
System.out.println(Arrays.toString(data)); // [1, 2, 3] — unchanged!
Here is the memory picture inside replace:
Stack Heap
┌──────────────────┐
│ main: │ ┌───┬───┬───┐
│ data: ─────────┼──────────►│ 1 │ 2 │ 3 │ (original)
├──────────────────┤ └───┴───┴───┘
│ replace: │ ┌────┬────┬────┐
│ arr: ──────────┼───────────►│ 99 │ 99 │ 99 │ (new object)
└──────────────────┘ └────┴────┴────┘
arr = new int[]{99, 99, 99} makes arr point to a brand-new array. It does not change where data points. When replace returns, the new array is garbage collected. The rule: a method can modify the array’s elements through the reference, but it cannot make the caller’s variable point somewhere else.
A method receives an int[] parameter. Which of these can the method do?
The Arrays Utility Class
Java provides java.util.Arrays with static methods that save you from writing common array operations by hand. You have already seen Arrays.toString. Here are the methods you will use most.
Arrays.toString
Converts an array to a readable string. Without it, printing an array gives you a useless memory address like [I@6d06d69c.
int[] data = {3, 1, 4, 1, 5};
System.out.println(Arrays.toString(data)); // [3, 1, 4, 1, 5]
Arrays.sort
Sorts the array in place (modifies the original). Uses a fast algorithm — much faster than selection sort or insertion sort for large arrays.
int[] data = {3, 1, 4, 1, 5};
Arrays.sort(data);
System.out.println(Arrays.toString(data)); // [1, 1, 3, 4, 5]
Because Arrays.sort modifies the array in place, the original order is gone after sorting. If you need to keep the original, make a copy first.
Arrays.copyOf
Creates a new, independent copy of an array. The second argument specifies the length of the new array:
int[] original = {10, 20, 30};
// Exact copy
int[] copy = Arrays.copyOf(original, original.length);
copy[0] = 99;
System.out.println(Arrays.toString(original)); // [10, 20, 30] — safe!
// Shorter copy (truncates)
int[] first2 = Arrays.copyOf(original, 2); // [10, 20]
// Longer copy (pads with zeros)
int[] padded = Arrays.copyOf(original, 5); // [10, 20, 30, 0, 0]
This is the right way to duplicate an array. The assignment int[] b = a creates an alias, not a copy.
Arrays.equals
Compares two arrays element by element. The == operator compares references (are they the same object?), which is almost never what you want:
int[] a = {1, 2, 3};
int[] b = {1, 2, 3};
System.out.println(a == b); // false (different objects)
System.out.println(Arrays.equals(a, b)); // true (same contents)
Summary Table
| Method | What It Does | Modifies Original? |
|---|---|---|
Arrays.toString(arr) |
Readable string representation | No |
Arrays.sort(arr) |
Sorts in ascending order | Yes |
Arrays.copyOf(arr, len) |
Returns a new copy with given length | No |
Arrays.equals(a, b) |
Element-by-element equality check | No |
Arrays.fill(arr, val) |
Sets every element to val |
Yes |
You need to sort an array but also keep the original order for later. What should you do?
ArrayList: A Resizable Alternative
Arrays have one serious limitation: their size is fixed at creation. If you allocate new int[5], you get exactly 5 slots. You cannot add a sixth element.
Java’s ArrayList is a class that wraps an array internally and grows automatically as you add elements. Think of it as a resizable array with methods instead of bracket syntax.
import java.util.ArrayList;
ArrayList<String> names = new ArrayList<>();
names.add("Alice"); // [Alice]
names.add("Bob"); // [Alice, Bob]
names.add("Charlie"); // [Alice, Bob, Charlie]
System.out.println(names.size()); // 3
System.out.println(names.get(1)); // Bob
names.remove(0); // [Bob, Charlie]
ArrayList vs. Array Syntax
| Operation | Array | ArrayList |
|---|---|---|
| Create | int[] a = new int[5]; |
ArrayList<Integer> a = new ArrayList<>(); |
| Get element | a[i] |
a.get(i) |
| Set element | a[i] = value; |
a.set(i, value); |
| Size | a.length |
a.size() |
| Add element | (not possible) | a.add(value); |
| Remove | (manual shifting) | a.remove(i); |
Arrays.toString(a) |
a.toString() (built-in) |
Generics and Wrapper Types
ArrayList uses generics (the <Type> syntax) to specify what it holds. It requires object types, not primitives. Java provides wrapper classes and automatically converts between them (autoboxing):
| Primitive | Wrapper |
|---|---|
int |
Integer |
double |
Double |
boolean |
Boolean |
char |
Character |
ArrayList<Integer> numbers = new ArrayList<>();
numbers.add(42); // autoboxing: int → Integer
int x = numbers.get(0); // unboxing: Integer → int
You cannot write ArrayList<int>. It must be ArrayList<Integer>.
When to Use Each
Use an array when:
- The size is known and fixed
- You need primitive performance (no autoboxing overhead)
- A method signature requires
int[](many library methods do)
Use an ArrayList when:
- The size changes at runtime (reading an unknown number of inputs)
- You need to add or remove elements frequently
- Convenience methods like
containsandindexOfare useful
We will use ArrayList extensively starting in the next unit. For now, know that it exists and understand the tradeoff: flexibility at the cost of slightly more verbose syntax and minor performance overhead.
Quick Reference
| Concept | Key Point |
|---|---|
| Reference semantics | Array variables hold references, not data. Assignment copies the reference. |
| Passing to methods | Methods receive a copy of the reference. They can modify contents, not reassign the caller’s variable. |
Arrays.toString |
Readable string. Use instead of println(arr). |
Arrays.sort |
Sorts in place. Original order is lost. |
Arrays.copyOf |
Independent copy. Use before sorting to preserve original. |
Arrays.equals |
Element-by-element comparison. Use instead of ==. |
ArrayList |
Resizable collection. Uses generics, wrapper types, and method calls instead of brackets. |
Summary
Arrays are reference types. When you pass an array to a method, the method receives a copy of the reference — not a copy of the data. Both the caller and the method see the same array on the heap. This means methods can modify array contents as a side effect, which is why sorting methods like Arrays.sort return void — they rearrange the original.
The java.util.Arrays class provides essential utilities: toString for printing, sort for ordering, copyOf for independent copies, and equals for content comparison. These save you from writing manual loops for common operations.
When you need a collection that can grow and shrink, ArrayList is the standard choice. It trades the bracket syntax and primitive support of arrays for the ability to add and remove elements dynamically.
Next lesson: File input with Scanner — reading data from files instead of the keyboard.