Java's Arrays.asList(int[]) Trap: The List of One That Looks Like Four

2026-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 Bug

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:

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.

The Fix

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.

Key Takeaway: 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.

All newsletters