A Guide to Intermediate and Terminal Operations with Java Streams

Ömer Naci Soydemir
3 min readApr 9, 2023

--

A Java Stream is a sequence of elements that can be processed in a functional style using operations such as filter, map, and reduce. A stream is different from a collection in that it does not store elements itself, but rather provides a way to process elements from a source, such as a collection or an I/O channel.

Stream operations

What are the benefits of using streams in Java?

Streams provide a concise and expressive way to process collections of data. They support functional programming paradigms such as immutability and lazy evaluation, and can make code more readable and maintainable. Streams also provide built-in support for parallel processing which can improve performance for certain types of operations.

You can create a stream from a collection in Java by calling the stream() method on the collection. For example;

List<Payment> payments = new ArrayList<>();
Stream<Payment> paymentStream = payments.stream();

What is a stream pipeline? What are the components of a stream pipeline?

A stream pipeline consists of a source, zero or more intermediate operations and a terminal operation. The source provides the elements that are processed by the pipeline.

Intermediate operations in Java streams are operations that transform a stream into another stream, while terminal operations are operations that produce a result or a side-effect.

intermediate-operations
List<Payment> payments = Arrays.asList(
new Payment("1", "John", new BigDecimal("100.50"), Payment.PaymentStatus.APPROVED, LocalDate.now()),
new Payment("2", "Mary", new BigDecimal("200.75"), Payment.PaymentStatus.PENDING, LocalDate.now().minusDays(60)),
new Payment("3", "John", new BigDecimal("50.00"), Payment.PaymentStatus.APPROVED, LocalDate.now()),
new Payment("4", "Bob", new BigDecimal("150.25"), Payment.PaymentStatus.REJECTED, LocalDate.now()),
new Payment("5", "Mary", new BigDecimal("75.00"), Payment.PaymentStatus.PENDING, LocalDate.now().minusDays(15))
);

// filter() to get only the approved payments:
List<Payment> approvedPayments = payments.stream()
.filter(p -> p.getStatus() == Payment.PaymentStatus.APPROVED)
.peek(System.out::println)
.toList();

// filter() to get only the approved payments made by John:
List<Payment> approvedPaymentsByJohn = payments.stream()
.filter(p -> p.getStatus() == Payment.PaymentStatus.APPROVED && p.getCustomerName().equals("John"))
.toList();

// map() to transform the Payment objects into their amounts:
List<BigDecimal> paymentAmounts = payments.stream()
.map(Payment::getAmount)
.toList();

// sorted() to sort the payments by their amount in descending order:
List<Payment> sortedPayments = payments.stream()
.sorted(Comparator.comparing(Payment::getAmount))
.toList();

// distinct() to get the unique customer names:
List<String> uniqueCustomerNames = payments.stream()
.map(Payment::getCustomerName)
.distinct()
.toList();

// limit() to get the first two payments made by Mary:
List<Payment> firstTwoPaymentsByMary = payments.stream()
.filter(p -> p.getCustomerName().equals("Mary"))
.limit(2)
.toList();
terminal-operations
List<Payment> payments = Arrays.asList(
new Payment("1", "John", new BigDecimal("100.50"), Payment.PaymentStatus.APPROVED, LocalDate.now()),
new Payment("2", "Mary", new BigDecimal("200.75"), Payment.PaymentStatus.PENDING, LocalDate.now().minusDays(60)),
new Payment("3", "John", new BigDecimal("50.00"), Payment.PaymentStatus.APPROVED, LocalDate.now()),
new Payment("4", "Bob", new BigDecimal("150.25"), Payment.PaymentStatus.REJECTED, LocalDate.now()),
new Payment("5", "Mary", new BigDecimal("75.00"), Payment.PaymentStatus.PENDING, LocalDate.now().minusDays(15))
);

// count(): This terminal operation returns the number of elements in the stream.
long count = payments.stream().count();
System.out.println("Number of payments: " + count);
// Output: Number of payments: 5

// forEach(): This terminal operation performs an action for each element in the stream.
payments.forEach(p -> System.out.println(p.getCustomerName() + ": " + p.getAmount()));
/*
Output:
John: 100.50
Mary: 200.75
John: 50.00
Bob: 150.25
Mary: 75.00
*/

// max(): This terminal operation returns the maximum element of the stream, according to a given comparator.
Optional<Payment> maxPayment = payments.stream().max(Comparator.comparing(Payment::getAmount));
maxPayment.ifPresent(payment -> System.out.println("Maximum payment amount: " + payment.getAmount()));
// Output: Maximum payment amount: 200.75

// min(): This terminal operation returns the minimum element of the stream, according to a given comparator.
Optional<Payment> minPayment = payments.stream().min(Comparator.comparing(Payment::getAmount));
minPayment.ifPresent(payment -> System.out.println("Minimum payment amount: " + payment.getAmount()));
// Output: Minimum payment amount: 50.00

// reduce(): This terminal operation reduces the elements of the stream to a single value, according to a given binary operator.
Optional<BigDecimal> totalAmount = payments.stream()
.map(Payment::getAmount)
.reduce(BigDecimal::add);
totalAmount.ifPresent(bigDecimal -> System.out.println("Total amount: " + bigDecimal));
// Output: Total amount: 576.50

Github repo : https://github.com/omernaci/medium-samples/tree/master/stream-exercise

--

--