Varargs

Accept any number of arguments with Type..., know that it is an array underneath, and avoid the null and primitive array traps.

SyntaxReturnType 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 rejects max().
  • 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 one null. count((String[]) null) passes no array at all, so the parameter itself is null. A bare count(null) compiles with a warning and means the second one.
  • Primitive arrays and Object.... An int[] is an Object but not an Object[], so passing it to Object... gives an array with a single element: the whole int[]. Arrays.asList(new int[] {1, 2}) is a List<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.of has fixed-argument versions for up to ten elements.
  • Generic varargs. A parameter like T... items produces 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

Try it yourself
Loading editor…
Press Run to execute the code.

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.

Example · java
// 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
// false

What 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.

Example · java
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 array

Object... 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.

Example · java
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

  • Be the first to comment on this lesson.