A method's parameters are the variables named in its declaration. The arguments are the values supplied at the call site. What happens in between — what gets copied, what the caller can still see afterwards, and which method actually runs when several share a name — is where most of the surprises in Java live.
This article covers parameter passing in full, including the claim that most tutorials get wrong: Java is always pass-by-value, even for objects. Then return values, final parameters, the rules of overloading, the four-phase algorithm the compiler uses to choose between overloads, and varargs. Every output line and every error message below was produced by compiling and running the code on OpenJDK 21.0.6.
![]()
Two rules carry the whole article: an argument is always copied, and the overload is chosen at compile time.
Parameter versus argument
The two words are not interchangeable, and using them precisely makes the rest of this article much easier to state.
public class ParamVsArg {
// vvvvvvvvvvvvvvvvvvvvvv parameters: names in the declaration
static int discount(int price, int percent) {
return price - price * percent / 100;
}
public static void main(String[] args) {
int total = 200;
// vvvvvvvvv arguments: values at the call site
System.out.println(discount(total, 15));
System.out.println(discount(99, 10));
}
}
170
90
| Term | Where it lives | What it is |
|---|---|---|
| Parameter | the method declaration | a variable name plus its type, created fresh on every call |
| Argument | the call site | an expression, evaluated before the call, whose value is copied into the parameter |
price and percent are parameters. total, 15, 99 and 10 are arguments. Note that total is not passed to discount — the value of total is. That distinction is the entire next section.
Java is always pass-by-value

Every argument is evaluated, and its value is copied into the parameter, which is a brand new variable belonging to the method. Assigning to a parameter therefore changes only that copy. There is no exception to this in Java — no ref keyword, no &, no way to hand a method the caller's variable itself.
A primitive argument: why swap cannot work
For a primitive the value is a number, so the number is copied.
public class SwapPrimitive {
static void swap(int a, int b) {
int tmp = a;
a = b;
b = tmp;
System.out.println("inside swap: a = " + a + ", b = " + b);
}
public static void main(String[] args) {
int x = 1;
int y = 2;
System.out.println("before swap: x = " + x + ", y = " + y);
swap(x, y);
System.out.println("after swap: x = " + x + ", y = " + y);
}
}
before swap: x = 1, y = 2
inside swap: a = 2, b = 1
after swap: x = 1, y = 2
The swap really happened — inside swap. a and b are copies of the values 1 and 2, and exchanging two copies leaves the originals alone. A swap method for primitives cannot be written in Java at all; you either return the pair or swap at the call site.
An object argument: mutation is visible, reassignment is not
For a reference type the value stored in the variable is a reference, so the reference is copied. The caller's variable and the parameter then point at the same heap object, exactly as String alias = name; produced two names for one object in the article on variables and data types. That gives two different outcomes depending on what the method does.
import java.util.Arrays;
public class ArrayParam {
static void mutate(int[] data) {
data[0] = 99;
System.out.println("inside mutate: data = " + Arrays.toString(data));
}
static void reassign(int[] data) {
data = new int[] {7, 8, 9};
System.out.println("inside reassign: data = " + Arrays.toString(data));
}
public static void main(String[] args) {
int[] nums = {1, 2, 3};
System.out.println("before mutate: nums = " + Arrays.toString(nums));
mutate(nums);
System.out.println("after mutate: nums = " + Arrays.toString(nums));
System.out.println();
System.out.println("before reassign: nums = " + Arrays.toString(nums));
reassign(nums);
System.out.println("after reassign: nums = " + Arrays.toString(nums));
}
}
before mutate: nums = [1, 2, 3]
inside mutate: data = [99, 2, 3]
after mutate: nums = [99, 2, 3]
before reassign: nums = [99, 2, 3]
inside reassign: data = [7, 8, 9]
after reassign: nums = [99, 2, 3]
mutate never touches its parameter. It follows the copied reference to the object and writes into the object, and there is only one object, so the caller sees [99, 2, 3]. reassign writes to the parameter itself, which re-points the local copy at a new array and leaves nums exactly where it was.
That is the whole behaviour in one sentence: you can change what the reference points at from inside the method, but only for your own copy of it.
The one-line test that settles the argument
If Java were pass-by-reference, swap would work on objects. It does not.
public class SwapObjects {
static void swap(StringBuilder a, StringBuilder b) {
StringBuilder tmp = a;
a = b;
b = tmp;
System.out.println("inside swap: a = " + a + ", b = " + b);
}
public static void main(String[] args) {
StringBuilder x = new StringBuilder("first");
StringBuilder y = new StringBuilder("second");
swap(x, y);
System.out.println("after swap: x = " + x + ", y = " + y);
}
}
inside swap: a = second, b = first
after swap: x = first, y = second
Identical to the primitive case. In a pass-by-reference language such as C# with ref, the caller's variables would come back swapped. In Java they do not, because what was copied was the reference, not the variable. The precise formulation is: Java passes references by value. It is not pass-by-reference.
Why String looks like a counter-example
A String parameter appears to prove that objects are passed by reference and then mysteriously fail to be modified.
public class StringParam {
static void shout(String text) {
text = text.toUpperCase();
System.out.println("inside shout: text = " + text);
}
static void append(StringBuilder sb) {
sb.append(" world");
System.out.println("inside append: sb = " + sb);
}
public static void main(String[] args) {
String s = "hello";
shout(s);
System.out.println("after shout: s = " + s);
StringBuilder b = new StringBuilder("hello");
append(b);
System.out.println("after append: b = " + b);
}
}
inside shout: text = HELLO
after shout: s = hello
inside append: sb = hello world
after append: b = hello world
String is immutable, as covered in the article on strings, so toUpperCase() cannot modify anything — it returns a new object, and the assignment re-points the parameter, which is the reassign case above. StringBuilder is mutable, so append mutates the shared object and the change is visible. The rule never changed; only the availability of a mutating method did.
Return values: one value, or none
A Java method returns exactly one value, or nothing. return expr; hands a value back and ends the method; void declares that there is no value, and a bare return; may still be used to end the method early. Everything else is a workaround.
Returning more than one value
There are three ways to get two values out of a method, and only two of them are good.
import java.util.Arrays;
public class ReturnValues {
// two values, as an array: works, but both must share a type
static int[] minMaxArray(int[] data) {
int min = data[0], max = data[0];
for (int n : data) {
if (n < min) min = n;
if (n > max) max = n;
}
return new int[] {min, max};
}
// two values, as an object: named, typed, self-documenting
record MinMax(int min, int max) {}
static MinMax minMax(int[] data) {
int[] pair = minMaxArray(data);
return new MinMax(pair[0], pair[1]);
}
// two values, by mutating a caller-supplied array: works, reads badly
static void minMaxInto(int[] data, int[] out) {
int[] pair = minMaxArray(data);
out[0] = pair[0];
out[1] = pair[1];
}
public static void main(String[] args) {
int[] data = {4, 9, 1, 7};
System.out.println("array -> " + Arrays.toString(minMaxArray(data)));
MinMax m = minMax(data);
System.out.println("record -> " + m + " min=" + m.min() + " max=" + m.max());
int[] out = new int[2];
minMaxInto(data, out);
System.out.println("out-param -> " + Arrays.toString(out));
}
}
array -> [1, 9]
record -> MinMax[min=1, max=9] min=1 max=9
out-param -> [1, 9]
| Approach | Use it when | Why |
|---|---|---|
Return an object or record | almost always | the values are named and independently typed, and the return type documents itself |
| Return an array | the values share a type and the order is obvious | cheap, but result[0] tells the reader nothing |
| Mutate a caller-supplied argument | almost never | it only works because references are copied, and the signature hides the fact that the method writes to it |
The third form is the "out parameter" style borrowed from C. It works — that is precisely what the previous section demonstrated — but a reader of minMaxInto(data, out) cannot tell which argument is the input without opening the method. Classes and records get their own article later in this series; until then, a small record is the right answer whenever two values belong together.
Returning early, and returning from inside a loop
return ends the method immediately, from wherever it appears, including from inside a loop.
public class EarlyReturn {
static String classify(int n) {
if (n < 0) {
return "negative";
}
if (n == 0) {
return "zero";
}
return "positive";
}
static int indexOf(int[] data, int target) {
for (int i = 0; i < data.length; i++) {
if (data[i] == target) {
return i; // leaves the loop AND the method
}
}
return -1; // only reached if the loop finished
}
static void warn(int n) {
if (n >= 0) {
return; // a bare return in a void method
}
System.out.println("negative input: " + n);
}
public static void main(String[] args) {
System.out.println(classify(-5) + " " + classify(0) + " " + classify(5));
int[] data = {4, 9, 1, 7};
System.out.println("indexOf(9) = " + indexOf(data, 9));
System.out.println("indexOf(42) = " + indexOf(data, 42));
warn(3);
warn(-3);
}
}
negative zero positive
indexOf(9) = 1
indexOf(42) = -1
negative input: -3
Early returns replace the nested if/else pyramid that the same logic would otherwise need, and returning from inside a loop is the normal way to write a search — no flag variable, no break followed by a second check.
The compiler enforces that every path out of a non-void method returns something. Forget one and the message names the closing brace rather than the missing statement:
static int abs(int n) {
if (n < 0) {
return -n;
}
}
MissingReturn.java:6: error: missing return statement
}
^
1 error
final parameters and what they actually prevent
Marking a parameter final forbids assigning to it. It says nothing whatsoever about the object it points at.
import java.util.Arrays;
public class FinalParam {
static void bump(final int[] data) {
data[0] = 42; // mutation: allowed
System.out.println("inside bump: " + Arrays.toString(data));
}
static void reassign(final int[] data) {
data = new int[] {0}; // reassignment: rejected
}
...
}
FinalParam.java:10: error: final parameter data may not be assigned
data = new int[] {0}; // reassignment: rejected
^
1 error
data[0] = 42 compiles and runs; data = ... does not. This is the same distinction as final List<String> names in the article on variables: the binding is frozen, the object is not.
final is also not part of the signature, so it cannot be used to distinguish two overloads:
public class FinalOverload {
static void f(int n) { }
static void f(final int n) { }
}
FinalOverload.java:3: error: method f(int) is already defined in class FinalOverload
static void f(final int n) { }
^
1 error
Use final on a parameter when a long method would otherwise let a reader wonder whether the value still means what the signature said. Do not use it as documentation of immutability, because it is not that.
Overloading: same name, different parameter list
Two methods in the same class may share a name as long as their parameter lists differ. Different types, a different number of parameters, or the same types in a different order all count.
public class OverloadShapes {
// different count
static String join(String a) { return a; }
static String join(String a, String b) { return a + "-" + b; }
// different types
static String describe(int n) { return "int " + n; }
static String describe(double d) { return "double " + d; }
static String describe(String s) { return "String " + s; }
// different ORDER of the same types
static String pair(int n, String s) { return "(int, String)"; }
static String pair(String s, int n) { return "(String, int)"; }
public static void main(String[] args) {
System.out.println(join("a"));
System.out.println(join("a", "b"));
System.out.println(describe(7));
System.out.println(describe(7.0));
System.out.println(describe("7"));
System.out.println(pair(1, "x"));
System.out.println(pair("x", 1));
}
}
a
a-b
int 7
double 7.0
String 7
(int, String)
(String, int)
| Difference | Counts as an overload? |
|---|---|
| Parameter types | yes |
| Number of parameters | yes |
| Order of parameter types | yes |
| Return type alone | no |
| Parameter names alone | no |
final on a parameter | no |
The return type is not part of what distinguishes two methods, because the compiler has to choose the method before it knows what you intend to do with the result. Two methods differing only in return type collide:
public class ReturnTypeOnly {
static int f(int n) {
return n;
}
static String f(int n) {
return String.valueOf(n);
}
...
}
ReturnTypeOnly.java:6: error: method f(int) is already defined in class ReturnTypeOnly
static String f(int n) {
^
1 error
Renaming the parameters changes nothing either — log(String message) and log(String text) produce the same already defined error. What the compiler compares is the ordered list of parameter types.
⚠️ Overloading is not overriding. Overriding replaces a superclass method with one of the same signature in a subclass and is resolved at runtime; overloading picks between different signatures and is resolved at compile time. Overriding needs inheritance, so it gets its own article later in this series.
How the compiler picks an overload

Two facts decide everything here. The choice is made at compile time, and it is made from the static types of the arguments — the types written in the source, not the runtime classes of the objects.
public class StaticType {
static void f(Object o) { System.out.println("f(Object) ran"); }
static void f(String s) { System.out.println("f(String) ran"); }
public static void main(String[] args) {
String s = "hello";
Object o = s; // same object, wider static type
f(s);
f(o);
System.out.println("s == o -> " + (s == o));
}
}
f(String) ran
f(Object) ran
s == o -> true
One object, two calls, two different methods. s == o is true, so the runtime value is identical; only the declared type of the variable differs, and only the declared type is consulted.
The choice itself runs in phases. The compiler collects the applicable methods using the rules of one phase; if any apply, one of them wins and the search stops. Only if none applies does it move on. It never goes back a phase, which is why a later phase can never beat an earlier one no matter how much better the match looks.
The four rungs above are the practical form of that rule, and they give the right answer for every call in this article. The specification states it slightly differently, and the difference is worth knowing. The JLS defines three phases: strict invocation, which permits an identity conversion, a widening primitive conversion and a widening reference conversion; loose invocation, which adds boxing and unboxing; and variable arity invocation, which finally admits varargs. When several methods are applicable in the same phase, the most specific one wins — the one whose parameter types could be passed to all the others. Splitting the first phase into "exact match" and "widening" is that most-specific rule made visible, which is why the two accounts never disagree.
Phase 1: an exact match wins
An exact match needs no conversion at all, so nothing beats it.
public class Phase1 {
static void f(int n) { System.out.println("f(int) ran with " + n); }
static void f(long n) { System.out.println("f(long) ran with " + n); }
public static void main(String[] args) {
int value = 5;
f(value);
}
}
f(int) ran with 5
f(long) is applicable too — int to long is a widening conversion, which the first phase allows — but f(int) requires no conversion, so it is the more specific of the two and takes the call. The same rule settles reference types: with f(String) and f(Object) declared, f("hi") prints f(String) ran.
Phase 2: widening beats boxing
This is the one that catches everyone. Remove f(int) and offer a widening conversion against a boxing conversion.
public class Phase2 {
static void f(long n) { System.out.println("f(long) ran with " + n); }
static void f(Integer n) { System.out.println("f(Integer) ran with " + n); }
public static void main(String[] args) {
int value = 5;
f(value);
}
}
f(long) ran with 5
f(Integer) looks like the closer match — the value is an int and Integer is the int wrapper — but widening primitive conversion is available before boxing is considered at all. f(long) is applicable without any boxing, so the search stops and f(Integer) never becomes a candidate. This ordering exists for backward compatibility: autoboxing arrived in Java 5, and code compiled before it had to keep calling the same methods afterwards. The same reasoning makes f(long) beat f(Object) for an int argument, which is easy to get wrong in the other direction.
When two widening conversions are both available, the more specific parameter type wins:
public class TwoWidenings {
static void f(long n) { System.out.println("f(long) ran"); }
static void f(float n) { System.out.println("f(float) ran"); }
...
}
f(long) ran
long widens to float, so f(long) is the more specific of the two.
Phase 2 also explains an error that looks arbitrary. Widening and boxing cannot be combined in either order:
public class NoWidenThenBox {
static void f(Long n) { System.out.println("f(Long) ran"); }
public static void main(String[] args) {
int value = 5;
f(value);
}
}
NoWidenThenBox.java:6: error: incompatible types: int cannot be converted to Long
f(value);
^
Note: Some messages have been simplified; recompile with -Xdiags:verbose to get full output
1 error
An int widens to long and a long boxes to Long, but no single phase does both, so there is no applicable method at all. This is the same rule that rejects Long x = 5; in an ordinary assignment.
Phase 3: boxing beats varargs
Phase 3 permits autoboxing and unboxing. Varargs are still not on the table.
public class Phase3 {
static void f(Integer n) { System.out.println("f(Integer) ran with " + n); }
static void f(int... n) { System.out.println("f(int...) ran with " + n.length + " arg(s)"); }
public static void main(String[] args) {
int value = 5;
f(value);
}
}
f(Integer) ran with 5
Only when no fixed-arity method applies in any earlier phase does the compiler consider variable arity:
public class Phase4 {
static void f(int... n) { System.out.println("f(int...) ran with " + n.length + " arg(s)"); }
public static void main(String[] args) {
int value = 5;
f(value);
}
}
f(int...) ran with 1 arg(s)
Put all four in one class and the ladder is visible in a single run — the same f(5) call resolves to f(int), and the other three overloads sit there unused.
null, the most specific type, and ambiguity
null is assignable to every reference type, so every reference overload is applicable. Java resolves that by choosing the most specific one: the type that could be passed to all the others.
public class NullMostSpecific {
static void f(Object o) { System.out.println("f(Object) ran"); }
static void f(String s) { System.out.println("f(String) ran"); }
public static void main(String[] args) {
f(null);
}
}
f(String) ran
Every String is an Object, so String is more specific and wins. Add a third overload whose type is unrelated to String and there is no most specific candidate left:
public class Amb {
static void f(Object o) { System.out.println("f(Object) ran"); }
static void f(String s) { System.out.println("f(String) ran"); }
static void f(Integer i) { System.out.println("f(Integer) ran"); }
public static void main(String[] args) {
f(null);
}
}
Amb.java:7: error: reference to f is ambiguous
f(null);
^
both method f(String) in Amb and method f(Integer) in Amb match
1 error
Neither String nor Integer is a subtype of the other, so neither is more specific, and the compiler refuses to guess. The fix is a cast at the call site: f((String) null) compiles and prints f(String) ran.
Ambiguity is not confined to null. Two overloads that each require boxing in a different position produce the same error:
public class BoxAmbiguous {
static void f(int a, Integer b) { System.out.println("f(int, Integer)"); }
static void f(Integer a, int b) { System.out.println("f(Integer, int)"); }
public static void main(String[] args) {
f(1, 2);
}
}
BoxAmbiguous.java:6: error: reference to f is ambiguous
f(1, 2);
^
both method f(int,Integer) in BoxAmbiguous and method f(Integer,int) in BoxAmbiguous match
1 error
Varargs: int... nums is an array

A parameter declared int... nums accepts any number of int arguments, including none. Inside the method it is an ordinary int[] — the compiler builds the array at the call site and passes it.
import java.util.Arrays;
public class Varargs {
static int sum(int... nums) {
System.out.println(" received " + nums.getClass().getSimpleName()
+ ", length " + nums.length + ", " + Arrays.toString(nums));
int total = 0;
for (int n : nums) {
total += n;
}
return total;
}
public static void main(String[] args) {
System.out.println("sum(1, 2, 3) = " + sum(1, 2, 3));
System.out.println("sum() = " + sum());
int[] data = {10, 20};
System.out.println("sum(data) = " + sum(data));
}
}
received int[], length 3, [1, 2, 3]
sum(1, 2, 3) = 6
received int[], length 0, []
sum() = 0
received int[], length 2, [10, 20]
sum(data) = 30
Three things that printout settles. getSimpleName() returns int[], so the parameter really is an array and not a special language object. Calling with zero arguments gives an array of length 0, not null, so for (int n : nums) is safe without a null check. And an existing int[] can be passed directly — the compiler recognises that the argument is already the array type and passes it through instead of wrapping it.
"Passes it through" is literal: the method receives the caller's array, not a copy, so mutating nums mutates the caller's array.
static int[] capture(int... nums) { return nums; }
...
int[] data = {1, 2};
System.out.println("existing array reused? " + (capture(data) == data));
System.out.println("two calls, same array? " + (capture(1, 2) == capture(1, 2)));
System.out.println("zero args, same empty? " + (capture() == capture()));
existing array reused? true
two calls, same array? false
zero args, same empty? false
Each call that actually packs arguments allocates a fresh array, including the zero-argument case, which allocates an empty one.
Three declaration rules, all enforced by javac. The varargs parameter must be last:
public class VarargsNotLast {
static void log(String... parts, String level) {
}
}
VarargsNotLast.java:2: error: varargs parameter must be the last parameter
static void log(String... parts, String level) {
^
1 error
A method may have only one, which is really the same rule — the first of two varargs parameters is by definition not last, and the message says so:
static void log(String... parts, int... codes) { }
VarargsTwice.java:2: error: varargs parameter must be the last parameter
static void log(String... parts, int... codes) {
^
1 error
And f(int[]) and f(int...) are the same method, not two overloads, because varargs is compiled to an array parameter:
public class ArrayVsVarargs {
static void f(int[] a) { System.out.println("f(int[])"); }
static void f(int... a) { System.out.println("f(int...)"); }
...
}
ArrayVsVarargs.java:3: error: cannot declare both f(int...) and f(int[]) in ArrayVsVarargs
static void f(int... a) { System.out.println("f(int...)"); }
^
1 error
Two more behaviours worth knowing. A fixed-arity overload always beats the varargs one, because of phase 4:
static void f(int a, int b) { System.out.println("f(int, int) ran"); }
static void f(int... nums) { System.out.println("f(int...) ran with " + nums.length); }
...
f(1, 2);
f(1, 2, 3);
f();
f(int, int) ran
f(int...) ran with 3
f(int...) ran with 0
And passing a bare null to a varargs parameter of reference type does not give an empty array — it passes null as the whole array, with a warning:
static void g(String... parts) {
System.out.println(parts == null ? "parts is null" : "length " + parts.length);
}
...
g(null);
VarargsNull.java:13: warning: non-varargs call of varargs method with inexact argument type for last parameter;
g(null);
^
cast to String for a varargs call
cast to String[] for a non-varargs call and to suppress this warning
1 warning
parts is null
The warning tells you exactly which cast to write. g((String) null) gives a one-element array containing null; g((String[]) null) passes null deliberately.
The classic bite: remove(int) versus remove(Object)
Overload resolution stops being academic the moment a real API overloads on int and Object. java.util.List does exactly that: remove(int index) removes by position and remove(Object o) removes by value.
import java.util.ArrayList;
import java.util.List;
...
List<Integer> a = new ArrayList<>(List.of(10, 20, 30));
List<Integer> b = new ArrayList<>(List.of(10, 20, 30));
a.remove(1); // remove(int index)
b.remove(Integer.valueOf(1)); // remove(Object o)
System.out.println("a.remove(1) -> " + a);
System.out.println("b.remove(Integer.valueOf(1)) -> " + b);
a.remove(1) -> [10, 30]
b.remove(Integer.valueOf(1)) -> [10, 20, 30]
a.remove(1) deleted 20 — the element at index 1 — because 1 is an int and remove(int) matches exactly in phase 1, so remove(Object) is never reached. b.remove(Integer.valueOf(1)) looked for the value 1, did not find it, and changed nothing.
The lesson is about your own APIs, not about collections: do not overload on int and Object, or on a primitive and its wrapper, when both overloads have visibly different meanings. The compiler will silently pick one, and the reader of the call site has no hint that a choice was made.
When to overload, and the forwarding pattern
Overloading is worth it when the overloads do the same thing to differently shaped inputs. System.out.println is the model: every overload prints its argument. It is a bad idea when the overloads do different things, as remove demonstrates.
Java has no default parameter values, so the idiomatic substitute is a set of short overloads that forward to one full implementation.
import java.nio.charset.StandardCharsets;
public class Forwarding {
// the one real implementation
static String connect(String host, int port, int timeoutMs, boolean tls) {
return "connect host=" + host + " port=" + port
+ " timeoutMs=" + timeoutMs + " tls=" + tls;
}
// defaults, expressed as overloads that forward
static String connect(String host, int port, int timeoutMs) {
return connect(host, port, timeoutMs, true);
}
static String connect(String host, int port) {
return connect(host, port, 5000, true);
}
static String connect(String host) {
return connect(host, 443, 5000, true);
}
public static void main(String[] args) {
System.out.println(connect("api.example.com"));
System.out.println(connect("api.example.com", 8080));
System.out.println(connect("api.example.com", 8080, 250));
System.out.println(connect("api.example.com", 8080, 250, false));
// the JDK does exactly this: new String(byte[]) forwards to new String(byte[], Charset)
byte[] bytes = {74, 97, 118, 97};
System.out.println(new String(bytes));
System.out.println(new String(bytes, StandardCharsets.UTF_8));
}
}
connect host=api.example.com port=443 timeoutMs=5000 tls=true
connect host=api.example.com port=8080 timeoutMs=5000 tls=true
connect host=api.example.com port=8080 timeoutMs=250 tls=true
connect host=api.example.com port=8080 timeoutMs=250 tls=false
Java
Java
Exactly one method contains logic; the rest supply defaults and delegate. Change the implementation once and every overload changes with it. The JDK is built this way throughout, and the shape is worth copying.
Common mistakes and the errors they produce
| Mistake | What happens | Message |
|---|---|---|
| Two methods differing only by return type | compile error | method f(int) is already defined in class ... |
Two methods differing only by parameter names, or by final | compile error | method f(String) is already defined in class ... |
| Varargs parameter not last, or two of them | compile error | varargs parameter must be the last parameter |
Declaring both f(int[]) and f(int...) | compile error | cannot declare both f(int...) and f(int[]) |
f(null) with two unrelated reference overloads | compile error | reference to f is ambiguous |
Passing an int where only f(Long) exists | compile error | incompatible types: int cannot be converted to Long |
Forgetting a return on one branch | compile error | missing return statement |
Expecting swap(a, b) to swap the caller's variables | compiles, silently does nothing | none |
Expecting f(Integer) to win over f(long) for an int | compiles, wrong method runs | none |
Overloading on int and Object with different meanings | compiles, wrong method runs | none |
The last three are the dangerous ones: they produce no diagnostic at all. Every one of them is caught by the same habit — decide which overload you meant, then check that the static types of your arguments actually select it.
FAQ
Is Java pass-by-value or pass-by-reference?
Pass-by-value, always. For a primitive the value copied is the number; for an object the value copied is the reference. Because the reference is copied rather than the variable, a method can mutate the object the caller sees, but assigning to the parameter only re-points the method's own copy. The test is swap: in a pass-by-reference language it would work on objects, and in Java it does not.
Can two methods differ only by return type in Java?
No. javac reports method f(int) is already defined in class .... The compiler must choose an overload from the arguments alone, before the result is used, so the return type is not part of what distinguishes methods. The same applies to parameter names and to final on a parameter.
Why does f(long) win over f(Integer) when I pass an int?
Because resolution runs in phases and never backtracks, and widening primitive conversion belongs to an earlier phase than boxing. int to long is a widening conversion, so f(long) is applicable without boxing anything and the search stops before f(Integer) is ever considered. The ordering was chosen so that adding autoboxing in Java 5 could not change which method existing code called.
Is varargs slower than passing an array?
Every call that packs loose arguments allocates a new array, and a call with zero arguments allocates an empty one, so a varargs method invoked in a hot loop does allocate where an array parameter would not. Passing an existing array skips the allocation entirely, and the JIT can often eliminate a short-lived array that never escapes the method. Treat it as a design question rather than a performance one, and measure your own workload before restructuring an API for it.
Why did list.remove(1) delete the wrong element?
Because List overloads remove(int index) and remove(Object o), and 1 is an int, which matches remove(int) exactly in phase 1. The element at index 1 was removed rather than the element equal to 1. Write list.remove(Integer.valueOf(1)) to remove by value.
How do I return two values from a Java method?
Return one object holding both. A record is the cleanest form — record MinMax(int min, int max) {} — because the two values keep their names and their own types. An array works when both values share a type and the order is self-evident. Writing into a caller-supplied array works too, but it hides in the signature what the method does, so prefer the first two.
Conclusion
Two ideas do most of the work. First, an argument is copied into the parameter, always — which is why swap cannot work, why arr[0] = 99 inside a method is visible outside, and why reassigning a parameter never reaches the caller. Java passes references by value; it is not pass-by-reference. Second, an overload is chosen at compile time from the static types, in phases that run exact match, then widening, then boxing, then varargs, and never step back — which is why f(long) beats f(Integer) for an int and why list.remove(1) removes by index.
The rest is a short list of rules: one return value or none, a record when you need two, final freezes the parameter and not the object, the parameter list is what distinguishes overloads, varargs must come last and hands you an empty array rather than null, and an overload set that means two different things is a bug waiting to be written.
Next in this series: local variables, scope and where a variable lives — which blocks a name is visible in, when it is created and destroyed, and why shadowing a name is legal but rarely a good idea.