Varargs
Accept any number of arguments with Type..., know that it is an array underneath, and avoid the null and primitive array traps.
ReturnType name(Type fixed, Type... rest) {
// rest is a Type[]; rest.length may be 0
}A varargs parameter, written Type... name, lets a caller pass zero or more arguments of that type. Inside the method it is simply an array: int... values arrives as an int[]. The compiler builds the array at the call site.
int sum(int... values) { ... }
sum(); // values is an empty array, length 0
sum(1, 2, 3); // values is {1, 2, 3}
sum(new int[] {4}); // an existing array is passed as is
The rules
- Only one varargs parameter per method, and it must be the last one.
- The caller may pass nothing at all, so the method must handle an empty array. If at least one value is required, make that value a normal parameter before the varargs:
max(int first, int... rest). The compiler then rejectsmax(). - An existing array can be passed directly in place of the separate values.
You already use varargs all the time: String.format, formatted, List.of beyond ten elements, Path.of(first, more...) and Arrays.asList. Spring's JdbcTemplate.update(String sql, Object... args) uses it for query parameters.
Traps
- null.
count((String) null)passes an array holding onenull.count((String[]) null)passes no array at all, so the parameter itself isnull. A barecount(null)compiles with a warning and means the second one. - Primitive arrays and Object.... An
int[]is anObjectbut not anObject[], so passing it toObject...gives an array with a single element: the wholeint[].Arrays.asList(new int[] {1, 2})is aList<int[]>of size one for the same reason. - Allocation. Every call allocates a new array. That is irrelevant in a request handler but can matter in a very hot loop. It is why
List.ofhas fixed-argument versions for up to ten elements. - Generic varargs. A parameter like
T... itemsproduces an unchecked warning about possible heap pollution. When the method only reads the elements and never stores or returns the array, annotate it with@SafeVarargs(allowed on static, final and private methods and constructors).
Varargs fit best for convenience APIs where callers write a short, literal list of values: a set of allowed roles, path segments, or query parameters. When the values usually come from a collection already, take a List instead.
Example
When to use it
- Check a user against several allowed roles with hasAnyRole(user, "admin", "editor").
- Pass bind values to a query helper shaped like JdbcTemplate.update(sql, Object... args).
- Build URL paths from segments with a path(first, more...) helper that cannot be called empty.
More examples
Requiring at least one argument
Putting a fixed parameter before the varargs moves an empty-input check from run time to compile time. hasAnyRole reads naturally at the call site, which is where varargs earn their keep.
// max() with no arguments will not compile: first is required.
int max(int first, int... rest) {
int best = first;
for (int n : rest) {
best = Math.max(best, n);
}
return best;
}
boolean hasAnyRole(Set<String> userRoles, String... allowed) {
for (String role : allowed) {
if (userRoles.contains(role)) {
return true;
}
}
return false;
}
void main() {
IO.println(max(7));
IO.println(max(3, 9, 4));
Set<String> roles = Set.of("editor");
IO.println(hasAnyRole(roles, "admin", "editor"));
IO.println(hasAnyRole(roles, "admin"));
}
// Output:
// 7
// 9
// true
// falseWhat null means with varargs
The cast decides whether null is one of the values or the array itself. Methods that accept varargs from other code should check the array for null before reading its length.
String count(String... names) {
if (names == null) {
return "no array";
}
return names.length + " element(s)";
}
void main() {
IO.println(count());
IO.println(count("a", "b"));
IO.println(count((String) null)); // an array holding one null
IO.println(count((String[]) null)); // no array: names itself is null
}
// Output:
// 0 element(s)
// 2 element(s)
// 1 element(s)
// no arrayObject... for query parameters
This is the same shape as JdbcTemplate.update(String sql, Object... args): primitives are boxed into the Object array. The last call shows the int[] trap, where the whole array becomes a single parameter.
String describeQuery(String sql, Object... params) {
long placeholders = sql.chars().filter(ch -> ch == '?').count();
if (placeholders != params.length) {
return "error: " + placeholders + " placeholder(s) but " + params.length + " value(s)";
}
return sql + " with " + Arrays.toString(params);
}
void main() {
IO.println(describeQuery("SELECT * FROM users WHERE id = ?", 42));
IO.println(describeQuery("SELECT * FROM orders WHERE status = ? AND total > ?", "PAID", 100));
IO.println(describeQuery("SELECT * FROM users WHERE id = ?"));
int[] ids = {1, 2, 3};
Object[] boxed = {1, 2, 3};
IO.println(describeQuery("? ? ?", boxed)); // an Object[] is passed as is
IO.println(describeQuery("? ? ?", ids)); // one element: the whole int[]
}
// Output:
// SELECT * FROM users WHERE id = ? with [42]
// SELECT * FROM orders WHERE status = ? AND total > ? with [PAID, 100]
// error: 1 placeholder(s) but 0 value(s)
// ? ? ? with [1, 2, 3]
// error: 3 placeholder(s) but 1 value(s)
Discussion