java streams: working with maps

Updated: Jul 3

a set of information can be processed in many ways: using an iterator, a while loop, a for loop, an indexed or a rekursive loop. with the release of java 8 in march 2014 the java stream api was introduced. a java stream is a sequence of elements, like a list or a map, but supports aggregation, filtering, mapping and many other operations. [1]


converting a collection to a map


lets have a look at data with structure of the following pojos (plain old java objects). in our scenario books can have unique titles, one genre, a price, a page size, a unique isbn and a collection of authors. different books, can have the same price, genre, page size and authors.

record Book(String title,
            Genre genre,
            double price,
            int pages,
            long isbn,
            List<Author> authors) {
}

enum Genre {
    COMEDY, DRAMA, ACTION, THRILLER
}

record Author(String name,
              LocalDate dateOfBirth) {
}

lets consider we have a list of books, which reference some authors. if we want to categorize books by isbn, the usage of java streams can be useful. converting a list into a map is a proper way of doing that. in java streams usually the collect operation in combination with the method Collectors.toMap(..) can give us the desired result.

final Map<Long, Book> bookByIsbn = books.stream()
        .collect(Collectors.toMap(Book::isbn, Function.identity()));

now we can easily access the the right book without further iteration. but we can't use this feature for every property ob the book class. since the genre cannot be unique, the operation would throw an exception if we want exactly one book per genre. if we really want this, we have to add a merge function, a rule which describes what should be done if a second book with the same category occurs in the collection. in our example we override the existing book with the new occurrence.

final Map<Genre, Book> bookByGenre = books.stream()
        .collect(Collectors.toMap(Book::genre, Function.identity(), (b1, b2) -> b2));

to have all books of the same genre, without missing any, we need another operation called Collectors.grouping(..).

final Map<Genre, Set<Book>> booksByGenre = books.stream()
        .collect(Collectors.groupingBy(Book::genre, Collectors.toSet()));

if we aren't interested in the book itself, we can map the book to return the isbn only.

final Map<Genre, Set<Long>> isbnsByGenre = books.stream()
        .collect(Collectors.groupingBy(Book::genre, Collectors.mapping(Book::isbn, Collectors.toSet())));

the same we can do with authors, but since this property is a collection we need the operation Collectors.flatMapping(..).

final Map<Genre, Set<Author>> authorsByGenre = books.stream()
        .collect(Collectors.groupingBy(Book::genre, Collectors.flatMapping(book -> book.authors().stream(), Collectors.toSet())));

summary

of course we can still fill the map using conventional approaches, but thanks to the stream api and static helpers of the collectors interface we can build small statements to solve our problem, it is less verbose, easier to read, immutable and therefor thread-safe. try it out!


 

[1] Stream (Java SE 17 & JDK 17) (oracle.com)

[2] kmo-dev/java-streams (github.com)


22 views0 comments

Recent Posts

See All