Arrays are passed by reference
Student can predict that mutations to an array parameter inside a method are visible to the caller, distinguish the "reference copied" rule from "object copied," and explain why final on the parameter does not prevent mutation.
When a method takes an array parameter, the caller and the method share the same underlying array. The parameter is not a fresh copy: it is a second reference to the same storage. This is the same rule that applies to every non-primitive type in Java (String, int[], ArrayList, every user-defined class).
public static void zeroOut(final int[] xs) {
for (int i = 0; i < xs.length; i++) {
xs[i] = 0;
}
}
final int[] data = {1, 2, 3};
zeroOut(data);
// data is now {0, 0, 0}: the method mutated the caller's array.
Compare to a primitive parameter:
public static void setToZero(int x) {
x = 0;
}
int n = 7;
setToZero(n);
// n is still 7: primitive parameters are copies.
The difference is not about the assignment inside the method: it is about what got passed in. The primitive n was copied into the parameter x; assigning to x only changes the copy. The array reference data was copied into xs, but the copy points to the same array as data. Indexing through either reference touches the same slots.
The "copy of a reference" mental model
Picture every array variable as an arrow pointing at a row of slots in memory. The variable is the arrow; the slots are the array. Calling a method copies the arrow but not the slots: both the caller's arrow and the parameter arrow point at the same slots.
Before zeroOut(data):
data ───► [1, 2, 3]
Inside zeroOut, after parameter binding:
data ───► [1, 2, 3]
xs ───► ^^^ (same storage)
After xs[0] = 0, xs[1] = 0, xs[2] = 0:
data ───► [0, 0, 0]
xs ───► ^^^ (same storage)
After zeroOut returns:
data ───► [0, 0, 0]
(xs goes out of scope, but the storage remains because data still references it)
The terminology is "Java passes object references by value": the reference itself is copied, but the object the reference points to is not. Functionally, this looks just like "pass by reference" from C++ or C# for the array's contents.
When this matters
- Useful: a method can fill an array given by the caller (e.g.,
Arrays.fill(arr, 0)); a sort method can rearrange the caller's array in place. - Surprising: a method that "modifies a copy" usually did not; the caller sees the change.
- Required: Lab 11's pattern of allocating an array in one method and returning it relies on the same semantics: the returned reference points at the array allocated inside the method, and the caller now has the only handle to it.
In other languages
- Python: lists behave the same way:
def f(xs): xs[0] = 99mutates the caller's list. - C: arrays decay to pointers when passed; the same sharing semantics apply.
- C#: arrays are reference types: same as Java.
- JavaScript: arrays are reference types: same as Java.
Pass-by-value-of-reference is the dominant idiom in object-oriented languages. Java's final keyword on a parameter (final int[] xs) means "the reference itself cannot be reassigned," not "the array cannot be mutated": students often hope for the second meaning and do not get it.
Mutation through an array parameter is visible to the caller
Student can predict, before running the code, whether a write inside a method that received an array parameter (arr[i] = v versus arr = new int[n]) will be visible to the caller after the method returns, and explain the result using the "reference is copied, array is shared" rule.
When a method receives an array parameter, the parameter is a copy of the reference, not a copy of the array. The local parameter and the caller's variable both refer to the same heap-allocated array. Writes to slots through the parameter are reads from the same slots through the caller's variable.
public static void zeroOut(final int[] arr) {
for (int i = 0; i < arr.length; i++) {
arr[i] = 0;
}
}
public static void main(final String[] args) {
final int[] xs = {1, 2, 3, 4, 5};
zeroOut(xs);
System.out.println(xs[0]); // 0: the array the caller named xs is the same heap object
}
Two things must be distinguished, and the distinction follows JLS §8.4.1 (parameter passing):
1. Slot writes propagate. arr[i] = 0 inside zeroOut reaches the same slot the caller's xs[i] reaches. The caller sees the change after zeroOut returns. This is the intended power of an array parameter: it lets a method modify a collection without returning anything. 2. Reassigning the parameter does not propagate. arr = new int[10] inside zeroOut would reassign the local parameter to a fresh array; the caller's xs would still point at the original. The reassignment is invisible outside the method because Java passes references by value, and the value being passed is the reference (not the variable that holds the reference).
The final modifier on arr enforces the second point at compile time: it forbids the reassignment but does not affect slot writes. final int[] arr means "you may not reassign arr," not "you may not modify the array arr refers to." The CSCD 210 lab style uses final on every parameter precisely to make this distinction visible to the reader.
Side effects as a design choice
A method whose only effect is on its array parameter (returning void) is using a side effect as its product. Two design patterns trade off against each other:
- Mutate-in-place (
void zeroOut(int[] arr)): no allocation, fast for large arrays, but the method is harder to compose because the caller'sxsis no longer the value it was before the call. - Return-new (
int[] zeroes(int n)returning a fresh array): every caller's variable means what it always meant; testing is simpler; trade-off is the allocation cost and the need for the caller to writexs = zeroes(xs.length).
CSCD 210 labs use mutate-in-place for sorts (where the size does not change and the array's contents are the "answer") and return-new for filter/map operations (where the result has a different size or shape).
In other languages
- C: identical mechanics. The function receives a pointer;
arr[i]and(arr+i)are interchangeable; writes propagate. C has nofinalfor parameters in the Java sense:const int arris the analog. - Python: list parameters behave the same way (Python's "everything is a reference" model).
def zero_out(lst): lst[0] = 0mutates the caller's list. - Rust: the parameter must be
&mut [i32]to allow mutation, and the borrow checker enforces no other reference exists. The mutation rule is explicit in the type, not implicit in the runtime.
Returning arrays from methods
Student can declare a method whose return type is an array (int[], double[], char[]), allocate the array inside the method body, fill it, and return the reference; and recognize the count-allocate-fill pattern as the standard way to size the array when the count comes from a separate pass.
A method whose return type is int[] (or double[], char[], any array type) returns a reference to an array. The standard shape: declare the return type with brackets, allocate the array inside the method, fill it, return the local variable.
public static int[] readInts(final String filename, final int count) throws FileNotFoundException {
// 1. precondition checks (omitted)
final Scanner sc = new Scanner(new File(filename));
sc.nextLine(); // skip the type tag
final int[] arr = new int[count]; // allocate once
for (int i = 0; i < count; i++) {
arr[i] = sc.nextInt(); // fill
}
sc.close();
return arr; // hand the reference back
}
Three observations:
1. The return type uses brackets on the type, not the name. int[] readInts(...) is the standard placement. Java also accepts int readInts(...)[ ] (legacy C-style), but no modern style guide allows it. 2. The caller is now the owner. After return arr, the local variable goes out of scope but the array survives: the only remaining reference is whatever variable the caller assigns the result to. Garbage collection reclaims the array only when that reference goes away. 3. The reference, not the contents, is returned. Returning an array is cheap regardless of size; the JVM passes the (4- or 8-byte) reference, not the values. Returning a million-element array is the same cost as returning a 10-element array.
The two-method allocation pattern (count-allocate-fill)
When the size of the array is not known until the data has been examined (e.g., counting lines in a file), the standard pattern is two passes:
- Pass 1 (
countValues): open the file, run an EOF loop, return the count. - Allocate: caller (or a wrapper) builds
new int[count]. - Pass 2 (
readInts): open the file again, fill the allocated array, return it.
Each method opens its own Scanner because Scanner does not rewind. The two passes touch the same disk file but with two independent Scanner objects, each closed in its own method.
The pattern is the standard CS1 answer to "how do I read N values when I do not know N until I've read the file?" The alternative (ArrayList-then-toArray) is more idiomatic in industrial code but introduces autoboxing for primitive types (ArrayList<Integer>) and obscures the array-allocation semantics that Week 6 has just introduced.
Returning null is rarely what you want
A method whose return type is int[] can technically return null. This almost always makes the caller's life worse: any arr.length or arr[i] call on the result throws NullPointerException, and the caller has to remember to null-check before using the value. A method that has "no values to return" should return an empty array (new int[0]): every loop on an empty array runs zero times, no null check needed.
In other languages
- Python: functions return lists by reference; same ownership-transfer semantics.
- C: returning an array is awkward: you either return a pointer to a heap-allocated buffer (and document that the caller must
freeit) or take an output parameter. The lack of garbage collection makes the ownership question explicit. - Go: slices (Go's array-like type) are returned by value of the slice header; underlying storage is shared. Similar to Java semantically.
- Rust:
Vec<i32>is returned by move; the borrow checker enforces that the caller becomes the unique owner.