Professional Stream Manipulation in Java with Uncommon Examples

Daniel Angel
5 min readMar 29, 2023

--

Streams in Java are a powerful tool for manipulating and processing collections in an efficient and concise way. With Streams, we can write code that is more readable and easier to maintain, applying Clean Code principles such as immutability and avoiding side effects.

In this tutorial, we’ll cover the basics of using Streams in Java, as well as some more advanced concepts and uncommon examples.

Basic Stream Operations

filter

filter(Predicate<T> predicate) is an intermediate operation that takes a Predicate as input and returns a Stream consisting of the elements that match the predicate.

Stream<Integer> evenNumbersStream = numbersStream.filter(n -> n % 2 == 0);

map

map(Function<T, R> mapper) is an intermediate operation that takes a Function as input and returns a Stream consisting of the results of applying the function to each element.

Stream<String> upperCaseNamesStream = namesStream.map(name -> name.toUpperCase());

flatMap

flatMap(Function<T, Stream<R>> mapper) is an intermediate operation that takes a Function that returns a Stream as input and returns a Stream consisting of the flattened results of applying the function to each element.

List<List<String>> listOfLists = Arrays.asList(Arrays.asList("a", "b"), Arrays.asList("c", "d"));
Stream<String> flattenedStream = listOfLists.stream().flatMap(List::stream);

sorted

sorted() is an intermediate operation that returns a Stream consisting of the elements sorted in natural order.

Stream<Integer> sortedNumbersStream = numbersStream.sorted();

distinct

distinct() is an intermediate operation that returns a Stream consisting of the distinct elements of the original Stream.

Stream<Integer> distinctNumbersStream = numbersStream.distinct();

limit

limit(long maxSize) is an intermediate operation that returns a Stream consisting of the first maxSize elements of the original Stream.

Stream<Integer> limitedNumbersStream = numbersStream.limit(10);

skip

skip(long n) is an intermediate operation that returns a Stream consisting of the elements of the original Stream after skipping the first n elements.

Stream<Integer> skippedNumbersStream = numbersStream.skip(5);

Terminal Stream Operations

forEach

forEach(Consumer<T> action) is a terminal operation that takes a Consumer as input and performs the specified action on each element of the Stream.

namesStream.forEach(System.out::println);

count

count() is a terminal operation that returns the number of elements in the Stream as a long.

long count = numbersStream.count();

anyMatch

anyMatch(Predicate<T> predicate) is a terminal operation that returns true if any element in the Stream matches the given predicate, otherwise it returns false.

boolean hasEvenNumber = numbersStream.anyMatch(n -> n % 2 == 0);

allMatch ✔️

allMatch(Predicate<T> predicate) is a terminal operation that returns true if all elements in the Stream match the given predicate, otherwise it returns false.

boolean areAllEvenNumbers = numbersStream.allMatch(n -> n % 2 == 0);

noneMatch ✖️

noneMatch(Predicate<T> predicate) is a terminal operation that returns true if no element in the Stream matches the given predicate, otherwise it returns false.

boolean hasNoEvenNumbers = numbersStream.noneMatch(n -> n % 2 == 0);

findFirst 👊

findFirst() is a terminal operation that returns an Optional object containing the first element of the Stream, or an empty Optional if the Stream is empty.

Optional<Integer> firstNumber = numbersStream.findFirst();

reduce ♻️

reduce(BinaryOperator<T> accumulator) is a terminal operation that performs a reduction on the elements of the Stream using the specified accumulator function.

Optional<Integer> sum = numbersStream.reduce((a, b) -> a + b);

collect 📥

collect(Collector<T, A, R> collector) is a terminal operation that collects the elements of the Stream into a Collection using the specified Collector.

List<Integer> numbersList = numbersStream.collect(Collectors.toList());

Advanced Stream Operations

Concatenating Streams

We can concatenate two Streams using the Stream.concat(Stream a, Stream b) method.

Stream<Integer> concatenatedStream = Stream.concat(numbersStream1, numbersStream2);

Generating Infinite Streams 💁

We can generate infinite Streams using methods such as Stream.iterate(T seed, UnaryOperator<T> f) or Stream.generate(Supplier<T> s).

Stream<Integer> infiniteStream = Stream.iterate(0, n -> n + 1);

Partitioning a List ➗

Let’s say we have a list of employees and we want to partition them into two lists, one for full-time employees and one for part-time employees. We can use the Collectors.partitioningBy() method to achieve this:

Map<Boolean, List<Employee>> partitionedEmployees = employees.stream()
.collect(Collectors.partitioningBy(Employee::isFullTime));

This will create a Map where the key true corresponds to the list of full-time employees and the key false corresponds to the list of part-time employees, amazing 😮 😵.

Grouping by Multiple Fields

Let’s say we have a list of transactions and we want to group them by both the transaction type and the month. We can use the Collectors.groupingBy() method with a custom Function to achieve this:

Map<TransactionType, Map<Month, List<Transaction>>> transactionsByTypeAndMonth = transactions.stream()
.collect(Collectors.groupingBy(Transaction::getType,
Collectors.groupingBy(transaction -> transaction.getDate().getMonth())));

Filtering and Collecting to a Custom Collection

Let’s say we have a list of products and we want to filter out the ones that are out of stock, and collect the remaining products into a custom ProductCatalog object. We can use the filter() and collect() methods to achieve this:

ProductCatalog catalog = products.stream()
.filter(Product::isInStock)
.collect(ProductCatalog::new, ProductCatalog::addProduct, ProductCatalog::combine);

Here, ProductCatalog is a custom collection that we want to collect the products into. We use the collect() method with three arguments: the first argument is a Supplier that creates a new ProductCatalog object, the second argument is a BiConsumer that adds each product to the ProductCatalog, and the third argument is a BiConsumer that combines two ProductCatalog objects into a single ProductCatalog

Converting a Stream to a Map with Grouping and Counting

Let’s say we have a list of Person objects and we want to create a map that groups them by age and gender, and counts the number of people in each group. We can use the Collectors.groupingBy() method with Collectors.counting() to achieve this:

class Person {
private final String name;
private final int age;
private final String gender;

Person(String name, int age, String gender) {
this.name = name;
this.age = age;
this.gender = gender;
}

// getters and toString method omitted for brevity
}

List<Person> people = Arrays.asList(
new Person("Alice", 25, "female"),
new Person("Bob", 30, "male"),
new Person("Charlie", 25, "male"),
new Person("Dave", 35, "male"),
new Person("Eve", 25, "female"),
new Person("Frank", 30, "male")
);

Map<String, Map<Integer, Long>> groupedAndCounted = people.stream()
.collect(Collectors.groupingBy(Person::getGender,
Collectors.groupingBy(Person::getAge, Collectors.counting())));

System.out.println(groupedAndCounted);
// prints {female={25=2}, male={35=1, 30=2, 25=1}}

Here, we first group the people by gender using Collectors.groupingBy(), and then group each subgroup by age and count the number of people in each age group using Collectors.groupingBy() and Collectors.counting(). This results in a Map<String, Map<Integer, Long>> where each key is a gender, each value is a submap with age as the key and count as the value.

Conclusion

In summary, Streams in Java are a powerful tool for manipulating and processing collections in an efficient and concise way. By using Streams, we can write more readable and maintainable code, and apply Clean Code principles such as immutability and avoiding side effects.

We also covered some uncommon examples of Stream manipulation, such as concatenating Streams, generating infinite Streams, and using parallel Streams.

By using Streams in a professional way, we can improve our efficiency in Java application development and write cleaner, more maintainable code.

--

--

Daniel Angel
Daniel Angel

Written by Daniel Angel

Backend developer | Java, Spring boot, Cloud| Laravel, Php

No responses yet