What the interviewer read in one line
The algorithm can be right while the declaration quietly makes it slower or older than it needs to be. These are the lines that make an interviewer pause.
| You wrote | What it signals | Write instead |
|---|
Stack<Integer> st = new Stack<>(); | Learned from an old tutorial. Stack extends Vector, whose operations are synchronised. | Deque<Integer> st = new ArrayDeque<>(); |
Queue<int[]> q = new LinkedList<>(); | Works, but allocates a node per element. | Queue<int[]> q = new ArrayDeque<>(); |
List<Integer> q = new ArrayList<>(); then q.remove(0) | Every poll shifts the remaining elements, so a BFS turns quadratic. | ArrayDeque and poll() |
List<String> seen = new ArrayList<>(); then seen.contains(x) in a loop | contains is a linear scan, so the loop is O(n²). | Set<String> seen = new HashSet<>(); |
ArrayList<Order> load(ArrayList<Long> ids) | Callers are tied to one implementation for no gain. | List<Order> load(List<Long> ids) |
The Stack row is not a style preference. The Stack Javadoc itself points to the Deque interface and its implementations as the more complete and consistent choice. The ArrayDeque Javadoc says it is likely to be faster than Stack as a stack and faster than LinkedList as a queue. Note its one restriction: it rejects null elements.
The remove(0) row has become easier to fall into. Since JDK 21, List has a removeFirst() method, declared by SequencedCollection, which reads like a queue operation. On an ArrayList it still shifts every remaining element, so it costs O(n) like remove(0).
Declared type and implementation are two separate decisions
Programming to interfaces starts from noticing there are two choices in every declaration. The declared type says what the code is allowed to do with the object. The implementation decides what each of those operations costs. Treat them as one choice and you get either a signature that leaks an implementation, or a cost you did not see.
Programming to interfaces matters most at boundaries: parameters, return types and fields. A method that accepts List<Long> works for an ArrayList, an immutable List.of(...) result, or a subList view, and the caller never has to copy. A method that demands ArrayList<Long> forces that copy on everyone who holds something else.
For a local variable inside a forty-line interview solution, the stakes are smaller. Declaring ArrayDeque<Integer> queue locally, or using var since Java 10, is defensible, and a strong candidate does not moralise about it. What interviewers do mark down is the opposite mistake: declaring an interface narrower than the operations you need. Declare Queue<Integer> and you cannot call pollLast(), so a sliding-window solution that needs both ends must declare Deque<Integer>. Pick the narrowest interface that still offers every operation you use.
An indexed loop over a List parameter
This is where the "always program to interfaces" answer stops being enough. Write a method against List, then loop over it by index:
The List interface promises what get(i) returns, not how fast. LinkedList.get(i) walks from whichever end is nearer, so it is O(n), and the first method is O(n²) on a linked list. The declaration said "any List", and the body silently said "an ArrayList".
The JDK has a name for the missing promise. RandomAccess is a marker interface that ArrayList implements and LinkedList does not. Collections.binarySearch checks for it and, on a large list without it, falls back to an iterator-based strategy. You rarely need to test for it yourself. You do need to write the second method, or declare a parameter type whose contract matches your loop.
So the strong answer to "why List and not ArrayList?" has two halves, and together they are programming to interfaces done properly. You declared the interface so callers are not coupled to an implementation. And you kept the body honest to that declaration, iterating rather than indexing, so every implementation the signature accepts runs in the time you claimed.
Fix the declaration out loud
When the interviewer stops at the line, do not defend it. Read it back as a claim: "I declared a List but I only ever ask whether something is in it, so this should be a HashSet, and the loop drops from O(n²) to O(n) expected." Then change it. That sentence shows the habit the pause was testing: matching the structure to the operations it serves. Two neighbouring questions go further into the same classes, on fail-fast iteration and on HashMap internals.
Programming to interfaces is a promise in two directions. Callers may pass any implementation of the type you declared, so the body must run in the time you claimed for every one of them.
© 2026 Preptima. Originally published at preptima.com (opens in a new tab).