🔌 Interfaces & Abstraction · Intermediate

Programming to an interface in Java

List<String> list = new ArrayList<>() and loose coupling.

🧩 The mysteryYour app stores users in a HashMap. Tomorrow the boss wants them sorted. Is that a one-line change or a week of rewrites? It depends on one habit.

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 changes
🔮 Predict it

The price of the contract

What happens here?

List<String> names = new ArrayList<>();
names.add("Ada");
names.ensureCapacity(100);
System.out.println(names);
  1. [Ada]
  2. Compile error
  3. []
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

✗ Leaks the choice
ArrayList<String> loadNames() {
    return new ArrayList<>(db.names());
}

Every caller is now tied to ArrayList forever.

✓ Hides the choice
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.)

💼 In the real world

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

  1. List<String> names = new ArrayList<>();
  2. void print(Collection<String> items) accepts any collection
  3. Swapping to LinkedList changes one line
  4. 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.

🤯 Did you know?

"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);
  1. [5]
  2. Compile error
  3. []
  4. 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)));
}
  1. 3 3
  2. Compile error: a Set is not a List
  3. 3 0
  4. 12 3
Check your answer

3 3. Because the parameter is the broad Collection interface, both a List and a Set are accepted.

Next: how does Collections.sort know that "Apple" comes before "fig"? A one-method interface decides.