The 30 second answer
Map turns each element into exactly one new element, so a stream of users becomes a stream of names. FlatMap turns each element into a whole stream and then joins all those small streams into one. I use it when each item has many children, like orders with line items, and I want one flat stream instead of a stream of lists.
map(f) is one-to-one: each input element produces one output element. flatMap(f) is one-to-many: f returns a Stream for each element, and the results are concatenated into a single stream.
List<List<Integer>> nested = List.of(List.of(1, 2), List.of(3));
nested.stream().map(List::size).toList(); // [2, 1]
nested.stream().flatMap(List::stream).toList(); // [1, 2, 3]
orders.stream().flatMap(o -> o.items().stream()).mapToDouble(Item::price).sum();The same idea exists on Optional: map wraps the result, while flatMap expects a function that already returns an Optional, so you don't end up with Optional<Optional<T>>. Java 16 also added Stream.mapMulti, an imperative alternative to flatMap for when creating a small stream per element is awkward.
Sources: Stream Javadoc (map, flatMap, mapMulti), Optional Javadoc