Professional Stream Manipulation in Java with Uncommon Examples
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.