Topics › Basics › Things to Know in C++/Java/Python or any language
Functions (Pass by Reference and Value)
Java source ↗ Edit this article ↗
Jump to code:
What this covers
A function (a method in Java) packages a piece of logic behind a name, a parameter list and a return type, so it can be called from many places and reasoned about on its own. The part that confuses people is what happens to the arguments: if a method changes its parameter, does the caller see the change? In C++ the answer depends on whether you declared the parameter by value or by reference. In Java the rule is simpler and absolute: every argument is passed by value. What makes it feel complicated is that for objects the value being copied is a reference, so the callee can modify the object the reference points to but can never change which object the caller’s variable refers to. This article demonstrates that rule on primitives, arrays, strings, StringBuilder and a small class, then covers returning several values, overloading, varargs and recursion.
Primitives are copied
When you pass an int, the method receives its own copy. Assigning to the parameter changes the copy and nothing else.
static void tryToChangeInt(int x) {
x = 100;
}
After int a = 5; tryToChangeInt(a); the variable a is still 5. The idiomatic way to “change” a primitive is to return the new value and let the caller store it:
static int increment(int x) {
return x + 1;
}
a = increment(a); makes the update explicit at the call site, which is one of the reasons the rule is a feature rather than a limitation.
References are copied too
An array variable holds a reference to an array object. Passing it copies the reference, so the parameter and the caller’s variable point at the same array. Writing to an element through the parameter is visible to the caller:
static void setFirst(int[] arr, int value) {
if (arr.length > 0) arr[0] = value;
}
But assigning a new array to the parameter only rebinds the local copy of the reference. The caller’s variable still points at the original array:
static void tryToReplaceArray(int[] arr) {
arr = new int[]{42, 42, 42};
}
This pair of methods is the whole story. Mutating the object: visible. Reassigning the parameter: invisible. Everything below is the same fact in different clothing.
The swap test
The classic demonstration is swapping two values. Through int parameters it cannot work, because only the copies are exchanged:
static void trySwap(int a, int b) {
int t = a;
a = b;
b = t;
}
Inside an array it works, because both sides share the array object and the method writes into it:
static void swapInArray(int[] a, int i, int j) {
int t = a[i];
a[i] = a[j];
a[j] = t;
}
This is why sorting and partitioning algorithms in Java operate on arrays (or on indices into them) rather than on pairs of loose variables.
String versus StringBuilder
StringBuilder is a mutable object. Appending through the copied reference changes the one object both caller and callee share:
static void appendWorld(StringBuilder sb) {
sb.append(" world");
}
String is immutable. s + " world" creates a brand-new string, and assigning it to s rebinds only the parameter. The caller’s string is untouched, for two independent reasons: strings cannot be modified, and parameters cannot be rebound from inside:
static void tryToChangeString(String s) {
s = s + " world";
}
If you want a method to “change” a String, return the new one.
Objects with fields
The same two cases on a user-defined class. Counter is a nested static class with a single int count field.
static void bump(Counter c) {
c.count++;
}
bump changes a field of the shared object; the caller sees count go up by one. The next method creates a new Counter and points the parameter at it; the caller’s Counter is unaffected and its count is unchanged:
static void tryToReplaceCounter(Counter c) {
c = new Counter();
c.count = 999;
}
The test runs both on counters starting at 0, 1, -1 and 1000 and checks that only bump has a visible effect.
Returning more than one value
A method returns exactly one value. When you need two, return a small array (or a record or class). The caller unpacks it by index.
static int[] minMax(int[] a) {
int min = a[0], max = a[0];
for (int x : a) {
if (x < min) min = x;
if (x > max) max = x;
}
return new int[]{min, max};
}
Initialising both to a[0] rather than to Integer.MAX_VALUE and Integer.MIN_VALUE makes a single-element array trivially correct; the method assumes a non-empty input.
Overloading
Two methods may share a name if their parameter lists differ. The compiler chooses by the number and types of the arguments at each call site.
static int area(int side) {
return side * side;
}
static int area(int width, int height) {
return width * height;
}
area(3) calls the first, area(3, 4) the second. Overloading is resolved at compile time, so the return type alone cannot distinguish two overloads.
Varargs
int... nums lets the caller pass any number of int arguments, or an int[] directly. Inside the method, nums is an ordinary array.
static long sumAll(int... nums) {
long sum = 0;
for (int x : nums) sum += x;
return sum;
}
sumAll() with no arguments receives an empty array and returns 0. A varargs parameter must be the last one in the list.
Recursion
A method may call itself. Each call gets its own copies of its parameters and locals on the call stack, which is pass-by-value again: the inner call’s n is independent of the outer call’s n. A base case stops the chain.
static int digitSum(int n) {
if (n < 10) return n;
return n % 10 + digitSum(n / 10);
}
digitSum(1234) computes 4 + digitSum(123), which computes 3 + digitSum(12), and so on down to digitSum(1), which is the base case. The recursion section of the sheet builds on exactly this shape.
Edge cases and pitfalls
- Expecting a reassigned parameter to affect the caller. It never does. Return the new value, or mutate the object’s contents.
- Mutating a shared object by accident. The mirror image: a method that sorts or modifies an array it was passed changes the caller’s data. Copy first (
Arrays.copyOf) when the caller must keep the original. nullarguments. A reference parameter may benull; dereferencing it throws. Decide whether to check or to document the precondition.- Empty arrays.
minMaxreadsa[0]; guard or document.setFirstchecks the length before writing. - Overload ambiguity.
area(2.0)would not compile with only theintoverloads above; adoubleargument needs a matching overload or an explicit cast. - Deep recursion. Each call consumes stack space; recursing
10^5levels deep throwsStackOverflowError. Prefer a loop for linear depth. - Overflow in return values.
increment(Integer.MAX_VALUE)wraps toInteger.MIN_VALUE, exactly like any otherintarithmetic.
Related problems
- For loops and While loops contain methods that overwrite their parameters freely, which is safe for exactly the reasons given here.
- What are arrays, strings? explains the reference semantics of arrays and the immutability of strings in more detail.
- Theory with examples counts the work done by recursive calls such as
digitSum.