Arrays.asList(int[]) Trap: The List of One That Looks Like Four2026-05-22
Spot the bug. This class enforces a whitelist of allowed network ports. The data is right there in the array — why does every legitimate port get rejected?
import java.util.Arrays;
public class PortChecker {
private static final int[] ALLOWED = {22, 80, 443, 8080};
public static boolean isAllowed(int port) {
return Arrays.asList(ALLOWED).contains(port);
}
public static int allowedCount() {
return Arrays.asList(ALLOWED).size();
}
public static void main(String[] args) {
System.out.println(isAllowed(80)); // expected: true actual: false
System.out.println(isAllowed(443)); // expected: true actual: false
System.out.println(allowedCount()); // expected: 4 actual: 1
}
}
The signature of Arrays.asList is <T> List<T> asList(T... a). Because T must be a reference type, Java cannot bind it to int. So when you pass an int[], the compiler does not unpack it into individual elements. Instead it infers T = int[] and treats the whole array as a single varargs element.
The result: Arrays.asList(ALLOWED) returns a List<int[]> of size 1, whose sole element is the array itself. Then:
list.contains(80) autoboxes 80 to Integer and looks for it in a list containing one int[]. Integer.equals(int[]) is always false.list.size() returns 1, not 4.And here's the truly nasty part: it compiles without a warning. If you had written List<Integer> allowed = Arrays.asList(ALLOWED); the type checker would have caught it. But because contains takes Object and size returns an int, the chained one-liner is perfectly well-typed. Change the array's element type to Integer[] and the bug vanishes — which is why this often slips through in code that was refactored from Integer[] to int[] for "performance."
Static analyzers flag it (IntelliJ shows "Confusing call to Arrays.asList"), but only if you've enabled the inspection.
Use a stream and stay in primitive land — no boxing required:
import java.util.Arrays;
public class PortChecker {
private static final int[] ALLOWED = {22, 80, 443, 8080};
public static boolean isAllowed(int port) {
return Arrays.stream(ALLOWED).anyMatch(p -> p == port);
}
public static int allowedCount() {
return ALLOWED.length;
}
}
If you genuinely need a List<Integer>, box explicitly:
List<Integer> allowed = Arrays.stream(ALLOWED).boxed().toList();
The same trap waits in List.of, Stream.of, Collections.addAll, and any other varargs method called with a primitive array. long[], double[], char[], boolean[] — all of them get wrapped as a single element. Only object arrays (String[], Integer[], etc.) get unpacked the way you'd expect, because then T can bind to the element type.
The deeper lesson: Java's generics and varargs were bolted on after primitives were a fundamental part of the language, and the boundary between them is sharp and silent. When a generic method meets a primitive array, the array becomes one big opaque object — and no amount of code review will see it, because the code looks correct.
Arrays.asList(someIntArray) returns a one-element List<int[]>, not a list of integers — because Java generics can't bind a type parameter to a primitive, so the whole array is captured as the single varargs argument.
