Filter Nested Collections with Stream in Java

Java Stream API provides excellent filtering capability on the elements in the Stream. Sometimes, the stream elements also contain the nested collections; we need to filter those elements in the nested collections. This Java tutorial discusses various approaches with examples to apply filtering on nested collections and collecting the filtered output elements in a new collection.

1. Setup

Let’s create some dummy data close to real-world applications to make the example more realistic. In this example, we have two classes AccountStatement and Transaction. Each account statement is associated with a list of transactions of varying amounts. We will apply the filtering to the transaction amounts.

@Data
@AllArgsConstructor
class AccountStatement {

  private long id;
  private String accountNumber;
  private List<Transaction> transactions;
}

@Data
@AllArgsConstructor
class Transaction {

  private long id;
  private double amount;
  private String accountNumber;
}

Next, we have added the following dummy data. The account statement 1 and 3 have transactions (one each) that value greater than 500. We will apply filtering to these records.

List<AccountStatement> accStmts = List.of(
        new AccountStatement(1, "A001", List.of(
            new Transaction(1, 100, "A001"),
            new Transaction(2, 60, "A001"),
            new Transaction(3, 550, "A001"))),  // More than 500

        new AccountStatement(2, "A002", List.of(
            new Transaction(4, 200, "A002"),
            new Transaction(5, 160, "A002"),
            new Transaction(6, 100, "A002"))),

        new AccountStatement(3, "A003", List.of(
            new Transaction(7, 10, "A003"),
            new Transaction(8, 20, "A003"),
            new Transaction(9, 3040, "A003"))));  // More than 500

2. Getting the List of Transactions

First, let us see a few examples to filter the list of transactions worth more than 500.

2.1. Using flatMap() and filter()

This is a quite straightforward method. In this approach, we flatten the nested collections into a single stream using ‘flatMap‘, and then apply the filtering condition using ‘filter‘.

List<Transaction> transactionsMoreThan500 = accStmts.stream()
  .flatMap(stmt -> stmt.getTransactions().stream())
  .filter(transaction -> transaction.getAmount() > 500)
  .collect(Collectors.toList());

System.out.println(transactionsMoreThan500);

The program output:

[Transaction(id=3, amount=550.0, accountNumber=A001), 
 Transaction(id=9, amount=3040.0, accountNumber=A003)]

2.2. Using mapMulti()

The mapMulti() method allows to apply conditions on stream elements and output an element of our choice if we want. We can skip the output element or even return multiple elements for a single input element.

We can use the mapMulti() method to add the condition (amount greater than 500) and return such filtered transactions as follows:

List<Transaction> transactionsMoreThan500_V2 = accStmts.stream()
    .<Transaction>mapMulti((stmt, consumer) -> {
      for (Transaction t : stmt.getTransactions()) {
        if (t.getAmount() > 500) {
          consumer.accept(t);
        }
      }
    })
    .collect(Collectors.toList());

System.out.println(transactionsMoreThan500_V2);

The program output:

[Transaction(id=3, amount=550.0, accountNumber=A001), 
 Transaction(id=9, amount=3040.0, accountNumber=A003)]

3. Getting the List of Account Statements

Sometimes, we may want to collect the accounts that have transactions worth more than 500. In such a case, simple flatmap() operation will not help. Let us see how we can perform filtering in this case.

3.1. Using mapMulti()

In this solution, we have made a slight change to the previous solution of mapMulti(). Everything is the same till we check the condition (amount > 500). In this solution, rather than adding the transaction instance we add the statement itself in the downstream consumer.

List<AccountStatement> stmtHavingTransactionsMoreThan500 = accStmts.stream()
  .<AccountStatement>mapMulti((stmt, consumer) -> {
    for (Transaction t : stmt.getTransactions()) {
      if (t.getAmount() > 500) {
        consumer.accept(stmt);     // Slight change here - Add 'stmt' in place of 't'
        break;
      }
    }
  })filter
  .collect(Collectors.toList());

System.out.println(stmtHavingTransactionsMoreThan500);

The program output:

[AccountStatement(id=1, accountNumber=A001, transactions=[...]), 
 AccountStatement(id=3, accountNumber=A003, transactions=[...])]

3.2. Using filter() and anyMatch()

As we are not collecting the elements of the type of nested collection, we can apply simple filtering and check the condition using the filter() method or any built-in method for conditions such as anyMatch().

List<AccountStatement> stmtHavingTransactionsMoreThan500_V2 = accStmts.stream()
  .filter(
    stmt -> stmt.getTransactions()
      .stream()
      .anyMatch(transaction -> transaction.getAmount() > 500)
  )
  .collect(Collectors.toList());

System.out.println(stmtHavingTransactionsMoreThan500_V2);

The program output:

[AccountStatement(id=1, accountNumber=A001, transactions=[...]), 
 AccountStatement(id=3, accountNumber=A003, transactions=[...])]

4. Conclusion

This short Java tutorial discussed various approaches to filter a Java stream based on a condition on nested collection. We looked upon the solutions using flatMap(), map(), mapMulti() and anyMatch() methods provided by the Stream interface.

Happy Learning !!

Source Code on Github

Comments

Subscribe
Notify of
guest
0 Comments
Inline Feedbacks
View all comments

About Us

HowToDoInJava provides tutorials and how-to guides on Java and related technologies.

It also shares the best practices, algorithms & solutions and frequently asked interview questions.