Java Streams grouping by on two fields

470 views Asked by At

I'm trying to group a data in a list and trying to add a count back to one of the fields of each object of list. Let me put some dummy example here as follows -

public class Book{
    String title;
    String author;
    int count;
}

and there's a list as follows

List<Book> posts = Arrays.asList(
                Book.builder().title("Book_1").author("ABC").build(),
                Book.builder().title("Book_2").author("PQR").build(),
                Book.builder().title("Book_1").author("ABC").build(),
                Book.builder().title("Book_3").author("XYZ").build()
        );

Now, my objective is to group data in a list by grouping title and author and update the count in count field of Book in list. i.e. in above list as title and author have same values, the count=2 for title="Book_1" and author="ABC", and for other entries count = 1.

I used Apache commons Pair.of() in grouping by as follows -

Map<Pair<String, String>, Long> map = posts.stream()
                .collect(Collectors.groupingBy(socialMediaPost -> Pair.of(socialMediaPost.getTitle(), socialMediaPost.getAuthor()), Collectors.counting()));

This actually produces the result but the return type is not what I want. I want List<Book> back with single entry for duplicate data(title, author) with increased count and other non-duplicate data with count = 1

4

There are 4 answers

3
Dave Ankin On BEST ANSWER

Maybe you could just use a second stream after your first? something like this where you just extract the books out of the intermediate Pair representation.

Edit: finally, you would just increment everytime you have to combine one, starting with an initial value of 1.

building off your idea, i think something like this would achieve the goal:

List<Book> posts = Arrays.asList(
        Book.builder().title("Book_1").author("ABC").build(),
        Book.builder().title("Book_2").author("PQR").build(),
        Book.builder().title("Book_1").author("ABC").build(),
        Book.builder().title("Book_3").author("XYZ").build()
);


List<Book> books = posts.stream()
        .collect(Collectors.groupingBy(
                socialMediaPost -> {
                    // initial value of 1
                    socialMediaPost.setCount(1);
                    return Pair.of(socialMediaPost.getTitle(),
                            socialMediaPost.getAuthor());
                },
                // gets the first of each repeat, incrementing count
                Collectors.reducing((a, b) -> a.setCount(a.getCount() + 1))))
        .values()
        .stream()
        .filter(Optional::isPresent)
        .map(Optional::get)
        .collect(Collectors.toList());

books.stream().map(Book::toString).forEach(System.out::println);

/*
SO_75775541.Book(title=Book_3, author=XYZ, count=1)
SO_75775541.Book(title=Book_1, author=ABC, count=2)
SO_75775541.Book(title=Book_2, author=PQR, count=1)
 */
2
Calvin P. On

Probably not the most elegant solution, but this should achieve the desired result.

The idea here is to identify the distinct elements and then reiterate through the stream to count how many times each appears in the original list and set that as the count for each distinct entry.

List<Book> filteredWithCounts = posts.stream().distinct().forEach(post -> 
    post.setCount(posts.stream().filter(p -> p.equals(post)).count()))
    .collect(Collectors.toList());

Another option would be to map with Book object as the key and then remap to a list of Book like so:

List<Book> result = posts.stream().collect(
    Collectors.groupingBy(Function.identity(), Collectors.counting()))
    .entrySet().stream().map(e -> {
        Book b = e.getKey();
        b.setCount(e.getValue());
        return b;
    }).collect(Collectors.toList());

Both methods assume that Book overrides the default equals method.

0
Bastien Aracil On

You can collect to a Map and use the merge option to increase the count. I assumed that count is initialized to 1 when a Book is instantiated (otherwise you will need to use something like Math.max(1,book,getCount()) instead of book.getCount() in the code below):

  final static Collector<Book, ?, Collection<Book>> COLLECTOR =
      Collectors.collectingAndThen(
          Collectors.toMap(
              book -> Pair.of(book.getTitle(), book.getAuthor()),
              Function.identity(),
              (book1, book2) -> {
                book1.setCount(book1.getCount() + book2.getCount());
                return book1;
              }
          ), Map::values);


  public static void main(String[] args) {
    List<Book> posts = Arrays.asList(
        new Book("Book_1", "ABC"),
        new Book("Book_2", "PQR"),
        new Book("Book_1", "ABC"),
        new Book("Book_3", "XYZ")
    );

    final var counted = posts.stream().collect(COLLECTOR);
    System.out.println(counted);

  }

It returns a Collection not a List but the modification is trivial and Collection might be what you need at the end.

0
Bhushan Nikhar On

I agree with Brian Goetz explanation of bad modeling during design phase. But if you are stuck with this design, here is my approach to do this.

You will have to do 2 tasks before you get count field populated for each book

  1. Count same books in List bookList.
  2. Populate book count for each book.

Although both tasks can be done in single stream, it complicates things too much and so hard to make changes. Here is how your code will look in 2 phase iteration for this problem.

 
    

    Map bookCountMap = new HashMap(); 
    //First iteration to identify counts for each book
    bookList.forEach((book)->{
        String key = book.getTitle()+"-"+book.getAuthor();
        int count = (bookCountMap.get(key) != null)?bookCountMap.get(key):0;
        bookCountMap.put(key, count+1);
    });
    //Second iteration to set count for each book
    System.out.println("Updating book model with counts:");
    bookList.forEach((book)->{
        String key = book.getTitle()+"-"+book.getAuthor();
        int count = (bookCountMap.get(key) != null)?bookCountMap.get(key):0;
        book.setCount(count);
        System.out.println(book);
    });