Explain Codes LogoExplain Codes Logo

What's the difference between map() and flatMap() methods in Java 8?

java
flatmap
map
streams
Anton ShumikhinbyAnton Shumikhin·Aug 3, 2024
TLDR

The map() function applies a transformation to each element, generating a new stream without disturbing the original structure. flatMap() works similarly but merges an array of streams into one.

  • map(): Stream.of("1", "2").map(Integer::valueOf); generates a Stream<Integer> containing [1, 2].
  • flatMap(): Stream.of(Stream.of(1), Stream.of(2)).flatMap(Function.identity()); results in a Stream<Integer> possessing [1, 2].

The map() function is for singular transformations, while flatMap() is for complex transformations of nested data.

Mapping and flattening – going under the hood

Map() applies a function encapsulating each product, thereby maintaining the original stream's structure. It shines for one-to-one transformations, common when you need a simple type conversion: for example, transforming Strings into Integers.

Different from map(), flatMap is versatile, producing multiple, single or no output per input. This makes it ideal for handling interwoven data collections and when you need to combine streams instead of having streams within streams. It proficiently avoids type mismatches, allowing for a clean Stream<T> instead of a messy Stream<Stream<T>>.

If you've got a List of Lists and you just want one big Yuuge list, flatMap() is your method of choice:

List<List<Integer>> listOfLists = Arrays.asList( Arrays.asList(1, 2), Arrays.asList(3, 4, 5), Arrays.asList(6) ); List<Integer> flattenedList = listOfLists.stream() .flatMap(List::stream) // Yuuge up here .collect(Collectors.toList()); // The result, Yuuge flattenedList: [1, 2, 3, 4, 5, 6]

Diving deeper with flatMap()

Working with arrays within streams

flatMap() is a blessing when handling arrays wrapped in streams. Paired with Arrays::stream, it effortlessly flattens the array, just like folding a laundry:

Stream<String[]> arrayStream = Stream.of(new String[]{"a", "b"}, new String[]{"c", "d"}); Stream<String> flattenedStream = arrayStream.flatMap(Arrays::stream); // Laundry folded. // Flattened stream: ["a", "b", "c", "d"]

Extracting unique elements from nested data

Suppose you aim to pull out unique elements from tangled structures; flatMap with distinct is a Power Rangers combo. It allows collapsing duplicates in convoluted streams:

Stream<List<Integer>> listStream = Stream.of(Arrays.asList(1, 2), Arrays.asList(2, 3)); Stream<Integer> uniqueElements = listStream.flatMap(List::stream).distinct(); // Look mum, no duplicates!! // Unique elements: [1, 2, 3]

juggling optional values with flatMap()

flatMap() also plays well with Optional and other monadic types, offering a neat way of handling nullable values, eliminating that dreaded NullPointerException:

Stream<Optional<Integer>> options = Stream.of(Optional.of(1), Optional.empty(), Optional.of(3)); Stream<Integer> presentValues = options.flatMap(Optional::stream); // Harry Houdini couldn't have disappeared better. // Present values: [1, 3]

When making the choice: map() or flatMap()?

The 'dev's' rule of thumb

While map and flatMap are both tools, flatMap should be your go-to method for transformational operations. As a rule of thumb, use map() for basic logistical operations and flatMap for more complexity.

Performance implications

It's critical to be aware that flatMap could potentially slow down performance due to its flattening process. It can become heavier in terms of performance when compared to map, especially handling large streams or intricate operations.

Pay attention to type safety and code readability

Although flatMap adds a layer of intricacy, it compensates through type safety. Additionally, producing more readable code when dealing with nested streams. By eliminating nested structures, it lowers mental overhead, making your code cleaner and easier to maintain.