Java Stream API

Develop code to extract data from an object using peek() and map() methods including primitive versions of the map() method

peek()

Stream<T> peek(Consumer<? super T> action)

Peek is an intermediate operation that returns a stream consisting of the elements of the original stream, and performing the provided action on each element.

According to the API, This method exists mainly to support debugging, where you want to see the elements as they flow past a certain point in a pipeline:

Stream.of("the", "good", "bad", "ugly")
   .filter(e -> e.length() > 3)
   .peek(e -> System.out.println("Filtered value: " + e))
   .map(String::toUpperCase)
   .peek(e -> System.out.println("Mapped value: " + e))
   .collect(Collectors.toList());

map()

<R> Stream<R> map(Function<? super T,? extends R> mapper)

Map is an intermediate operation that returns a stream consisting of the results of applying the given function to the elements of this stream. Since we're talking about a Function, it's possible to convert the items in the stream to other objects. In other words, for each item you create a new object based on that item.

For example, to convert all strings in the stream to uppercase:

Stream.of("a", "b", "c", "d").map(s -> s.toUpperCase()).forEach(s -> System.out.print(s));

Just like a Function, map() include primitive versions:

IntStream mapToInt(ToIntFunction<? super T> mapper)
LongStream mapToLong(ToLongFunction<? super T> mapper)
DoubleStream mapToDouble(ToDoubleFunction<? super T> mapper)

These methods can be used when you want a stream of these primitive types. For example, consider a stream of elements of this type:

class Employee {
  private int id;
  private String name;
  public int getId() {
    return id;
  }
  public String getName() {
    return name;
  }
  public void setId(int id) {
    this.id = id;
  }
  public void setName(String name) {
    this.name = name;
  }
}

We can extract the employee ids directly to a stream of int in this way:

List<Integer> employeeIds = employees.stream().mapToInt(e -> e.getId()).collect(Collectors.toList());

This works because getInt() returns an int, the type expected by mapToInt().

 

Search for data by using search methods of the Stream classes including findFirst, findAny, anyMatch, allMatch, noneMatch

We have two types of methods in the Stream API to search for data:

  • findXXX methods. Take no arguments and return an Optional object with the result, or an empty Optional if nothing is found.
  • XXXMatch methods. Take a Predicate and return a boolean if an element in the stream returns true by applying the Predicate.

The methods are:

Optional<T> findAny() This is a short-circuiting terminal operation that returns an Optional representing some element of the stream. This doesn't guarantee to return the first element of a sorted stream.

Optional<T> findFirst() This is a short-circuiting terminal operation that returns the first element of the stream. If the stream has no order, then any element may be returned.

boolean anyMatch(Predicate<? super T> predicate) This is a short-circuiting terminal operation that returns whether any elements of this stream match the provided predicate. If the stream is empty, then false is returned and the predicate is not evaluated.

boolean allMatch(Predicate<? super T> predicate) This is a short-circuiting terminal operation that returns whether all elements of this stream match the provided predicate. If the stream is empty, then true is returned and the predicate is not evaluated.

boolean noneMatch(Predicate<? super T> predicate) This is a short-circuiting terminal operation that returns whether no elements of this stream match the provided predicate. If the stream is empty, then true is returned and the predicate is not evaluated.

A short-circuiting terminal operation basically means that there's no need to process the entire stream to return a result. As soon as an element that fits the predicate is found or that a result can be inferred, the result is returned.

Here's an example:

Optional<Integer> first = Stream.of(1, 10, 5, 3, 13, 20).filter(i -> i % 2 == 0).findFirst(); //returns 2
Optional<Integer> any = Stream.of(1, 10, 5, 3, 13, 20).filter(i -> i % 2 == 0).findAny(); //can return 10 or 20
boolean any2 = Stream.of(1, 10, 5, 3, 13, 20).anyMatch(i -> i % 3 == 0); //returns true
boolean all = Stream.of(1, 10, 5, 3, 13, 20).allMatch(i -> i % 2 == 0); //returns false
boolean none = Stream.of(1, 10, 5, 3, 13, 20).noneMatch(i -> i % 6 == 0); //returns true

 

Develop code that uses the Optional class

The java.util.Optional<T> class acts as a container which may or may not contain a non-null value.

The main methods of the class are:

  • isPresent() that returns true if it contains a non-null value, false otherwise.
  • get() that returns the non-null value if it contains a non-null value, and throws a NoSuchElementException otherwise. To avoid the exception, before getting the value you must check if the Optional object contains a non-null value.
  • ifPresent(Consumer<? super T> action) takes a Consumer to perform some operation on the value contained in the Optional. If the Optional is empty or null, it does not perform anything.

OptionalInt, OptionalLong, and OptionalDouble deal with optional primitive values.

  • getAsInt() method from OptionalInt class returns int value.
  • getAsLong() method from OptionalLong class return long value.
  • getAsDouble() method from OptionalDouble class return double value.

Some stream operations (like map(), max(), min(), etc.) return optional values with nothing inside if the stream is empty. For example:

OptionalInt min = Stream.of(10, 20, 30, 40).filter(n -> n > 50).min();
if (min.isPresent()) {
  System.out.println(min.getAsInt());
} else {
  System.out.println("No value");
}

Or if we don't need to print the "No value" message:

OptionalInt min = Stream.of(10, 20, 30, 40).filter(n -> n > 50).min();
min.isPresent(n -> System.out.println(n));

Besides get(), we have three other methods to obtain values from an Optional:

  • T orElse(T defaultValue) Returns the value contained in the Optional. If empty, it returns the specified defaultValue.
  • T orElseGet(Supplier<? extends T> defaultSupplier) Returns the value contained in the Optional. If empty, it returns the value returned from the specified defaultSupplier.
  • <X extends Throwable> T orElseThrow(Supplier<? extends X> exceptionSupplier) throws X extends Throwable Returns the value contained in the Optional. If empty, it throws the exception returned from the specified exceptionSupplier. For example:
    Stream.of(10, 20, 30, 40).filter(n -> n > 50).min().orElse(0);
    Stream.of(10, 20, 30, 40).filter(n -> n > 50).min().orElseGet(() -> logAndReturnDefault());
    Stream.of(10, 20, 30, 40).filter(n -> n > 50).min().orElseThrow(Exception::new);
    

If we are not using streams and we need to create an Optional, there are three methods we can use:

  • <T> Optional<T> empty() Returns an empty Optional.
  • <T> Optional<T> of(T value) Returns an Optional containing the specified value If the value is null, it throws a NullPointerException.
  • <T> Optional<T> ofNullable(T value) Returns an Optional containing the specified value if the value is non-null. If the specified value is null, it returns an empty Optional.

For example:

Optional<String> empty  = Optional.empty();
Optional<String> string = Optional.of("Hello");
Optional<String> empty2  = Optional.ofNullable(null);

 

Develop code that uses Stream data methods and calculation methods

Data methods

Optional<T> max(Comparator<? super T> comparator) Terminal operation that returns the maximum element of this stream according to the provided Comparator wrapped in an Optional object, or an empty Optional if the stream is empty.

Optional<T> min(Comparator<? super T> comparator) Terminal operation that returns the minimum element of this stream according to the provided Comparator wrapped in an Optional object, or an empty Optional if the stream is empty.

long count() Terminal operation that returns the count of elements in this stream.

Comparator is a functional interface, so you can create a comparator in the form of a lambda expression. For example:

Comparator<String> byLength = (s1, s2) -> Integer.compare( s1.length(), s2.length());
Optional<String> max = Stream.of("hello","good bye", "black", "white", "good", "bad")
        .max(byLength); //returns "good bye"
Optional<String> min = Stream.of("hello","good bye", "black", "white", "good", "bad")
        .min(byLength); //returns "bad"
long count = Stream.of("hello","good bye", "black", "white", "good", "bad")
        .count(); //returns 6

Calculation methods

Calculation methods only work with the primitive versions of Stream: IntStream, LongStream, and DoubleStream. For example, for IntStream the methods are:

int sum() Terminal operation that returns the sum of elements in the stream.

OptionalInt min() Terminal operation that returns an OptionalInt describing the minimum element of the stream, or an empty optional if this stream is empty.

OptionalInt max() Terminal operation that returns an OptionalInt describing the minimum element of the stream, or an empty optional if this stream is empty.

OptionalDouble average() Terminal operation that returns an OptionalDouble describing the arithmetic mean of elements of the stream, or an empty optional if this stream is empty.

Except for average() that always returns an OptionalDouble, the other methods return a long and OptionalLong for LongStream, and a double and OptionalDouble for DoubleStream.

Here's an example:

OptionalInt max = IntStream.of(1, 34, 667, 3, 32, 23).max(); // max() returns 667

OptionalInt min = IntStream.of(1, 34, 667, 3, 32, 23).min(); // min() returns 1

OptionalDouble average = IntStream.of(1, 34, 667, 3, 32, 23).average(); // returns 126.66

int sum = IntStream.of(1, 34, 667, 3, 32, 23).sum(); // returns 760

 

Sort a collection using Stream API

To sort a collection using the method sorted() from the Stream API, you need a Comparator (there's a version of sorted that takes no arguments that returns a stream with its elements sorted according to natural order).

Comparator is a functional interface, so you can create a comparator in the form of a lambda expression. For example:

Comparator<String> byLength = (s1, s2) -> Integer.compare( s1.length(), s2.length());

Stream.of("hello","good bye", "black", "white", "good", "bad")
          .sorted(byLength)
          .forEach(s -> System.out.println(s));

The output:

bad
good
hello
black
white
good bye

If you want to compare using multiple criteria, in Java 8 there's a new method to make this easily, Comparator.thenComparing():

Comparator<String> byLength = (s1, s2) -> Integer.compare( s1.length(), s2.length());
Comparator<String> byLetters = (s1, s2) -> s1.compareTo(s2);

Stream.of("hello","good bye", "black", "white", "good", "bad")
          .sorted(byLength.thenComparing(byLetters))
          .forEach(s -> System.out.println(s));

The output:

bad
good
black
hello
white
good bye

 

Save results to a collection using the collect method and group/partition data using the Collectors class

Streams have the following method:

collect(Collector<? super T,A,R> collector)

This method performs a mutable reduction operation on the elements of this stream using a Collector.

A mutable reduction operation accumulates (or collects) input elements into a mutable result container, such as a Collection or StringBuilder, as it processes the elements in the stream.

A collect operation requires three functions:

  • A supplier function to construct new instances of the result container
  • An accumulator function to incorporate an input element into a result container
  • A combining function to merge the contents of one result container into another.

In fact, streams also have the following method that takes the above functions to create our own collect operations:

collect(Supplier<R> supplier, BiConsumer<R,? super T> accumulator, BiConsumer<R,R> combiner)

For example, traditionally, to collect the elements of a list into another list, we could do:

List<String> word = new ArrayList<>();;
List<String> letters = new ArrayList<>();
letters.add("H");
letters.add("e");
letters.add("l");
letters.add("l");
letters.add("o");
for (String s : letters) {
   word.add(s.toUpperCase());
}

Now with the collect method:

List<String> letters = new ArrayList<>();
letters.add("H");
letters.add("e");
letters.add("l");
letters.add("l");
letters.add("o");
List<String> word = letters.stream().collect(() -> new ArrayList<>(),
                                    (c, s) -> c.add(s.toUpperCase()),
                                    (c1, c2) -> c1.addAll(c2));

However, there's a class, Collectors, which implements many useful reduction operations. So using Collectors, the code becomes simpler:

List<String> letters = new ArrayList<>();
letters.add("H");
letters.add("e");
letters.add("l");
letters.add("l");
letters.add("o");
List<String> word = letters.stream().map(s -> s.toUpperCase()).collect(Collectors.toList());

Other implementations of Collectors are:

// Accumulate into a TreeSet
Set<String> set = letters.stream()
                          .map(s -> s.toUpperCase())
                          .collect(Collectors.toCollection(TreeSet::new));

// Convert elements to strings and concatenate them, separated by commas
String joined = letters.stream()
                      .map(s -> s.toUpperCase())
                      .collect(Collectors.joining(", "));

// Compute sum of all letters
int total = letters.stream()
                      .collect(Collectors.summingInt(s.length())));

// Group by starting letter
Map<String, List<String>> grouped = letters.stream()
                                    .collect(Collectors.groupingBy(s.substring(0,1)));

// Partition letters into uppercase and lowercase
Map<Boolean, List<String>> upperLower = letters.stream()
                                    .collect(Collectors.partitioningBy(s -> Character.isUpperCase(s.codePointAt(0))));

The last method, partitionBy, takes a predicate that returns Map<Boolean, List <String>> where the key is a boolean and the results are based on the behavior of the predicate. So its output is:

{false=["e","l","l","o"], true=[H]}

 

Use of merge() and flatMap() methods of the Stream API

<R> Stream<R> flatMap(Function<? super T,? extends Stream<? extends R>> mapper)

This method returns a stream consisting of the results of replacing each element of the original stream with the content of the stream produced by the provided function (if the function returns null, an empty stream is used, instead).

This method is commonly used in one‐to‐many relationships to flatten all the elements into a new stream.

For example, consider the following classes:

class Student {
  private int id;
  private String name;
  public int getId() {
    return id;
  }
  public String getName() {
    return name;
  }
  public void setId(int id) {
    this.id = id;
  }
  public void setName(String name) {
    this.name = name;
  }
}
class Course {
  private List<Student> students;
  private String name;
  public List<Student> getStudents() {
    return students;
  }
  public String getName() {
    return name;
  }
  public void setStudents(List<Student> students) {
    this.students = students;
  }
  public void setName(String name) {
    this.name = name;
  }
}

Assuming there's a list of courses and each course has a list of students that take that course, if we want to aggregate the names of all students taking a course into one stream, the following code will do it:

List<String> students = courses.stream()
                .flatMap(course -> course.getStudents().stream()) //Get the students of each course
                .map(student -> student.getName()) //Now that we have a stream with all the students, we extract their name
                .collect(Collectors.toList());

There are other examples in the javadoc of the method.

 

Content