Arrays as Method Parameters
Pass-by-value applied to a reference: the method can change your data
In a nutshell
In lesson 5b you learned the rule: Java passes parameters by value. For primitives, that meant a method could not change the caller’s variable. The method only saw a copy.
The rule is the same for arrays — but the value being copied is now a reference, a pointer to the array on the heap. Both the caller and the method end up with their own copy of the reference, but those references both point at the same array. So:
- A method can write into the slots of the caller’s array, and the caller will see those writes. (Pass-by-value passed the reference; aliasing did the rest.)
- A method that replaces its parameter with a brand-new array (
arr = new int[10]) only changes its own copy of the reference. The caller’s reference is untouched. - To “grow” or “shrink” an array, the method must build a new array and return it. The caller stores the returned reference.
Today you learn three patterns that follow from those rules: mutate-in-place, return-a-new-array (append/remove), and the two-pass count-then-fill pattern (the withoutEvens example from class).
Today in three sentences. Passing an array passes the reference. The method can write into your slots, but cannot rebind your variable. To grow or shrink, return a new array.
After this lesson, you will be able to:
- Predict whether a method that takes an array parameter will (or will not) change the caller’s data, given the method’s body.
- Write a
voidmethod that mutates an array in place and a non-voidmethod that returns a fresh array of a different size. - Apply the two-pass count-then-fill pattern (count how many slots you need, allocate, walk again to fill).
- Explain in one sentence why
arr = newArray;inside a method has no effect on the caller’s variable.
From CSCD 110. Python’s behavior is identical in spirit. Passing a list lets the function
lst.append(...)and the caller sees the change; rebindinglst = []inside the function does not. Java’s mechanic is “pass-by-value of a reference” and Python’s is “pass-by-object-reference,” but for the user the rules feel the same: mutate-through-the-name, yes; rebind-the-name, no.
Reference semantics: passing the address, not the contents
Picture the call. The caller has a variable data that names an array. When the caller writes frobnicate(data), Java copies the reference (the address of the array on the heap) into the parameter arr of frobnicate. The array itself is not copied. After the call has begun, both names point at the one array:
caller's data ----\
\---> [ 7 | 2 | 9 | 4 ]
/
method's arr ----/
This is just aliasing (lesson 6b), now arranged across a method boundary. Any write through arr[i] lands in the same slots that data[i] names, because there is only one array.
Two consequences. First, methods that take arrays do not need to return the array to communicate changes; they can mutate the array directly and the caller will see the new contents. Second, students who think someMethod(data) will be safe because “Java copies the parameter” are about to learn otherwise.
public static void zeroOut(int[] arr) {
for (int i = 0; i < arr.length; i++) {
arr[i] = 0;
}
}
int[] data = {1, 2, 3, 4, 5};
zeroOut(data);
System.out.println(data[0] + " " + data[2] + " " + data[4]);
// prints: 0 0 0
Inside zeroOut, the parameter arr is a fresh local variable. But its initial value is the same reference the caller passed in. So arr[i] = 0 writes into the slots of the caller’s array, not into a separate copy.
Mutation visible to caller
That property is sometimes a feature, sometimes a hazard. As a feature, it lets a method do useful work that “outlasts” the call. As a hazard, it means you cannot pass an array to a method written by someone else without checking whether they will modify it.
A useful mutate-in-place example: fill an array with consecutive integers starting from a given value.
public static void fillWithCounting(int[] arr, int start) {
for (int i = 0; i < arr.length; i++) {
arr[i] = start + i;
}
}
int[] xs = new int[5];
fillWithCounting(xs, 10);
// xs is now {10, 11, 12, 13, 14}
The method returns nothing (void). It does not need to. Its job was the side effect of writing into the caller’s array, and that effect is visible the moment the method returns.
A second example: swap two slots in place.
public static void swap(int[] arr, int i, int j) {
int temp = arr[i];
arr[i] = arr[j];
arr[j] = temp;
}
Notice this does swap, even though the previous lesson’s primitive-only swap(int a, int b) could not. The difference is exactly the reference semantics: this method is operating on the caller’s array directly, so writes stick.
Documenting the mutation matters. A reader who sees swap(scores, 3, 7) should not have to guess whether scores will change. Reges-style Javadoc spells it out:
/**
* Swaps the values at indices i and j in the given array.
* Modifies arr in place; the array passed in will have its slots i and j exchanged.
*
* @param arr the array to modify (must be non-null)
* @param i the first index (0 <= i < arr.length)
* @param j the second index (0 <= j < arr.length)
*/
public static void swap(int[] arr, int i, int j) { ... }
Common pitfall: a method that “doesn’t seem to change anything” does change the caller’s array. If you read a method body and see
arr[i] = somethinganywhere, that change is visible to the caller. There is no “private” version of the array inside the method. The only way to keep the caller’s data untouched is to copy it first (or have the method allocate its own working array).
Check your understanding. What does this print?
public static void doubleAll(int[] arr) { for (int i = 0; i < arr.length; i++) { arr[i] = arr[i] * 2; } } int[] xs = {3, 5, 7}; doubleAll(xs); System.out.println(xs[0] + " " + xs[1] + " " + xs[2]);Reveal answer
Output:
6 10 14. The method writes through the shared reference into the caller’s array. Noreturnis needed because the change is already visible to the caller through the same array object.
The “I tried to grow it” trap
Here is where pass-by-value rears its head. Inside a method, you can rebind the parameter to point at a different array. The caller’s variable does not follow.
public static void grow(int[] arr) {
arr = new int[arr.length + 1]; // rebinds the local parameter only
arr[arr.length - 1] = 999; // writes into the new local array
}
int[] data = {1, 2, 3};
grow(data);
System.out.println(data.length); // prints 3, not 4
System.out.println(data[2]); // prints 3, not 999
Walk through what happens. grow is called with a copy of data’s reference. The first line of grow reassigns its parameter arr to a brand-new 4-slot array. From that point on, the parameter arr and the caller’s data point at different arrays. The write arr[arr.length - 1] = 999 lands in the new local array, which goes out of scope and is garbage-collected when the method returns. The caller’s data is untouched.
This is exactly the same rule as primitive pass-by-value. Reassigning a parameter inside a method changes only the local copy. The reference the caller holds was duplicated for the call, and the caller’s copy is unaffected by anything the method does to its own copy.
So you cannot grow or shrink the caller’s array from inside a method. You can only mutate the elements of the caller’s array (because that goes through the shared reference). To produce an array of different size, you have to build a new one and return it, and the caller has to store the return.
Common pitfall: writing
arr = ...inside a method and expecting it to propagate. It will not. The compiler is happy, the program runs, and nothing changes for the caller. The fix is to return the new array and have the caller assign it.
Check your understanding. What does this program print?
public static void replace(int[] arr) { arr = new int[]{99, 99, 99}; } int[] data = {1, 2, 3}; replace(data); System.out.println(data[0] + " " + data[1] + " " + data[2]);Reveal answer
Output:
1 2 3. Insidereplace, the parameter is rebound to a new local array; the caller’sdatareference is unchanged and still points at the original{1, 2, 3}. The new local array{99, 99, 99}is unreachable after the method returns and is garbage-collected.
Building and returning a new array
The pattern for “I want an array of a different size” is: declare the method with an array return type, build the new array inside, copy/transform/skip elements, return the new array. The caller assigns the return value to a variable.
Append (one bigger)
public static int[] append(int[] arr, int value) {
int[] result = new int[arr.length + 1];
for (int i = 0; i < arr.length; i++) {
result[i] = arr[i];
}
result[result.length - 1] = value;
return result;
}
int[] xs = {3, 1, 4};
xs = append(xs, 5); // caller MUST reassign to see the new array
// xs is now {3, 1, 4, 5}
The caller has to write xs = append(xs, 5);. If they write only append(xs, 5);, the new array is computed and immediately discarded. The compiler does not warn (the method returns int[], not void, so dropping the value is legal Java even when it is almost certainly a bug).
Remove by index (one smaller)
public static int[] removeAt(int[] arr, int target) {
int[] result = new int[arr.length - 1];
int j = 0;
for (int i = 0; i < arr.length; i++) {
if (i == target) continue; // skip the target slot
result[j] = arr[i];
j++;
}
return result;
}
int[] xs = {3, 1, 4, 1, 5};
xs = removeAt(xs, 2); // remove the slot at index 2
// xs is now {3, 1, 1, 5}
Two indices in this loop. i walks the input array. j walks the (shorter) output array. They match up except at the slot being skipped, where j lags one behind. This two-index pattern shows up again in any method that does selective copying.
Two-pass count-then-fill
The classic puzzle: write a method that returns a new array containing every odd value from the input, in order. The catch is that you do not know the size of the result until you have walked the array once. So walk it twice.
public static int[] withoutEvens(int[] arr) {
int count = 0;
for (int i = 0; i < arr.length; i++) { // pass 1: count
if (arr[i] % 2 != 0) {
count++;
}
}
int[] result = new int[count];
int j = 0; // write index for result
for (int i = 0; i < arr.length; i++) { // pass 2: fill
if (arr[i] % 2 != 0) {
result[j] = arr[i];
j++;
}
}
return result;
}
Trace it on {3, 8, 5, 2, 7}. First pass: count ends at 3 (the odd values are 3, 5, 7). Allocate result = new int[3]. Second pass: j advances each time the predicate holds, so result[0] = 3, then result[1] = 5, then result[2] = 7. Return {3, 5, 7}.
Edge cases. If the input has no odd values ({2, 4, 6}), count ends at 0, the result array has length 0 (legal!), and the second pass enters but never writes anything. Return value: a zero-length array, often written as {} in literals.
Common pitfall: forgetting that the caller has to reassign. A method like
append,removeAt, orwithoutEvensreturns a fresh array. If the caller writesappend(xs, 5);instead ofxs = append(xs, 5);, the new array is built and thrown away. The caller’sxsdoes not change. The compiler will not warn you.
Check your understanding. Sketch a method
int[] doubled(int[] arr)that returns a new array of the same length where each slot holds twice the corresponding input slot. Then explain in one sentence what changes if you instead implement it asvoid doubled(int[] arr).Reveal answer
public static int[] doubled(int[] arr) { int[] result = new int[arr.length]; for (int i = 0; i < arr.length; i++) { result[i] = arr[i] * 2; } return result; }If you instead write a
voidversion (for (int i = ...) arr[i] = arr[i] * 2;), the method mutates the caller’s array in place and the caller’s original values are gone. Returning a fresh array preserves the input; mutating in place destroys it. Pick whichever is correct for the use case.
Check your understanding. What is the result of this program?
public static int[] addOne(int[] arr) { int[] result = new int[arr.length + 1]; for (int i = 0; i < arr.length; i++) { result[i] = arr[i]; } result[arr.length] = 0; return result; } int[] xs = {1, 2, 3}; addOne(xs); System.out.println(xs.length);Reveal answer
Output:
3. The method correctly builds a new 4-slot array and returns it, but the caller never assigns the return value. The new array is garbage-collected;xsstill names the original 3-slot array. The fix isxs = addOne(xs);.
Wrap up and what’s next
Recap.
- Passing an array passes a copy of the reference. Both the caller’s variable and the parameter point at the same array.
- A method can mutate the caller’s array through that shared reference (and frequently does, intentionally).
- A method cannot grow or shrink the caller’s array by rebinding its parameter (
arr = new int[...]only rebinds the local). It must build a new array and return it. - Patterns: append (length + 1), remove-at (length − 1, skip one slot), and the two-pass count-then-fill for filtered copies.
- The caller is responsible for storing the returned array. Forgetting to write
xs = append(xs, 5);is the dominant bug in this lesson.
What you can do now. Read any method that takes an array parameter and predict whether it changes the caller’s data. Write void “mutate in place” methods and array-returning “build a new one” methods. Apply the count-then-fill pattern to any filtering problem.
Next up: Searching Arrays. Two algorithms, one easy, one trickier. Linear search walks every slot until it finds a match. Binary search jumps to the middle, halves the search space, and repeats — but it requires a sorted array. You will learn the −1 sentinel for “not found” and trace a binary search step by step.
Related resources
- Reges & Stepp, Building Java Programs, Chapter 7 section 7.4 covers arrays as parameters with the same Reges-style worked examples.
- FAQ entry on “why does my method ‘work’ but my array is unchanged?”