Definition
Varargs is syntactic sugar for arrays:
- Syntax:
public void process(BigDecimal... amounts)accepts zero or more BigDecimal arguments. - Internally: The compiler converts it to
process(BigDecimal[] amounts). The bytecode is identical. - Calling: You can pass individual values:
process(a, b, c)or an array:process(new BigDecimal[]{a, b, c})or nothing:process(). - Rules: Must be last parameter. Only one per method.
A bank deposit slip with a variable number of amount fields. You can write one amount, five amounts, or no amounts at all. The teller processes whatever you provide, treating them all as a list internally.
Understanding the Concepts
How do varargs work?
public class TransferService {
// Varargs: accepts zero or more amounts
public BigDecimal totalDeposit(BigDecimal... amounts) {
BigDecimal total = BigDecimal.ZERO;
for (BigDecimal amount : amounts) {
total = total.add(amount);
}
return total;
}
}
TransferService svc = new TransferService();
// Call with individual values
BigDecimal t1 = svc.totalDeposit(
new BigDecimal("50000"),
new BigDecimal("12000"),
new BigDecimal("8000")
); // 70000
// Call with zero arguments
BigDecimal t2 = svc.totalDeposit(); // 0
// Call with an array
BigDecimal[] arr = {new BigDecimal("1000"), new BigDecimal("2000")};
BigDecimal t3 = svc.totalDeposit(arr); // 3000
What are the rules?
// Rule 1: varargs must be the LAST parameter
public void process(String accountId, BigDecimal... amounts) { } // OK
// public void process(BigDecimal... amounts, String accountId) { } // ERROR
// Rule 2: only ONE varargs parameter
// public void process(int... ids, String... names) { } // ERROR
// Rule 3: varargs can receive zero arguments
public void log(String... messages) {
// messages.length can be 0
}
log(); // OK: messages is an empty array
How does varargs interact with overloading?
Varargs has lower priority than exact matches:
static void print(String s) { System.out.println("exact: " + s); }
static void print(String... s) { System.out.println("varargs: " + Arrays.toString(s)); }
print("hello"); // "exact: hello" (exact match wins)
print("a", "b"); // "varargs: [a, b]" (only varargs matches)
print(); // "varargs: []" (only varargs matches zero args)
What is the ambiguity trap?
static void process(int... nums) { System.out.println("int varargs"); }
static void process(long... nums) { System.out.println("long varargs"); }
// process(1, 2, 3); // COMPILE ERROR: ambiguous – both match via widening!
When two varargs overloads compete and neither is more specific, the compiler rejects the call.
What about @SafeVarargs?
With generic varargs, the compiler warns about heap pollution:
// Warning: Possible heap pollution from parameterized vararg type
public static <T> List<T> listOf(T... items) {
return Arrays.asList(items);
}
// Suppress with @SafeVarargs (only on final or static methods)
@SafeVarargs
public static <T> List<T> safeListOf(T... items) {
return Arrays.asList(items); // safe: no modification of the array
}
What about performance?
Varargs creates a new array on every call:
// Each call allocates a new BigDecimal[] array
for (int i = 0; i < 1_000_000; i++) {
svc.totalDeposit(amount1, amount2); // new BigDecimal[2] each time
}
// For hot paths, pass a pre-built array instead
BigDecimal[] amounts = {amount1, amount2};
for (int i = 0; i < 1_000_000; i++) {
svc.totalDeposit(amounts); // reuses the same array
}
What are common mistakes?
- Varargs not last.
(int... ids, String name)does not compile. - Two varargs.
(int... a, String... b)does not compile. - Passing null explicitly.
process(null)passes a null array, not a one-element array containing null. Useprocess((String) null)orprocess(new String[]{null}). - Overloading ambiguity. Two varargs overloads with compatible types cause compile errors.
What should a senior engineer know?
- List.of() and Map.of() use varargs internally.
List.of(1, 2, 3)callsList.of(E... elements). - printf/String.format use varargs:
String.format("%s: %d", name, count). - In Spring, @Value annotation can inject arrays:
@Value("${rates}") double[] rates. - Varargs is a compiler feature. At bytecode level, it is just an array parameter. Reflection sees
BigDecimal[], notBigDecimal....
Visual Overview
Source: process(BigDecimal... amounts)
Bytecode: process(BigDecimal[] amounts)
→ identical at runtime
Calling:
process(a, b, c) → compiler creates new BigDecimal[]{a, b, c}
process() → compiler creates new BigDecimal[]{}
process(existingArray) → uses the array directly
Rules:
must be LAST parameter
only ONE per method
zero arguments allowed
exact match wins over varargs in overloading
Key Takeaway
Varargs lets a method accept zero or more arguments of the same type using the type... name syntax. The compiler converts it to an array internally, so the method receives a regular array. You can call it with individual values, with an array, or with no arguments at all.
The rules are simple: varargs must be the last parameter, and only one varargs parameter is allowed per method. In overloading, an exact match beats a varargs match. Two competing varargs overloads can cause ambiguity.
In banking, varargs is useful for methods that accept a variable number of amounts, IDs, or flags. But for hot paths, pass a pre-built array to avoid the allocation overhead of creating a new array on every call.
One sentence to remember: varargs (type... name) is syntactic sugar for an array parameter – must be last, only one per method, and creates a new array on each call.
---