avatarGavin Fong

Summary

This context provides 15 practical exercises to help master the Java Stream API, a powerful tool for simplifying code logic and reducing lines of code.

Abstract

The Java Stream API, introduced in Java 8, offers a new way of coding that simplifies code logic and reduces the lines of code required for many programming tasks. This context provides 15 practical exercises based on a data model of customers, orders, and products to help users master the Java Stream API. The exercises cover a wide range of scenarios and operations, including filtering, transforming, sorting, and consolidating data. The context also includes a Spring Boot project with a data model, repositories, and sample data for users to practice with.

Bullet points

  • The Java Stream API, introduced in Java 8, simplifies code logic and reduces lines of code.
  • The context provides 15 practical exercises to help master the Java Stream API.
  • The exercises are based on a data model of customers, orders, and products.
  • The exercises cover a wide range of scenarios and operations, including filtering, transforming, sorting, and consolidating data.
  • A Spring Boot project with a data model, repositories, and sample data is provided for users to practice with.
  • The context includes a GitHub link to the project.
  • The context lists the Java Stream API operations covered in the exercises.
  • The context provides detailed solutions for each exercise, including code snippets.
  • The context includes a diagram illustrating the general concept of the Java Stream API.
  • The context includes an entity relationship diagram of the data model used in the exercises.
  • The context includes a source code listing of the data model.
  • The context includes a link to the Spring JPA repository for each entity.
  • The context includes a note about the use of the in-memory H2 database.

15 Practical Exercises Help You Master Java Stream API

Simplify Your Code Logic Using Powerful Java Stream API

Photo by Compare Fibre on Unsplash

The debut of the Java stream API since Java 8 created a new way of coding which dramatically simplifies code logic and reduces the lines of code for many programming tasks. Instead of looping through each item from a list or array, Stream API works with data stream flow, so you can achieve business requirements by adding a series of operations to the stream.

Let me show you the power of the stream API with an example. Say, the task is to group an array of employee records into a data map organized by job title. Here is a traditional way to loop through the list and construct a data map.

Java Stream API imply applies the collector to generate data map grouping by employee’s title. You can see that the coding style making use the Stream API is simpler and you can write less code to achieve the same outcome.

Java Stream API is not only useful for data manipulation but also makes the data consolidation and calculation much easier. Let’s see another example which calculate average salary of all employee in the list

The traditional way is to create a for-loop to sum up the salary of each employee and then calculate the average by dividing the total sum by number of record.

How to achieve it using Java Stream API? It transforms employee records to salary amount in the data stream flow and then calculates the average. I would say the code using Stream API is more expressive and easier to read.

Key Concepts of Stream API

The design of Java stream API is in line with functional programming, it is a coding style to implement program logic by composing functions and executing them in a data flow.

The general concept of Java Stream API is illustrated in the diagram below, the stream emits data elements and passes through a list of operations such as data transformation, filtering and sorting. The whole process is ended by a terminal operation which generates output such as a calculated average and data collection.

In fact, the mechanism is similar to a for-loop which iterates each data element in a data list and carries out program logic. However, the program code is simpler and more readable.

Java Stream API — A Typical Data Flow

Java Stream API Exercises

Practicing hands-on exercises is a quick way to master a new skill. In this article, you will go through 15 exercises and cover a wide range of scenarios so as to gear you up the skill in Java Stream API.

The exercises are based on a data model — customer, order and product. Refer to the entity relationship diagram below, customers can place multiple orders and so it is a one-to-many relationship while the relationship between products and orders is many-to-many

Data Model

This is the source code of the data model

Spring JPA repository is provided for each entity, so that you can use a repository method such as findAll() to fetch records of product, order and customer.

I prepared a Spring Boot project with data model, repositories and sample data loaded into in-memory H2. So, you can play with the stream API using the sample data.

You will learn the following Java Steam API operations as you practice the 15 exercises, so you will have enough knowledge to further explore other operations.

Non-Terminal Operations

  • filter()
  • map()
  • distinct()
  • sorted()
  • peek()

Terminal Operations

  • anyMatch()
  • collect()
  • count()
  • findFirst()
  • min()
  • max()
  • sum()
  • average()

Exercise 1 — Obtain a list of products belongs to category “Books” with price > 100

This is obviously a filtering logic, the output should fulfill the two filtering requirements. So, you can apply 2 filter() operations to obtain the result.

Exercise 2 — Obtain a list of order with products belong to category “Baby”

You need to start from the data flow from the order entities and then check if order’s products belong to the category “Baby”. Hence, the filter logic looks into the products stream of each order record and use anyMatch() to determine if any product fulfill the criteria.

Exercise 3 — Obtain a list of product with category = “Toys” and then apply 10% discount

In this exercise, you will see how to transform data using the stream API. After you’ve obtained a product list with a category that belongs to “Toys” using filter(), you can then apply a 10% discount to the product price by using map().

Exercise 4 — Obtain a list of products ordered by customer of tier 2 between 01-Feb-2021 and 01-Apr-2021

This exercise illustrates the usage of flatMap(). You can firstly start from an order list and then filter the list by customer tier and order date. Next, get the product list from each order record and use flatMap() to emit product records into the stream. For example, if we have 3 order records and each order contains 10 products, then flatMap() will emit 10 data elements for each order record, resulting in 30 (3 x 10) product record output in the stream.

Since product list would contain duplicated product records if multiple orders would include the same product. In order to generate a unique product list, applying distinct() operation can help to produce the unique list.

Exercise 5 — Get the cheapest products of “Books” category

One of the effective ways to obtain the product with the lowest price is to sort the product list by price in an ascending order and get the first element. Java Stream API provides sorted() operation for stream data sorting based on specific field attributes. In order to obtain the first element in the stream, you can use the terminal operation findFirst(). The operation returns the first data element wrapped by Optional as it is possible that the output stream can be empty.

This solution can only return a single product record with the lowest price. If there are multiple product records with the same lowest price, the solution should be modified such that it looks for the lowest price amount first and then filters product records by the price amount so as to get a list of products with the same lowest price.

UPDATE:

Thanks Александр Шаклеин for your suggestion, use min() is a better solution as the code is cleaner and it can be done using 2 operatorsn(filter →min) instead of 3 (filter → sorted →findFirst).

Exercise 6 — Get the 3 most recent placed order

Similar to previous exercise, the obvious solution is to sort the order records by order date field. The tricky part is that the sorting this time should be in descending order such that you can obtain the order records with the most recent order date. It can be achieved simply by calling Comparator.reversed().

Exercise 7 — Get a list of orders which were ordered on 15-Mar-2021, log the order records to the console and then return its product list

You can see that this exercise involves two actions — (1) write order records to the console and (2) produce a list of products. Generating different output from a stream is not possible, how can we fulfill this requirement? Apart from running the stream flow twice, operation peek() allows execution of system logic as part of a stream flow. The sample solution runs peek() to write order records to the console right after data filtering, then subsequent operations such as flatMap() will be executed for the output of product records.

Exercise 8 — Calculate total lump sum of all orders placed in Feb 2021

All previous exercise was to output a record list by a terminal operation, let’s do some calculation this time. This exercise is to sum up all the products ordered in Feb 2021. As you’ve gone through previous exercises, you can easily obtain the list of products using filter() and flatMap() operations. Next, you can make use of mapToDouble() operation to convert the stream into a stream of data type Double by specifying the price field as the mapping value. At last, terminal operation sum() will help you add up all values and return the total value.

Exercise 9 — Calculate order average payment placed on 14-Mar-2021

In addition to total sum, stream API offers operation for average value calculation as well. You might find that the return data type is different from sum() as it is an Optional data type. The reason is that the data stream would be empty and so calculation won’t output an average value for a empty data stream.

Exercise 10 — Obtain a collection of statistic figures (i.e. sum, average, max, min, count) for all products of category “Books”

What if you need to get sum, average, max, min and count at the same time? Should we run the data stream 5 times to get those figures one by one? Such an approach is not quite effective. Luckily, stream API provides a convenient way to get all those values at once by using terminal operation summaryStatistics(). It returns a data type DoubleSummaryStatistics which contains all the required figures.

Exercise 11 — Obtain a data map with order id and order’s product count

Except for value calculation, all previous exercises just output a record list. The helper class Collectors provide a number of useful operations for data consolidation and data collection output. Let’s check out the exercise to create a data map with order id as the key while the associated value is product count. The terminal operation Collectors.toMap() accepts two arguments for your specify the key and value respectively.

Exercise 12 — Produce a data map with order records grouped by customer

This exercise is to consolidate a list of orders by customer. Collectors.groupingBy() is a handy function, you can just simply specify what is the key data element, it will then aggregate data for you.

Exercise 13 — Produce a data map with order record and product total sum

The data map output this time is not a simple extraction of data fields from the stream, you need to create a sub-stream for each order in order to calculate the product total sum. Since, the key element is the order record itself instead of an order id, so Function.identity() is used to tell Collectors.toMap() to use the data element as the key.

Exercise 14 — Obtain a data map with list of product name by category

This exercise helps you get familiar with the way to transform the data output of data map entries. If you only use Collectors.groupingBy(Product::getCategory), then the output will be Map<Category, List of Products> but the expected output should be Map<Category, List of Product Name>. You can use Collectors.mapping() to convert product objects to product names for the data map construction.

Exercise 15 — Get the most expensive product by category

Similar to data transformation using Collectors.mapping(), Collectors.maxBy() helps to obtain the record with max value as part of data map construction. By providing a comparator for product price, maxBy() is able to get the product with the largest value for each category.

Final Thoughts

Hope that the exercises are useful to help you get familiar with the use of Java Stream API and the way to write logic in a simpler approach. Without a doubt, it is a mindshift to adopt Java Stream API as your thinking process is to be transited from traditional imperative programming to functional programming. Hence, practicing exercises is important to help you think through the logic in a stream data flow. With the skill in Java Stream API, you can easily pick up technologies such as Spring WebFlux development as its coding style shared similar concept of Java Stream API.

Java
Stream
Reactive Programming
Programming
Software Engineering
Recommended from ReadMedium