Parameters and Return Values
Master Java method parameters — pass-by-value semantics, varargs, and how to return single or multiple values cleanly.
Master Java method parameters — pass-by-value semantics, varargs, and how to return single or multiple values cleanly.
Parameters and Return Values
Every method call is an exchange — you pass data in, and the method hands something back. Understanding exactly how Java handles this exchange is fundamental to writing correct and predictable code.
Introduction
Every method call is an exchange of data: the caller passes arguments into the method, and the method returns a result back. Method parameters and return values are the two sides of this contract, and getting them right is fundamental to writing code that behaves predictably. Java is strictly pass-by-value — for primitives the value itself is copied, and for reference types the reference (memory address) is copied, not the underlying object. This distinction is the source of more confusion in Java than perhaps any other language feature, because it means a method can modify the state of an object it received as a parameter, but cannot change which object the caller’s reference points to.
The parameter list defines what a method needs from its caller to do its job. Java supports variable arguments (varargs) for situations where the number of arguments can vary — String... messages accepts zero or more strings and treats them as an array inside the method. Varargs must be the last parameter, and a method can only have one. For returning values, Java lets you return a single value of any type — including primitives, objects, arrays, or void (returning nothing). When you need to return multiple values, records (Java 16+) provide a clean, type-safe way to bundle them together without defining a separate class.
This post covers pass-by-value semantics in detail so the difference between modifying an object’s state and reassigning a caller’s reference becomes crystal clear. It explains varargs syntax, rules, and the fixed-parameter-over-varargs precedence rule in overload resolution. It also covers returning values: how to use void correctly, why you should return empty collections instead of null, how Optional expresses absent values, and how records let you return multiple values without boilerplate.
When to Use
- Passing data into a method for computation or state mutation
- Returning a result from a computation or a status indicator
- When you need to process a variable number of arguments (varargs)
When Not to Use
- Avoid varargs when fixed arity is clearer — if you always need exactly 2 or 3 arguments, use distinct parameters
- Avoid returning
nullfor empty collections — return an empty collection instead - Avoid out-parameters (passing an object to be mutated) — prefer returning the new value
Pass-by-Value Semantics
Java is strictly pass-by-value. For primitives, the value itself is copied. For references, the reference value is copied — not the object it points to.
public class PassByValueDemo {
public static void main(String[] args) {
int primitive = 10;
modifyPrimitive(primitive);
System.out.println(primitive); // 10 — unchanged
StringBuilder sb = new StringBuilder("hello");
modifyReference(sb);
System.out.println(sb); // "hello world" — object mutated
}
static void modifyPrimitive(int n) {
n = 99; // only local copy changed
}
static void modifyReference(StringBuilder sb) {
sb.append(" world"); // mutates the shared object
}
}
| Type | What gets copied | Effect |
|---|---|---|
| Primitive | The actual value | Changes to param don’t affect caller |
| Reference type | The reference address | Can mutate the object, but cannot reassign caller’s reference |
Varargs
Varargs allow a method to accept zero or more arguments of a specified type.
public class MathUtils {
// varargs — treated as an array inside the method
public static int sum(int... numbers) {
int total = 0;
for (int n : numbers) total += n;
return total;
}
public static void main(String[] args) {
System.out.println(sum(1, 2, 3)); // 6
System.out.println(sum(1, 2, 3, 4, 5)); // 15
System.out.println(sum()); // 0
}
}
Rules:
- Varargs must be the last parameter in the signature
- A method can have only one varargs parameter
- Inside the method, the varargs parameter is accessed as an array
// VALID
public void log(String level, String... messages) { }
// INVALID — varargs must be last
// public void log(String... messages, String level) { }
Returning Single Values
public record Point(int x, int y) {}
// Simple return
public int add(int a, int b) {
return a + b;
}
// Returning an object
public Point midpoint(Point p1, Point p2) {
return new Point((p1.x + p2.x) / 2, (p1.y + p2.y) / 2);
}
Returning Multiple Values
Option 1 — Use a record (Java 16+):
public record MinMax(int min, int max) { }
public MinMax findMinMax(int[] values) {
int min = Integer.MAX_VALUE;
int max = Integer.MIN_VALUE;
for (int v : values) {
min = Math.min(min, v);
max = Math.max(max, v);
}
return new MinMax(min, max);
}
// Usage
MinMax result = findMinMax(new int[]{3, 1, 4, 1, 5});
System.out.println(result.min() + " " + result.max()); // 1 5
Option 2 — Use a Map or List:
public Map<String, Integer> minMaxMap(int[] values) {
// ... computation
return Map.of("min", min, "max", max);
}
Mermaid Diagram — Pass-by-Value Flow
flowchart TD
subgraph "Caller Stack Frame"
A["int x = 10"]
B["StringBuilder ref = new SB()"]
end
subgraph "Method Stack Frame"
C["int n = 10<br/><i>copy of value</i>"]
D["StringBuilder sb = ref<br/><i>copy of reference</i>"]
end
A -->|"pass primitive"| C
B -->|"pass reference"| D
D -->|"mutate object"| B
C -.->|"local only, no effect on A"| A
Failure Scenarios
Reassigning reference parameter without affecting caller:
static void reassign(List<String> list) {
list = new ArrayList<>(); // only local reference changed
list.add("new"); // affects the new list, not the caller
}
public static void main(String[] args) {
List<String> original = new ArrayList<>();
original.add("original");
reassign(original);
System.out.println(original); // ["original"] — unchanged
}
Null pointer from uninitialized return:
public List<String> filterEmpty(List<String> input) {
// Forgot to handle null input
return input.stream()
.filter(s -> !s.isEmpty())
.toList(); // NPE if input is null
}
Trade-off Table
| Approach | Pros | Cons |
|---|---|---|
| Return single primitive | Simple, obvious | Limited for complex results |
| Return object/record | Type-safe, multiple fields | Requires defining the type |
| Return array | No extra type needed | No type safety, fixed size |
| Use out-parameter (mutate param) | Avoids new object | Side effects make code harder to reason about |
| Throw exception for invalid input | Explicit failure signal | Overuse clutters code |
Code Snippets
Defensive parameter validation:
public double calculateAverage(List<Integer> values) {
if (values == null || values.isEmpty()) {
return 0.0; // or throw IllegalArgumentException
}
return values.stream()
.mapToInt(Integer::intValue)
.average()
.orElse(0.0);
}
Varargs with additional fixed parameter:
public void printReport(String title, String... rows) {
System.out.println("=== " + title + " ===");
for (String row : rows) {
System.out.println(row);
}
}
printReport("Sales", "Q1: 100", "Q2: 200", "Q3: 150");
Returning optional for nullable result:
public Optional<String> findUserById(int id) {
return database.containsKey(id)
? Optional.of(database.get(id))
: Optional.empty();
}
Observability Checklist
- All parameters are validated at method entry
- Null inputs are handled explicitly — not assumed
- Return value type matches what is actually returned
- Collections returned are either immutable or the caller knows not to mutate them
- Varargs method handles 0 arguments gracefully
- Documentation states whether null is a valid input
Security Notes
- Never trust caller-supplied objects — validate before use
- Do not expose internal collections through return values; return copies or unmodifiable views
- Parameterized types (
List<String>) prevent type confusion attacks - Avoid logging full parameter values that may contain sensitive data (PII, credentials)
Pitfalls
- Confusing pass-by-value with pass-by-reference — Java is always pass-by-value, never pass-by-reference
- Modifying a returned collection — changes affect all code holding that reference unless you return a copy
- Varargs ambiguity with overloads — multiple varargs overloads can cause compilation ambiguity
- Returning mutable static state — a method that returns a static collection gives all callers a view into the same object
- Not checking for null before streaming —
nullStreamcauses NPE at.stream()call
Quick Recap
- Java is strictly pass-by-value: primitives copy the value, references copy the address
- The object behind a reference can be mutated; the caller’s reference variable itself cannot be changed
- Varargs must be the last parameter and are accessed as an array
- Return empty collections instead of null; use
Optionalfor nullable results - Use records (Java 16+) to cleanly return multiple values without extra classes
Interview Questions
void log(String level, String... messages) is valid. You cannot have two varargs parameters in the same method because the compiler would have no way to disambiguate where one ends and the other begins.".size() or .isEmpty() on an empty collection without NPE. For optional values, Optional.empty() is preferred over null."public record MinMax(int min, int max) {}. Before Java 16, you would use a custom class or a Map. Returning an array is possible but loses type safety. Passing an output parameter (mutating a caller-provided object) is possible but obscures the method's effect."Function<T, R>, Consumer<T>, or Predicate<T>."null behave when passed as an argument?null can be passed for any reference type parameter. Inside the method, if you attempt to call a method on null without checking, you get a NullPointerException. Always validate reference-type parameters at method entry if null is not a valid input. For collections, return empty instead of null so callers can iterate safely."void logAll(String... messages) that accept zero or more strings. The varargs parameter is an array inside the method. For bulk operations on collections, use enhanced for-each or streams — varargs is for method parameters, not iteration over collections."print(a, b, c) instead of print(new String[]{a, b, c}."Further Reading
- Method Overloading — varargs ambiguity and overload resolution
- Static Methods — static methods and their interaction with parameters
- Lambda Expressions — passing behavior as parameters with lambdas
- Method References — constructor references for object creation
- Java Records Documentation — clean multiple-value returns
Conclusion
Java’s pass-by-value semantics apply uniformly to all types — primitives copy the value, references copy the address. Neither reassigning the parameter nor mutating the object behind it behaves like pass-by-reference, and confusing the two is a common source of bugs.
Varargs (int...) provides a clean way to accept a variable number of arguments, but it must be the last parameter and the fixed-parameter version always wins in overload resolution. When returning values, prefer records (Java 16+) for multiple values, empty collections over null, and Optional for nullable results.
The caller’s contract matters: validate inputs, document null behavior, and avoid exposing internal mutable state through return values. Defensive copies and unmodifiable views are the standard tools for that.
For related reading: Varargs and overloading interactions are covered in the Method Overloading post, and how static methods differ from instance methods in their ability to access state is covered in the Static Methods post.
Category
Related Posts
Abstract Classes in Java
Learn about partially implemented classes that define contracts for subclasses using abstract methods and concrete implementations.
Arithmetic Operators in Java
Master Java arithmetic operators: addition, subtraction, multiplication, division, and modulo with integer division gotchas and operator precedence explained.
Array Basics in Java
Learn Java array fundamentals: declaration, initialization, element access, and the length property explained simply.