Jul 30, 2020 · by Andreea Avram

Functional Programming in Java: Stream API

A four-part series dedicated to a deeper dive on the various functional programming concepts

In the previous three articles in this series about functional programming, we talked about lambdas, functional interfaces, and optional class. Let’s now see how these three concepts can be combined and used in a new API. 

This new API is the Stream API which is the most important addition to Java 8 and is part of the java.util.stream package. The main type in java.util.stream package is Stream interface, which is the stream of object references. Even though Stream is a generic interface, the java.util.stream package has interfaces defined for the primitive types int, long and double and the main reason is the boxing and the unboxing which means an extra effort that takes time. These interfaces are: IntStream, LongStream, and DoubleStream respectively. 

What is a stream?
“A stream is a sequence of elements and we can perform sequential operations when we obtain a stream using the stream() method. Streams provide pipelining capability—you can filter, map, and search data. The most common source of stream is collection objects such as sets, maps, and lists, but the stream API can be used independent of the collections.”[1]

Stream Interface

As previously mentioned the Stream interface is the most important interface provided in the java.util.stream package. The classes IntStream, LongStream, and DoubleStream are Stream specializations for int, long, and double. A stream is not a data structure instead it is created starting from collections, arrays or I/O channels. Also, another thing to note is that the stream on which certain operations are applied does not change but a new stream is created. In the stream pipeline can be identified three parts: source, intermediate operations, and terminal operations. A stream can have several intermediate operations chained but only one terminal operation.

1. Source:

  • Create a stream from a collection or array

//stream from an array
String[] array = {"a", "b", "c", "d", "d"};
Stream stream = Arrays.stream(array);

//stream from a collection
ArrayList names = new ArrayList<>();
Stream namesStream = names.stream();

  • Using one of these two methods:

I. Stream<T> of(T… values) 

Stream stream = Stream.of("Steve", "Tim", "Leon");

II. Stream<T>  generate(Supplier<T> s)

Stream<Double> streamDouble = Stream.generate(() -> Math.random() * 10).limit(10);

2. Intermediate operations:

These operations can be chained because an intermediate operation always returns a stream and of course we should mention that those operations are optional.

  • Stream<R> map(Function<? super T,? extends R> mapper): The map operation takes a Function as argument and applies that function for every value in the input stream. The function passed as parameter to the map operation always returns only a single value that is sent to the output stream. The most common use case for this operation is to convert a stream from one type to another.

The Stream API provides different versions of the map operation that use specializations of the generic Function interface.

  • mapToInt(ToIntFunction<? super T> mapper): This operation takes a stream of objects and converts it to an IntStream
  • mapToLong(ToLongFunction<? super T> mapper): Takes a stream of objects and converts it to a LongStream
  • mapToDouble(ToDoubleFunction<? super T> mapper): Takes a stream of objects and converts it to a DoubleStream

String[] array = {"a", "b", "c", "d", "d"};
//example 1
Stream stream = Arrays.stream(array);
Stream convertedStream = stream.map(letter -> letter.toUpperCase());

//example 2
Function<String, String> function = letter -> letter.toUpperCase();
Stream convertedStream2=stream.map(function)

  • Stream<R>  flatMap(Function<? super T,? extends Stream<? extends R>> mapper): This operation is actually a map operation combined with a flattening operation. That means the elements are first converted from one type to another and after that the output stream is flattened.

String[][] array = new String[][] { { "a","b" } ,{"d"} , {"e" }};
Stream<String[]> stream=Arrays.stream(array);
Stream flatMapStream=stream.flatMap(s-> Arrays.stream(s));
flatMapStream.forEach(s-> System.out.println(s));






flatMapToDouble, flatMapToInt, flatMapToLong are specializations of the flatMap operation.

  • Stream<T> filter(Predicate<? super T> predicate): Takes a Predicate as argument and returns a new stream with the elements that match the given predicate. This operation is an intermediate operation that enables us to call another stream operation (e.g. forEach) on the result.

Stream stream = Stream.of("Steve", "Tim", "Leon");
.forEach(name -> System.out.println(name));

Output: Tim

  • Stream<T> peek(Consumer<? super T> action): Executes the passed lambda expression on the elements but returns the same stream; is meant primarily for debugging purpose
  • Stream<T> limit(long maxSize): Limits the number or truncate elements to be processed in the stream
  • Stream<T>  sorted(): Returns a stream sorted according to natural order.
  • Stream<T>  sorted(Comparator<? super T> comparator): This is an overloaded version of sorted() operation which will return a stream sorted according to the logic provided by comparator object.

//sort in natural order
Stream stream = Stream.of("Steve", "Tim", "Leon");
stream.forEach(s -> System.out.println(s));

Output: Leon Steve Tim

//sort using a comparator
Stream stream = Stream.of("Steve", "Tim", "Leon");
stream.forEach(s -> System.out.println(s));

Output: Tim Steve Leon

3. Terminal operations: Produce a result

  • collect(): Will convert the stream into some other container such as a list, a map, a set
  • toArray(): Convert array to array list and convert collection to array
  • min()
  • max()
  • count()
  • anyMatch(): Returns true if any element in the stream matches a provided predicate predicate
  • allMatch(): Returns true if and only if all elements match a provided predicate, otherwise return false
  • findAny(): Returns an Optional instance
  • findFirst(): Returns an Optional instance

The intermediate Stream operation returns another Stream which means we can further call other methods of Stream class to compose a pipeline. Once a terminal method is called, we cannot call any other method of Stream or reuse the Stream.

Let’s see an example to understand how these operations work together.

ArrayList names = new ArrayList<>();
//intermediate operation
.map(name -> name.toUpperCase())
//intermediate operation
.filter(name -> name.length() > 3)
//terminal operation
.forEach(name -> System.out.println(name));

What does the above code do?

Starting from a list is created a stream and for each element is applied the uppercase method and is checked if the name length is greater than 3, if the name length is greater than 3 then the element is added to the output stream. In the end, the elements from the output stream are printed to the console.

In the end, the elements from the output stream are printed to the console.







This new style that was introduced in Java 8 is a big win for this programming language and the main advantages are:  

  • The removal of boilerplate code so the code is now cleaner, more concise and easier to reuse
  • Higher-order functions that allow to: send functions to other functions, create functions into other functions, and return functions from other functions.
  • Reducing the gap between business logic and code
  • Lazy evaluations: lambda expression as a method argument, the compiler will evaluate the expression when it’s called in the method


Works Cited:

1. Oracle Certified Professional Java SE 8 Programmer Exam

2. https://docs.oracle.com/javase/8/docs/api/?java/util/stream/Stream.html

3. https://www.deadcoderising.com/why-you-should-embrace-lambdas-in-java-8/


Share This Article

Post A Comment