Programming to an interface in Java
List<String> list = new ArrayList<>() and loose coupling.
Ask for a taxi, not a specific car
Declare variables, parameters and return types with the interface (List, Map), and pick the concrete class only at new. Callers depend only on the contract — that's loose coupling. Like asking for "a taxi" instead of "the blue Toyota": any taxi can do the job.
List<String> names = new ArrayList<>();
Map<String, Integer> ages = new HashMap<>();Swap in one line
Need cheap inserts at the front? Change only new ArrayList<>() to new LinkedList<>(). Need sorted keys? new HashMap<>() becomes new TreeMap<>(). Every other line keeps working, because it only knows the List or Map contract.
Map<String, User> cache = new TreeMap<>();
// was: new HashMap<>() — nothing else changesThe price of the contract
What happens here?
List<String> names = new ArrayList<>();
names.add("Ada");
names.ensureCapacity(100);
System.out.println(names);[Ada]Compile error[]
Show the answer
ensureCapacity exists only on ArrayList. The compiler allows only methods of the declared type, List. That's the trade-off: flexibility in exchange for using just the contract.
Broad parameters accept more
A parameter typed with a broad interface accepts every implementation. total(Collection<Integer> c) happily takes a List and a Set: total(List.of(1, 2)) and total(Set.of(3)) both return 3.
int total(Collection<Integer> c) {
int sum = 0;
for (int n : c) sum += n;
return sum;
}Return types
ArrayList<String> loadNames() {
return new ArrayList<>(db.names());
}Every caller is now tied to ArrayList forever.
List<String> loadNames() {
return new ArrayList<>(db.names());
}Change the implementation later without breaking callers. (Returning Object would go too far — it loses every useful method.)
Why teams insist on it
Frameworks like Spring inject dependencies by interface, so a test can pass a fake UserRepository and production a real database one. When a HashMap-backed cache needs to become thread-safe, it's often one new ConcurrentHashMap<>() away.
Key takeaways
- List<String> names = new ArrayList<>();
- void print(Collection<String> items) accepts any collection
- Swapping to LinkedList changes one line
- You can only call methods the declared type knows about
💡 Ask for 'a taxi', not 'the blue Toyota with plate 123' — any taxi can then do the job.
"Program to an interface, not an implementation" comes from the 1994 book Design Patterns by the "Gang of Four" — still quoted in code reviews today.
Practice questions
What does this print?
List<Integer> nums = new ArrayList<>();
nums.add(5);
nums.trimToSize();
System.out.println(nums);- [5]
- Compile error
- []
- Throws UnsupportedOperationException
Check your answer
Compile error. trimToSize() exists only on ArrayList. The variable's declared type is List, so the compiler won't allow it.
What does this print?
int total(Collection<Integer> c) {
int sum = 0;
for (int n : c) sum += n;
return sum;
}
void main() {
System.out.println(total(List.of(1, 2)));
System.out.println(total(Set.of(3)));
}- 3 3
- Compile error: a Set is not a List
- 3 0
- 12 3
Check your answer
3 3. Because the parameter is the broad Collection interface, both a List and a Set are accepted.