Parameters and Return Values
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
Model Answer: "Java is strictly pass-by-value. For primitives, the literal value is copied. For reference types, the reference address is copied — not the object itself. The object can be mutated through that copy, but you cannot change what the caller's original reference points to."
Model Answer: "Yes, but varargs must always be the last parameter in the method signature. For example, 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."
Model Answer: "Returning null forces the caller to add null checks everywhere, and forgetting one produces an NPE. Returning an empty collection (or array) allows the caller to iterate safely with no special case needed. The caller can also call .size() or .isEmpty() on an empty collection without NPE. For optional values, Optional.empty() is preferred over null."
Model Answer: "The cleanest approach in modern Java is to define a record — a lightweight, immutable data carrier. For example: 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."
Model Answer: "The modification affects the underlying object, and all references to that object — including the caller's reference — will see the change. This is because the reference value (the address) was copied, but both copies point to the same object in the heap. However, if you reassign the parameter to a new object inside the method, only the local copy changes; the caller's reference still points to the original."
Model Answer: "Not directly — Java methods cannot be passed as parameters. You can pass a lambda expression or method reference that implements a functional interface, or you can pass an object with a method that does what you need. Higher-order functions in Java work through functional interfaces like Function<T, R>, Consumer<T>, or Predicate<T>."
Model Answer: "Primitives are copied entirely — the method gets an independent value. Object references are copied by value too, but the copied reference points to the same object. This means the method can mutate the object's state (call methods, modify fields) but cannot change what the caller's original reference variable points to."
Model Answer: "Java uses positional arguments — parameters are matched by their position in the call, not by name. This keeps the language simpler and avoids the complexity of resolving named arguments at runtime. It also means parameter names are not part of the method's signature for overload resolution. Methods with the same parameter types in the same order are considered identical."
Model Answer: "A defensive copy creates a new instance of an object rather than returning the original reference. Use it when returning mutable internal state — create a new ArrayList, new byte array, etc. This prevents callers from mutating internal state. The same applies to parameters — if you store a reference to a caller-supplied mutable object, you may want to make a copy to avoid external changes affecting your class."
null behave when passed as an argument?Model Answer: "A 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."
Model Answer: "No. Java is strictly pass-by-value for all types. Primitives are always passed by value (the actual bits are copied). There is no way to pass a primitive by reference in Java. If you need to return multiple values from a method, use a record, array, or collection. Some APIs simulate pass-by-reference using wrapper objects, but this still passes the reference by value."
Model Answer: "Arrays are fixed-size and mutable; Lists are variable-size and can be immutable (List.of()). When you pass an array, the caller can modify its contents and those changes are visible to the method's caller. When you pass a List, the same applies to mutable Lists. Returning List instead of array provides better type safety and allows returning immutable copies. Arrays support covariance (Object[] from String[]) which loses type information; List<String> from List<String> preserves it."
Model Answer: "Strings are immutable objects, so while the reference is passed by value, you cannot modify the caller's String. Any operation that appears to modify a String (concatenation, substring, replace) actually creates a new String. If you reassign the String parameter inside the method, only the local copy changes — the caller's reference remains unchanged. This immutability makes Strings safe to pass without defensive copying."
Model Answer: "A bulk operation applies an action to each element of a collection. With varargs, you can define methods like 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."
Model Answer: "At the JVM level, instance methods receive the this reference as a hidden first argument. Static methods receive only their declared parameters. All arguments are passed by value — for references, the pointer value is copied. The JVM uses operand stack slots and local variables for parameters, not named registers like some other platforms."
Model Answer: "Varargs must be the last parameter in a method signature, and a method can only have one varargs parameter. When overloading, the compiler resolves to the most specific applicable method — fixed-arity overloads are preferred over varargs. Calling a method with the same arguments that could match both a fixed and varargs overload will match the fixed one. Ambiguous calls between overloaded methods will not compile."
Model Answer: "Returning a primitive avoids boxing overhead. For example, returning int is more efficient than returning Integer. However, returning null for a primitive wrapper is possible (returning Integer) but returning null for a primitive is impossible. Use primitives for performance-critical code, and use boxed types only when you need to represent null or when working with collections that require object types."
Model Answer: "Use an explicit array parameter when the method signature needs to accept an array as a single argument, or when the array type has specific meaning. Use varargs when the method accepts a variable number of arguments of the same type and zero arguments is valid. Varargs is syntactic sugar that makes the call site cleaner — print(a, b, c) instead of print(new String[]{a, b, c}."
Model Answer: "Java's type system divides into primitives (byte, short, int, long, float, double, boolean, char) and reference types (classes, arrays, interfaces). Primitives are always copied by value — a new value is created. Reference types pass the reference by value — the object is shared but the caller's reference cannot be changed. Java does not support pass-by-reference in the C++ sense where the caller sees modifications to the parameter as if it were a local variable."
Model Answer: "In instance methods, this refers to the current instance and is implicitly passed as the first argument by the JVM. In static methods, there is no this — this is not accessible. Accessing this in a static context results in a compile error. This is why instance methods can access instance fields and static methods cannot."
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.