skip to content

From an API-design standpoint, why is Iterable separate from Collection, and why did the designers keep Map outside the Collection hierarchy? What does this teach about interface segregation?

level: principalimportance: nice to knowfreq 30%

answer

  1. Iterable = minimal 'traversable' capability (ISP)
  2. non-collections (lazy/infinite/cursors) can be Iterable
  3. Map has 2 type params, no honest single E
  4. Map as Collection would violate LSP -> use views instead
  5. accept Iterable, return Collection - weakest sufficient type

basics

~20 s

Iterable is split out so anything traversable - not just collections - can be used in for-each (streams sources, lazy/infinite sequences, custom cursors). Map is kept separate because it stores pairs, not single elements, so it can't honestly implement the single-element Collection contract.

solid answer

~50 s

The split reflects interface segregation: each interface promises exactly one capability so callers depend only on what they need. Iterable promises just 'you can traverse me once via an Iterator' - a far weaker, broader contract than Collection (size, add/remove, contains, bulk ops). By making Iterable the minimal super-interface, the JDK lets non-collection things (lazy generators, infinite sequences, resource-backed cursors) participate in for-each without pretending to be full collections, and it gives streams a clean source (spliterator()). Map stays outside Collection because Collection<E> is defined entirely in terms of a single element type E - add(E), contains(E), iterator() returning E. A Map has two type parameters and stores associations; there is no honest E for it. Rather than bolt on a leaky abstraction, the designers exposed Map's collection-shaped views (keySet/values/entrySet). The lesson: model the narrowest truthful contract, and prefer composition (views) over forcing a subtype relationship that would violate Liskov.

go deeper

for a junior

Can state that Iterable just means 'usable in for-each' and that Map is separate because it holds pairs.

for a middle

Explains the single-element vs pair mismatch and that Map exposes views instead of being a Collection.

for a senior

Frames the split via ISP and the Map decision via LSP, and knows to accept Iterable when only iterating.

for a principal

Reasons about the design trade-offs end to end (counterfactuals, future-proofing for streams, composition over inheritance, weakest-sufficient-type API contracts) and could defend or critique the JDK's choice.

## The question behind the question This is really about **interface design**: how the JDK authors decided *what promises each interface makes* and *who is allowed to implement it*. Two choices stand out: (a) `Iterable` is a separate, tinier interface *above* `Collection`, and (b) `Map` is deliberately **not** a `Collection`. ## Background terms - **Interface Segregation Principle (ISP):** the 'I' in SOLID - clients should not be forced to depend on methods they don't use; prefer many small, focused interfaces over one fat one. - **Liskov Substitution Principle (LSP):** a subtype must be usable anywhere its supertype is, *honoring the supertype's contract*. A subtype that can't honestly fulfill the contract shouldn't be a subtype. - **Reified vs erased / capability:** here, the relevant idea is matching an interface to the *capability* a type can truthfully provide. ## Why Iterable is separate from Collection `Collection<E>` is a **rich** contract: `size()`, `isEmpty()`, `add`, `remove`, `contains`, the bulk ops, `toArray`, plus `iterator()`. Many things in software can be **traversed** but are **not** full collections: - A **lazy or infinite sequence** (e.g. all natural numbers) - it has no `size()`, you can't `add` to it, but you *can* walk it. - A **resource-backed cursor** - lines of a file, rows of a result set; traversable once, not a stored container. - A **stream source** - `Iterable` only needs `iterator()`/`spliterator()`. If for-each had required `Collection`, none of these could use the clean loop syntax, and you'd be forced to implement (or throw `UnsupportedOperationException` from) a dozen irrelevant methods. By extracting the **minimal** capability - *"I can produce a one-shot cursor"* - into `Iterable`, the JDK lets all of the above participate in for-each and streams while only promising what they can deliver. This is **ISP in action**: depend on `Iterable<T>` in an API when all you need is to traverse; depend on `Collection`/`List` only when you need their extra powers. It also future-proofed the language: when Java 8 added streams, `Iterable.spliterator()` and the `Collection.stream()` default slotted in without breaking anyone. ## Why Map is NOT a Collection `Collection<E>` is defined *entirely* in terms of **one** element type `E`: `add(E)`, `remove(Object)`, `contains(Object)`, `iterator()` yielding `E`, `toArray()` of elements. A **`Map<K,V>`** stores **pairs**; it has **two** type parameters and no single, honest `E`. If `Map` extended `Collection<?>`, every one of those single-element methods would be a lie: - `add(E)` - add *what*? a key? a value? an entry? None fits cleanly. - `contains(Object)` - of a key or a value? - `iterator()` - over keys, values, or entries? Forcing the subtype relationship would **violate LSP**: a `Map` could not be safely used where a `Collection` is expected without surprising behavior or thrown exceptions. So the designers chose **composition over (inappropriate) inheritance**: `Map` is its own interface, and it *exposes* collection-shaped **views** - `keySet()` (a `Set<K>`), `values()` (a `Collection<V>`), `entrySet()` (a `Set<Map.Entry<K,V>>`). Those views are live (backed by the map), giving you all the Collection power *where it actually makes sense* without pretending the Map itself is a Collection. ## What this teaches 1. **Model the narrowest truthful contract.** `Iterable` promises only traversal; that single, honest promise makes it reusable far beyond collections. 2. **Don't force a subtype relationship that breaks the contract** (LSP). When a type can't honestly *be* a supertype, expose the overlap via **views/adapters** (composition) instead. 3. **Small interfaces compose and evolve well** (ISP). Splitting capabilities let streams be added later without breakage. 4. **APIs should accept the weakest type that suffices** - take `Iterable<T>` when you only iterate, return `List`/`Collection` when callers need more. ## Counterfactual Had the designers made `Map extends Collection<Map.Entry<K,V>>`, you'd gain a uniform `iterator()` but lose key-based clarity and invite contract violations (what does `add(entry)` mean if the key exists?). The chosen design - separate interface + views - is the cleaner trade.

  • If you were designing a method that only loops over its input, what parameter type should you accept and why?
    Iterable<T> (or Stream/Collection only if you need more). Accepting the weakest sufficient type maximizes the callers that can use your method - lazy sequences, custom cursors, any collection - which is exactly the ISP benefit Iterable provides.
  • How would making Map extend Collection<Map.Entry<K,V>> violate the Liskov principle?
    Collection methods like add(E)/contains(Object) would have ambiguous or exception-throwing meaning for a pair-based store, so a Map could not safely substitute for a Collection - breaking the supertype's contract. Views avoid this by exposing only the parts that map honestly.

saying these in an interview costs you the question

  • Saying Map is just an oversight rather than a deliberate contract decision
  • Claiming Iterable and Collection are redundant
  • Arguing Map should extend Collection<Map.Entry> with no awareness of the LSP/contract problems
  • Treating ISP/LSP as buzzwords without tying them to the concrete add(E)/iterator() mismatch

context