Java Streams and Lambda Expressions: An In-Depth Exploration

We have already talked about Streams and Lambda expressions in JAVA on other posts, but on this one, I'd like to talk about it again on more specific topics related to some more common features and functions used by our community.

In this article, we will delve into some of the most important stream operations: filter, map, reduce, flatMap, and collect. Each section will provide a detailed explanation, followed by both simple and complex examples to illustrate their usage.

1. Filter

The filter method is used to select elements that satisfy a given predicate. It returns a stream consisting of the elements that match the predicate.

Simple Example 1:

List<String> names = Arrays.asList("Alice", "Bob", "Charlie", "David");
List<String> filteredNames = names.stream()
                                  .filter(name -> name.startsWith("A"))
                                  .collect(Collectors.toList());
System.out.println(filteredNames); // Output: [Alice]
  • Explanation:

    • names.stream(): Converts the list of names into a stream.

    • .filter(name -> name.startsWith("A")): Filters the stream to include only names that start with "A".

    • .collect(Collectors.toList()): Collects the filtered names into a new list.

Simple Example 2:

List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5, 6);
List<Integer> evenNumbers = numbers.stream()
                                   .filter(n -> n % 2 == 0)
                                   .collect(Collectors.toList());
System.out.println(evenNumbers); // Output: [2, 4, 6]
  • Explanation:

    • numbers.stream(): Converts the list of numbers into a stream.

    • .filter(n -> n % 2 == 0): Filters the stream to include only even numbers.

    • .collect(Collectors.toList()): Collects the filtered numbers into a new list.

Complex Example 1:

List<Employee> employees = // initialize with Employee objects
List<Employee> highEarners = employees.stream()
                                      .filter(e -> e.getSalary() > 100000)
                                      .collect(Collectors.toList());
  • Explanation:

    • employees.stream(): Converts the list of employees into a stream.

    • .filter(e -> e.getSalary() > 100000): Filters the stream to include only employees with a salary greater than 100,000.

    • .collect(Collectors.toList()): Collects the filtered employees into a new list.

Complex Example 2:

Map<String, List<Product>> productsByCategory = // initialize with category-product mapping
List<Product> expensiveElectronics = productsByCategory.get("Electronics").stream()
                                                       .filter(p -> p.getPrice() > 1000)
                                                       .collect(Collectors.toList());
  • Explanation:

    • productsByCategory.get("Electronics").stream(): Retrieves the list of electronic products and converts it into a stream.

    • .filter(p -> p.getPrice() > 1000): Filters the stream to include only products with a price greater than 1000.

    • .collect(Collectors.toList()): Collects the filtered products into a new list.

2. Map

The map method applies a given function to each element of the stream, producing a stream of the transformed elements.

Simple Example 1:

List<String> names = Arrays.asList("Alice", "Bob", "Charlie");
List<Integer> nameLengths = names.stream()
                                 .map(String::length)
                                 .collect(Collectors.toList());
System.out.println(nameLengths); // Output: [5, 3, 7]
  • Explanation:

    • names.stream(): Converts the list of names into a stream.

    • .map(String::length): Maps each name to its length.

    • .collect(Collectors.toList()): Collects the lengths into a new list.

Simple Example 2:

List<Integer> numbers = Arrays.asList(1, 2, 3);
List<Integer> squaredNumbers = numbers.stream()
                                      .map(n -> n * n)
                                      .collect(Collectors.toList());
System.out.println(squaredNumbers); // Output: [1, 4, 9]
  • Explanation:

    • numbers.stream(): Converts the list of numbers into a stream.

    • .map(n -> n * n): Maps each number to its square.

    • .collect(Collectors.toList()): Collects the squared numbers into a new list.

Complex Example 1:

List<Employee> employees = // initialize with Employee objects
List<String> employeeNames = employees.stream()
                                      .map(Employee::getName)
                                      .collect(Collectors.toList());
  • Explanation:

    • employees.stream(): Converts the list of employees into a stream.

    • .map(Employee::getName): Maps each employee to their name.

    • .collect(Collectors.toList()): Collects the names into a new list.

Complex Example 2:

List<Product> products = // initialize with Product objects
Map<String, Double> productPriceMap = products.stream()
                                              .collect(Collectors.toMap(Product::getName, Product::getPrice));
  • Explanation:

    • products.stream(): Converts the list of products into a stream.

    • .collect(Collectors.toMap(Product::getName, Product::getPrice)): Collects the products into a map where the key is the product name and the value is the product price.

3. Reduce

The reduce method performs a reduction on the elements of the stream using an associative accumulation function and returns an Optional. It is often used for operations such as summing, concatenating, or finding the maximum or minimum.

Detailed Explanation:

  • reduce takes three parameters:

    1. Identity: The initial value for the reduction and the default result if the stream is empty.

    2. Accumulator: A function that takes two parameters: a partial result and an element, and combines them into a new partial result.

    3. Combiner (optional): A function used to combine the results of the accumulator when the stream is processed in parallel.

Simple Example 1:

List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5);
int sum = numbers.stream()
                 .reduce(0, (a, b) -> a + b);
System.out.println(sum); // Output: 15
  • Explanation:

    • numbers.stream(): Converts the list of numbers into a stream.

    • .reduce(0, (a, b) -> a + b):

      • Identity: 0 (the initial value).

      • Accumulator: (a, b) -> a + b (a function that adds the partial result a to the current element b).

    • The stream elements are combined by adding them together, resulting in the sum of all elements.

Simple Example 2:

List<String> words = Arrays.asList("Java", "Stream", "API");
String concatenatedString = words.stream()
                                 .reduce("", (a, b) -> a + b);
System.out.println(concatenatedString); // Output: JavaStreamAPI
  • Explanation:

    • words.stream(): Converts the list of words into a stream.

    • .reduce("", (a, b) -> a + b):

      • Identity: "" (the initial empty string).

      • Accumulator: (a, b) -> a + b (a function that concatenates the partial result a with the current element b).

    • The stream elements are concatenated together, resulting in a single concatenated string.

Complex Example 1:

List<Employee> employees = // initialize with Employee objects
double totalSalaries = employees.stream()
                                .map(Employee::getSalary)
                                .reduce(0.0, Double::sum);
  • Explanation:

    • employees.stream(): Converts the list of employees into a stream.

    • .map(Employee::getSalary): Maps each employee to their salary, creating a stream of salaries.

    • .reduce(0.0, Double::sum):

      • Identity: 0.0 (the initial value).

      • Accumulator: Double::sum (a method reference that adds the partial result to the current element).

    • The stream of salaries is reduced to the total sum of all salaries.

Complex Example 2:

List<Order> orders = // initialize with Order objects
double totalRevenue = orders.stream()
                            .map(Order::getTotalPrice)
                            .reduce(0.0, Double::sum);
  • Explanation:

    • orders.stream(): Converts the list of orders into a stream.

    • .map(Order::getTotalPrice): Maps each order to its total price, creating a stream of total prices.

    • .reduce(0.0, Double::sum):

      • Identity: 0.0 (the initial value).

      • Accumulator: Double::sum (a method reference that adds the partial result to the current element).

    • The stream of total prices is reduced to the total revenue from all orders.

4. FlatMap

The flatMap method is used to flatten a stream of collections into a single stream.

Simple Example 1:

4. FlatMap

The flatMap method is used to flatten a stream of collections into a single stream. It is particularly useful when dealing with nested collections.

Simple Example 1:

List<List<String>> namesNested = Arrays.asList(
    Arrays.asList("Alice", "Bob"),
    Arrays.asList("Charlie", "David")
);
List<String> namesFlat = namesNested.stream()
                                    .flatMap(Collection::stream)
                                    .collect(Collectors.toList());
System.out.println(namesFlat); // Output: [Alice, Bob, Charlie, David]
  • Explanation:

    • namesNested.stream(): Converts the list of lists into a stream.

    • .flatMap(Collection::stream): Flattens each inner list into a single stream of names.

    • .collect(Collectors.toList()): Collects the flattened names into a new list.

Simple Example 2:

List<List<Integer>> numbersNested = Arrays.asList(
    Arrays.asList(1, 2),
    Arrays.asList(3, 4)
);
List<Integer> numbersFlat = numbersNested.stream()
                                         .flatMap(List::stream)
                                         .collect(Collectors.toList());
System.out.println(numbersFlat); // Output: [1, 2, 3, 4]
  • Explanation:

    • numbersNested.stream(): Converts the list of lists into a stream.

    • .flatMap(List::stream): Flattens each inner list into a single stream of numbers.

    • .collect(Collectors.toList()): Collects the flattened numbers into a new list.

Complex Example 1:

List<Department> departments = // initialize with Department objects, each containing a list of employees
List<Employee> allEmployees = departments.stream()
                                         .flatMap(department -> department.getEmployees().stream())
                                         .collect(Collectors.toList());
  • Explanation:

    • departments.stream(): Converts the list of departments into a stream.

    • .flatMap(department -> department.getEmployees().stream()): Flattens each department's list of employees into a single stream of employees.

    • .collect(Collectors.toList()): Collects all employees into a new list.

Complex Example 2:

List<Company> companies = // initialize with Company objects, each containing a list of departments
List<Employee> allEmployees = companies.stream()
                                       .flatMap(company -> company.getDepartments().stream())
                                       .flatMap(department -> department.getEmployees().stream())
                                       .collect(Collectors.toList());
  • Explanation:

    • companies.stream(): Converts the list of companies into a stream.

    • .flatMap(company -> company.getDepartments().stream()): Flattens each company's list of departments into a single stream of departments.

    • .flatMap(department -> department.getEmployees().stream()): Flattens each department's list of employees into a single stream of employees.

    • .collect(Collectors.toList()): Collects all employees into a new list.

5. Collect

The collect method is used to transform the elements of the stream into a different form, often a collection like a List, Set, or Map.

Simple Example 1:

List<String> names = Arrays.asList("Alice", "Bob", "Charlie");
Set<String> namesSet = names.stream()
                            .collect(Collectors.toSet());
System.out.println(namesSet); // Output: [Alice, Bob, Charlie]
  • Explanation:

    • names.stream(): Converts the list of names into a stream.

    • .collect(Collectors.toSet()): Collects the elements of the stream into a set.

Simple Example 2:

List<String> names = Arrays.asList("Alice", "Bob", "Charlie");
Map<String, Integer> nameLengthMap = names.stream()
                                          .collect(Collectors.toMap(name -> name, String::length));
System.out.println(nameLengthMap); // Output: {Alice=5, Bob=3, Charlie=7}
  • Explanation:

    • names.stream(): Converts the list of names into a stream.

    • .collect(Collectors.toMap(name -> name, String::length)): Collects the elements of the stream into a map where the key is the name and the value is the length of the name.

Complex Example 1:

List<Employee> employees = // initialize with Employee objects
Map<Department, List<Employee>> employeesByDepartment = employees.stream()
                                                                 .collect(Collectors.groupingBy(Employee::getDepartment));
  • Explanation:

    • employees.stream(): Converts the list of employees into a stream.

    • .collect(Collectors.groupingBy(Employee::getDepartment)): Groups the employees by their department into a map where the key is the department and the value is the list of employees in that department.

Complex Example 2:

List<Transaction> transactions = // initialize with Transaction objects
Map<Currency, Double> totalByCurrency = transactions.stream()
                                                    .collect(Collectors.groupingBy(Transaction::getCurrency,
                                                                                   Collectors.summingDouble(Transaction::getAmount)));
  • Explanation:

    • transactions.stream(): Converts the list of transactions into a stream.

    • .collect(Collectors.groupingBy(Transaction::getCurrency, Collectors.summingDouble(Transaction::getAmount))): Groups the transactions by currency and sums the amounts for each currency into a map where the key is the currency and the value is the total amount for that currency.

These detailed explanations and examples should provide a comprehensive understanding of these fundamental stream operations. By mastering these techniques, you can write more efficient and expressive code in Java.

0
Subscribe to my newsletter

Read articles from André Felipe Costa Bento directly inside your inbox. Subscribe to the newsletter, and don't miss out.

Written by

André Felipe Costa Bento
André Felipe Costa Bento

Fullstack Software Engineer.