Streaming custom code to map – Functional style programming – extending API
179. Streaming custom code to map
Let’s assume that we have the following legacy class:
public class Post {
private final int id;
private final String title;
private final String tags;
public Post(int id, String title, String tags) {
this.id = id;
this.title = title;
this.tags = tags;
}
…
public static List<String> allTags(Post post) {
return Arrays.asList(post.getTags().split(“#”));
}
}
So, we have a class that shapes some blog posts. Each post has several properties including its tags. The tags of each post are actually represented as a string of tags separated by hashtag (#). Whenever we need the list of tags for a given post, we can call the allTags() helper. For instance, here is a list of posts and their tags:
List<Post> posts = List.of(
new Post(1, “Running jOOQ”, “#database #sql #rdbms”),
new Post(2, “I/O files in Java”, “#io #storage #rdbms”),
new Post(3, “Hibernate Course”, “#jpa #database #rdbms”),
new Post(4, “Hooking Java Sockets”, “#io #network”),
new Post(5, “Analysing JDBC transactions”, “#jdbc #rdbms”)
);
Our goal is to extract from this list a Map<String, List<Integer>> containing for each tag (key) the list of posts (value). For instance, for tag #database, we have articles 1 and 3, for tag #rdbms, we have articles 1, 2, 3, and 5, and so on.Accomplishing this task in functional programming can be done via flatMap() and groupingBy(). Both of these have been covered in detail in Java Coding Problems, First Edition. In a nutshell, flatMap() is useful to flatten a nested Stream<Stream<R>> model, while groupingBy() is a collector useful to group data in a map by some logic/property.We need flatMap() because we have the List<Post> that, for each Post, nests via allTags() a List<String> (so, if we simply call stream() then we get back a Stream<Stream<R>>). After flattening, we wrap each tag in Map.Entry<String, Integer>. Finally, we group these entries by tags into a Map as follows:
Map<String, List<Integer>> result = posts.stream()
.flatMap(post -> Post.allTags(post).stream()
.map(t -> entry(t, post.getId())))
.collect(groupingBy(Entry::getKey,
mapping(Entry::getValue, toList())));
But, based on the previous problem, we know that starting with JDK 16, we can use mapMulti(). So, we can re-write the previous snippet as follows:
Map<String, List<Integer>> resultMulti = posts.stream()
.<Map.Entry<String, Integer>>mapMulti((post, consumer) -> {
for (String tag : Post.allTags(post)) {
consumer.accept(entry(tag, post.getId()));
}
})
.collect(groupingBy(Entry::getKey,
mapping(Entry::getValue, toList())));
This time, we saved the map() intermediate operation and intermediate streams.