Java 8 introduced a powerful new feature called the Stream API, which marked a significant evolution in the language. Developers can use
the Stream API to perform data processing operations on collections concisely
and expressively. In this blog post, we'll look at the Stream API, its core
concepts, and how it simplifies data processing in Java applications.
What is the Stream API?
In Java 8, a Stream is a collection of elements processed concurrently
or sequentially. CPUs have become more powerful and complex due to recent
advancements in hardware development, with multiple cores that can process
data in parallel. The Stream API was introduced to support parallel
data processing in Java, without the boiler code of defining and synchronizing
threads.
Unlike traditional collections, streams do not store data; they serve as a
pipeline for transforming and aggregating data from a source such as a List,
Set, or array.
Streams enable developers to perform declarative and functional operations
such as filtering, mapping, sorting, and reducing.
The Stream interface defines many operations, which can be divided into
two categories: intermediate operations and
terminal operations. Stream operations that can be connected are referred to as
intermediate operations, while those that close a stream are known as
terminal operations.
We can divide the operations into two groups:
- Intermediate Operations: filter() and map() return another stream as a return type and can be combined to form a pipeline.
- Terminal Operations: collect() executes the pipeline, returns the result, and closes it.
Creating Streams
Before optimizing our code with streams, let's look at how to create them. To
create a stream, we need a source. That source could be anything: a collection
(list, set, or map), an array, or I/O resources that are used as input.
A stream does not change its source, so multiple stream instances can be created from the same source and used for different purposes.
Create Streams from Collections
In the code snippet below, we convert a list of integers into a stream by
calling the stream() method. After creating a stream, we use the
forEach() method to print the stream's values as well as the name of
the execution thread on which this code is running.
public class StreamFromCollection {
public static void main(String[] args) {
List<Integer> geometrySeries = List.of(1, 3, 9, 27, 81, 243);
geometrySeries.stream()
.forEach(s -> System.out.println(Thread.currentThread().getName() + ": " + s));
}
}
The previous code generates a stream of integer elements. The most commonly
used method is forEach(), which iterates over the stream's elements and
prints their values. Because the output of the forEach() method is not
a stream, it is also referred to as a terminal function (operation).
Create Streams from Arrays
We can also create streams from arrays. Consider the following piece of code.
public class StreamFromArrays {
public static void main(String[] args) {
Integer<> squareSeries = {1, 4, 9, 16, 25};
Arrays.stream(squareSeries)
.forEach(s -> System.out.println(Thread.currentThread().getName() + ": " + s));
}
}
The static method stream(int[] array) from the utility class
Arrays creates a stream of primitives.
By using the parallel() method, we can also parallelise the stream that
is created from arrays.
public class StreamFromArrays {
public static void main(String[] args) {
Integer<> squareSeries = {1, 4, 9, 16, 25};
Arrays.stream(squareSeries).parallel()
.forEach(i -> System.out.println(Thread.currentThread().getName() + ": " + i));
}
}
In the previous example, we saw how to pass an array object to the
stream() method. The stream() method also allows us to pass the
start and end indexes along with the array object. This will create a subarray
of the original array object.
public class StreamFromArrays {
public static void main(String[] args) {
Integer<> squareSeries = {1, 4, 9, 16, 25, 36, 49};
Arrays.stream(squareSeries, 3, squareSeries.length - 1)
.forEach(i -> System.out.println(Thread.currentThread().getName() + ": " + i));
}
}
Create Finite Stream
You can also create streams without a source, such as collections or arrays,
using Stream.generate() and Stream.builder(). The code
below shows how the builder() method can be used to generate a finite
stream of Integer values.
public class StreamFinite {
public static void main(String[] args) {
Stream<Integer> integerStream = Stream.<Integer>builder()
.add(1).add(2).add(9).add(16).add(25)
.build();
integerStream.takeWhile(s -> s > 0)
.forEach(s -> System.out.println(s + " "));
}
}
Because the builder() method is generic, it is required to specify the
type that will be used to create the stream's elements.
The generate() method can be used to create a stream. This method takes
an instance of Supplier<T> as a parameter.
public class StreamFinite {
public static void main(String[] args) {
Stream<BigDecimal> randomDecimal = Stream.generate(
new Supplier<BigDecimal>() {
@Override
public BigDecimal get() {
Random random = new Random();
return BigDecimal.valueOf(random.nextDouble() * 100)
.setScale(2, RoundingMode.UP);
}
}
).limit(10);
randomDecimal.forEach(s -> System.out.println(s + " "));
}
}
To keep the stream finite, we used the limit() method to control the
number of elements generated.
Product class
Here's a simple Product class with a few properties. We will be using this
class throughout the tutorial.
Product List
We've written a static method that returns a list of Products. This product
list is used later in the tutorial.
Intermediate Operation: map() And Terminal Operation: collect()
The map() method is used to perform intermediate operations.
Intermediate operations process elements are in the stream and send the
result as a stream to the next function in the pipeline. It takes an argument
of type Function<T, R>, essentially a reference to a function,
and
then applies this function to each element of the stream and returns a
stream.
A stream of objects can be transformed in a variety of ways using the
map() method. For instance,
we can extract a particular attribute or field from each item in a stream
or change an object stream from one type to another.
👉 Consider this example. Here, we will find the square of each element in the
stream and print the resulting stream.
import java.util.Arrays;
import java.util.List;
import java.util.stream.Collectors;
import java.util.stream.Stream;
public class StreamMapExample {
public static void main(String args[]) {
List<Integer> result = Arrays.asList(2, 3, 4, 5, 6, 7, 8)
.stream()
.map(n-> n*n)
.collect(Collectors.toList());
System.out.println(result);
}
}
The map() method in the preceding program takes the stream values one
by one and executes the logic that we specified. The collector then gathers
the resultant elements in a list and returns it to the caller method.
Convert lowercase letters to uppercase
Let's look at another example of the Java stream's map() method, this
time converting all lowercase letters to uppercase letters. The code for this
is shown below.
import java.util.stream.Stream;
import java.util.function.Function;
public class StreamMapExample {
public static void main(String args[]) {
Function<String, String> uppercaseFunction = String::toUpperCase;
Stream.of("Seoul", "Istanbul", "Chicago", "Barcelona", "Sydney")
.map(uppercaseFunction)
.forEach(System.out::println);
}
}
Number of characters in each word
In this example, we want to know how many characters are in each word. We can
accomplish this task by calling the map() method. This method will take
each word and return its length.
public class StreamMapCollect {
public static void main(String[] args) {
List<String> countries = List.of("Barbados", "Japan", "New Zealand");
List<Integer> nameLength = countries
.stream()
.map(String::length)
.collect(Collectors.toList());
System.out.println(nameLength);
}
}
Fetch values from a list of objects
In the next example, we will extract a list of brand names from the Product
list.
import java.util.List;
import java.util.function.Function;
import java.util.stream.Collectors;
import java.util.stream.Stream;
public class StreamMapCollect {
public static void main(String[] args) {
List<String> brandList = ProductList.getProductList()
.stream().map(Product::getBrand)
.collect(Collectors.toList());
System.out.println(brandList);
Function<String, String> uppercaseFunction = String::toUpperCase;
brandList = ProductList.getProductList()
.stream().map(obj -> uppercaseFunction.apply(obj.getBrand()))
.collect(Collectors.toList());
System.out.println(brandList);
}
}
The map() method returns a String stream. Using the
collect() method, all of these elements are added to a
List<String>. This method adds elements to a Collection instance as they are
processed.
Fetch distinct values from a list of objects
We can find distinct brand names in this manner:
import java.util.List;
import java.util.stream.Collectors;
import java.util.stream.Stream;
public class StreamMapCollect {
public static void main(String[] args) {
List<String> distinctBrands = ProductList.getProductList()
.stream().map(Product::getBrand).distinct()
.collect(Collectors.toList());
System.out.println(distinctBrands);
}
}
The distinct() method takes a stream and returns a stream containing
all of the stream's different elements.
👉 We can find distinctive brand names in another way.
public class StreamMapCollect {
public static void main(String[] args) {
Set<String> brandSet = ProductList.getProductList()
.stream()
.map(Product::getBrand).collect(Collectors.toSet());
System.out.println(brandSet);
}
}
So we're collecting the processed elements in a
Set<String> with the collect() method. We will get
a collection of distinct elements because Set does not permit duplicate
elements.
Create a list of Object using Stream
👉 We can extract elements from the stream and create another list of objects
using the map() method.
public class StreamMapCollect {
public static void main(String[] args) {
Set<ProductDetails> productSet = ProductList.getProductList()
.stream().map(obj -> {
return new ProductDetails(obj.getTitle(),
obj.getDescription(),
obj.getPrice());
}).collect(Collectors.toSet());
System.out.println(productSet);
}
}
class ProductDetails {
private String name;
private String description;
private double price;
public ProductDetails() {
}
public ProductDetails(String name, String description, double price) {
this.name = name;
this.description = description;
this.price = price;
}
public String getName() {
return name;
}
public String getDescription() {
return description;
}
public double getPrice() {
return price;
}
public void setName(String name) {
this.name = name;
}
public void setDescription(String description) {
this.description = description;
}
public void setPrice(double price) {
this.price = price;
}
@Override
public String toString() {
return "ProductDetails [name=" + name + ", description=" + description + ", price=" + price + "]";
}
}
We made a ProductDetails class with a few properties. We extract a few
properties from ProductList and use them to create
ProductDetails objects with the map(..) method.
Intermediate Operation: filter()
The Stream API's filter() method chooses elements from a
collection based on a condition. It takes a predicate (a functional
interface that returns a boolean) as input and returns a new stream containing
only elements that satisfy the condition. Here’s the syntax:
Stream<T> filter(Predicate<? super T> predicate)
Let's look at a basic example. Assume we have a list of integers and want to
exclude the even numbers.
public class StreamFilterCollect {
public static void main(String[] args) {
List<Integer> numbers = Arrays.asList(19, 24, 32, 47, 52, 67, 71, 85);
// Predicate to filter out even numbers
Predicate<Integer> predicate = (x) -> x % 2 != 0;
List<Integer> oddNumbers = numbers
.stream()
.filter(predicate)
.collect(Collectors.toList());
System.out.println("Odd Numbers: " + oddNumbers);
}
}
Filtering Objects
You can also filter a list of objects based on specific attributes. Our data
source will be ProductList.
public class StreamFilterExample {
public static void main(String[] args) {
List<Product> products = ProductList.getProductList()
.stream()
.filter(e -> e.getRating() > 4.5)
.collect(Collectors.toList());
System.out.println("Products :: " + products);
}
}
We've filtered the products based on their ratings. In this case,
predicate is mentioned in the filter() method.
We can use the && or || operators in the
filter() method to apply multiple conditions.
public class StreamFilterExample {
public static void main(String[] args) {
List<Product> products = ProductList.getProductList()
.stream()
.filter(e -> e.getRating() > 4.5
&& e.getDiscountPercentage() > 10)
.collect(Collectors.toList());
System.out.println("Products :: " + products);
}
}
Products are filtered by their ratings and discounts.
Combining filter() with map()
To extract Product titles based on ratings and discounts, we
first filter the stream and then use map() to extract the title.
public class StreamFilterExample {
public static void main(String[] args) {
List<String> productTitles = ProductList.getProductList()
.stream()
.filter(e -> e.getRating() > 4.5
&& e.getDiscountPercentage() > 10)
// .map(e -> e.getTitle())
.map(Product::getTitle)
.collect(Collectors.toList());
System.out.println("Product titles :: " + productTitles);
}
}
Intermediate Operation: findAny()
Use findAny() to find any element in the current stream. It
yields an Optional<T> instance. Here's a basic example.
public class StreamFindAnyDemo {
public static void main(String[] args) {
Optional<Integer> anyNumber = Arrays.asList(8, 57, 19, 14, 51, 33, 38)
.stream()
.filter(x -> x % 2 != 0)
.findAny();
anyNumber.ifPresent(n -> System.out.println(n));
}
}
👉 We can also select a product from our Product List.
public class StreamExample {
public static void main(String[] args) {
Optional<Product> productAny = ProductList.getProductList()
.stream()
.findAny();
productAny.ifPresent(c -> System.out.println(c));
}
}
👉 We can use findAny() alongside other intermediate operations. In the
example below, it is combined with filter().
public class StreamExample {
public static void main(String[] args) {
Optional<Product> productFilterAny = ProductList.getProductList()
.stream()
.filter(e -> e.getPrice() > 890)
.findAny();
productFilterAny.ifPresent(c -> System.out.println(c));
}
}
Intermediate Operation: anyMatch()
We want to know if a specific element is in the stream, we can use
anyMatch(). It accepts Predicate<T> arguments and returns a true or
false boolean.
Here's a simple code snippet for checking whether or not a specific word is in
the list.
public class StreamAnyMatchExample {
public static void main(String[] args) {
Boolean isAnimalPresent = List.of("Fern", "Zebra", "Peace Lily", "Snake Plant")
.stream()
.anyMatch(e -> e.equals("Zebra"));
if (isAnimalPresent) {
System.out.println("List contains animal name.");
} else {
System.out.println("We have a list of plant names.");
}
}
}
👉 The code snippet below checks to see if there is a Product in the stream
with a price greater than a specified amount.
public class StreamAnyMatchExample {
public static void main(String[] args) {
// Is there any Product of price greater than 1000/- in the Product list.
Predicate<Product> predicate = (p) -> p.getPrice() > 1000;
Boolean anyMatch = ProductList.getProductList()
.stream()
.anyMatch(predicate);
System.out.println("Product of price 1000/- is preset? " + anyMatch);
}
}
👉 We can combine the anyMatch() and filter() methods. We've got
a list of brand names. We want to know if these brand names are present in our
existing stream.
public class StreamAnyMatchExample {
public static void main(String[] args) {
List<String> brandNames = List.of(new String[]{"Apple", "Boat"});
List<Product> products = ProductList.getProductStream()
.filter(e -> brandNames.stream().anyMatch(a -> a.equals(e.getBrand())))
.collect(Collectors.toList());
System.out.println("Products :: " + products);
}
}
Conclusion
The Stream API is an effective tool for modern Java development. Understanding
its key concepts and operations will help you write more concise, readable,
and efficient code. Whether you're working with collections, arrays, or custom
data sources, the Stream API gives you a flexible and declarative way to
process data.
Happy coding!!! 😊