Java8 | Stream API

Hasan Kadir Demircan
8 min readApr 13, 2021

What we need to know about Stream API

  • It is another top subject that comes with Java 8.
  • It added java.util.stream with Java8.
  • A stream in Java is a sequence of data.
  • A stream pipeline is the operations that run on a stream to produce results.
  • The pipeline has three sections.
    Source: Where the stream comes from(raw data)(from a collection or an array or using Stream methods such as of() and generate())
    Intermediate operations: Transforms the stream into another one.(such as map(), filter(), distinct(), and sorted() methods in the Stream Interface)
    Terminal operation: Actually produces a result.(such as sum(), collect(), forEach() and reduce() methods in the Stream Interface)

Stream Hierarchy

Stream Source

  • We created Stream Source as follows,
Stream<String> stream1 = Stream.empty();
Stream<Integer> stream2 = Stream.of(1);
Stream<Integer> stream3 = Stream.of(1, 2, 3, 4, 5, 6, 7);
  • We can convert the list to stream
List<String> list = Arrays.asList("a", "b", "c");
Stream<String> fromList = list.stream();
  • We can create random numbers and iterate and we should use limit() method.
  • If we don’t use it, it creates infinite numbers.
Stream<Double> randoms = Stream.generate(Math::random).limit(5);

Stream<Integer> oddNumbers = Stream.iterate(1, n -> n + 2).limit(5);
  • And Stream Pipeline flows as follows
Arrays.stream(Object.class.getMethods()) // source
.map(Method::getName) // intermediate op
.distinct() // intermediate op
.forEach(System.out::println); // terminal operation
  • We can use range and rangeClosed methods
  • range doesn’t involve the endExclusive (so, 1…9)
  • rangeClosed involves the endExclusive
IntStream.range(1, 10).forEach(System.out::println);

IntStream.rangeClosed(1, 5)
.map(e -> e * 2)
.forEach(System.out::println);
  • We can create a stream of numbers by giving the starting and ending values.
IntStream.iterate(10, i -> i + 5)
.limit(6)
.forEach(System.out::println);
  • We should careful the return type While we create a stream
// public static IntStream stream(int[] array) {
IntStream intStream = Arrays.stream(new int[] { 1, 2, 3, 4, 5 });

// public static <T> Stream<T> stream(T[] array) {
Stream<Integer> integerStream = Arrays.stream(new Integer[] { 1, 2, 3, 4, 5 });
  • We can use Pattern.compile with regex
//public static Pattern compile(String regex)
// public Stream<String> splitAsStream(final CharSequence input)
Pattern.compile(" ")
.splitAsStream("java 8 streams")
.forEach(System.out::println);

Intermediate Operations

peek()

Stream<T> peek(Consumer<? super T> action);
  • Returns the same elements in the stream, but also executes the passed consume lambda expression on the elements.
  • The peek() method is meant primarily for debugging purposes. it helps us understand how the elements are transformed in the pipeline. do not use it in production code.
  • If we don’t add some methods such as .count(), collect(), we can’t see any output. Because, according to the program, the result can be perceived as unchanged.
Stream.of("one", "two", "three", "four")
.filter(e -> e.length() > 3)
.peek(e -> System.out.println("Filtered value: " + e))
.map(String::toUpperCase)
.peek(e -> System.out.println("Mapped value: " + e));
//this code doesn't give any outputs,
//we should add this -> .collect(Collectors.toList()); <-

Another example;

Stream.of(1, 2, 3, 4, 5)
.peek(i -> System.out.printf("%d ", i))
.map(i -> i * i)
.peek(i -> System.out.printf("%d ", i)).count();
//if we didn't add the .count(), we wouldn't' see any outputs

filter()

Stream<T> filter(Predicate<? super T> predicate);
  • The filter() method in the Stream interface is used for removing the elements that do not match the given condition.
  • Here is a simple example that uses Stream’s filter() method for removing odd integers
IntStream.rangeClosed(0,10)
.filter(i -> (i % 2 == 0))
.map(i -> i*3)
.forEach(System.out::println);
  • Here is a simple example that uses Stream’s filter() method for removing empty Strings
Stream.of("Hasan", "Kadir", "", "Java")
.filter(name -> !name.isEmpty())
.forEach(System.out::println);

distinct()

Stream<T> distinct();
  • The distinct() method returns a stream with duplicate values remove
Stream.of("Java", "Java", "Java8", "JAva")
.distinct()
.forEach(System.out::println);

skip() and limit()

  • The skip(n) method is an intermediate operation that discards the first n elements of a stream. The n parameter can’t be negative, and if it’s higher than the size of the stream, skip() returns an empty stream.
  • The limit(n) method is another intermediate operation that returns a stream not longer than the requested size. As before, the n parameter can’t be negative.
Stream<Integer> s = Stream.iterate(10, n -> n + 2);

s.skip(3).limit(2).forEach(System.out::println); //16, 18

map()

  • The map() method on streams is for transforming data. Don’t confuse it with the Map interface, which maps keys to values
Stream<String> s = Stream.of("hasan", "kadir", "demircan");
s.map(String::length).forEach(System.out::print); // 558
  • We can use it to find the only data in the list we have.
List<String> numbers = Arrays.asList("1", "2", "3", "4", "5", "6");
List<Integer> even = numbers.stream()
.map(Integer::valueOf)
.filter(number -> number % 2 == 0)
.collect(Collectors.toList());
  • Another map example
List<Match> matchList = Arrays.asList(
new Match("FootballMatch", 11),
new Match("BasketballMatch", 7),
new Match("Soccer", 7),
new Match("Tennis", 2)
);

Optional<String> matchName = matchList.stream()
.map(Match::getName)
.filter(name -> name.endsWith("Match"))
.sorted()
.findFirst();
System.out.println("Result: " + matchName.orElse("match no found"));

flatMap()

  • Java 8 Stream flatMap() method is used to flatten a Stream of collections to a stream of objects. The objects are combined from all the collections in the original Stream.
  • The flatMap() operation has the effect of applying a one-to-many transformation to the elements of the stream and then flattening the resulting elements into a new stream.
  • Stream.flatMap() helps in converting Stream<Collection<T>> to Stream<T>.
<R> Stream<R> flatMap(Function<? super T, ? extends Stream<? extends R>> mapper);

flatMap() vs Map()

  • The Stream’s map() method produces or returns a single result value, which is sent to the Output stream
  • The Stream’s flatMap() method produces or returns a Stream of elements
  • The map() method is only used for modification while the flatMap() method is used for both flattening and modification
flatMap() = Flattening + map()

For Example;

When we use Map()

String[] string = "you never know what you have until you clean your room".split(" ");
Arrays.stream(string)
.map(word -> word.split(""))
.distinct()
.forEach(System.out::println);
//This code prints gibberish like this:Ljava.lang.String;@5f184fc6[Ljava.lang.String;@3feba861[Ljava.lang.String;@5b480cf9[
  • Why? Because the word.split() returns a String[] and distinct() removes duplicate references. Since the elements in the stream are of type String[], the forEach() prints calls the default toString() implementation that prints something that is not human-readable.
  • One way to solve this problem is to use Arrays.stream() again on word.split(“”), and convert the resulting streams into individual entries (i.e., “flatten” the streams) as in: flatMap(word -> Arrays.stream(word.split(“”))).

When we use flatMap()

String[] string = "you never know what you have until you clean your room".split(" ");
Arrays.stream(string)
.flatMap(word -> Arrays.stream(word.split("")))
.distinct()
.forEach(System.out::println);
//This code prints gibberish like this:
y
o
u
n
(etc..)

Another example with flatMap()

  • We imagine that we have two arrays within ArrayList,
  • So, how can we process this case(sorting, multiply, etc..)?
List<List<Integer>> intsOfInts = Arrays.asList(Arrays.asList(1, 3, 5), Arrays.asList(2, 4));

intsOfInts.stream()
.flatMap(Collection::stream)
.sorted()
.map(i -> i * i)
.forEach(System.out::println);
  • When we investigate step by step;
flatMap Example Schema

sorted()

  • Sorts the elements of a Stream using natural ordering; the element class must implement the Comparable interface.
 Stream<T> sorted()
Stream<T> sorted(Comparator<? super T> comparator)

Example for sorted() method;

Stream<String> s = Stream.of("brown-", "bear-", "bee-");
s.sorted().forEach(System.out::print);
// bear-bee-brown-

Note: We can get ‘java.lang.OutOfMemoryError: Java heap space’ when we use sorted()

For example;

  • This example tries to generate infinite String and tries sorted. Then get OutOfMemoryError
Stream.generate(() -> "Elsa")
.filter(n -> n.length() == 4)
.sorted()
.limit(2)
.forEach(System.out::println);
  • We can change the code like that,
Stream.generate(() -> "Elsa")
.filter(n -> n.length() == 4)
.limit(2)
.sorted()
.forEach(System.out::println);

Terminal Operation

count()

  • The count() method determines the number of elements in a finite stream.
  • We can only call the terminal method once on a stream.
Stream<String> s1 = Stream.of("k1", "k2", "k3");
long countElement = s1.count();
System.out.println(countElement);

min() and max()

  • We can find min or max values that the String or Array.
  • It is checked to alphabetical order
Stream<String> stream1 = Stream.of("monkey", "ape", "bonobo");
Stream<String> stream2 = Stream.of("monkey", "ape", "bonobo");

Comparator<String> comp = Comparator.comparingInt(String::length);

Optional<String> min = stream1.min(comp);
Optional<String> max = stream2.max(comp);

System.out.println(min);
System.out.println(max);

Another example,

String[] string = "you never know what you have until you clean your room".split(" ");
String[] string2 = "you never know what you have until you clean your room".split(" ");
String[] string3 = "you never know what you have until you clean your room".split(" ");

System.out.println(Arrays.stream(string).min(String::compareTo).get());
System.out.println(Arrays.stream(string2).max(String::compareTo).get());

Comparator<String> lengthCompare = Comparator.comparingInt(String::length);
System.out.println(Arrays.stream(string3).min(lengthCompare).get());

findAny() and findFirst()

  • findAny(), returns one of the elements from the stream; if there is no element present in the stream, it returns an empty Optional<T> object.
  • findFirst(), returns the first element from the stream; if there is no element present in the stream, it returns an empty Optional<T> object.
  • findFirst() return the first elements of the stream but findAny() is free to select any element in the stream. (especially when we use parallel streaming)
Stream<String> s = Stream.of("monkey", "gorilla", "bonobo");
Stream<String> infinite = Stream.generate(() -> "chimp");

s.findAny().ifPresent(System.out::println); // monkey

infinite.findAny().ifPresent(System.out::println); // chimp

Another example;

// DoubleStream filter(DoublePredicate predicate);
OptionalDouble temperature = DoubleStream.of(-10.1, -5.4, 6.0, -3.4, 8.9, 2.2)
.filter(temp -> temp > 0)
.findFirst();

// DoubleStream filter(DoublePredicate predicate);
OptionalDouble temperature2 = DoubleStream.of(-10.1, -5.4, 6.0, -3.4, 8.9, 2.2)
.filter(temp -> temp > 0)
.findAny();

System.out.println(temperature);
System.out.println(temperature2);

anyMatch(), allMatch(), noneMatch()

  • anyMatch(), returns true if there is any elements in the stream that matches the given predicate. Returns false if the stream is empty or if there are no matching elements.
  • allMatch(), returns true only if all elements in the stream matches the given predicate. Returns true if the stream is empty without evaluating the predicate.
  • noneMatch(), returns true only if none of the elements in the stream matches the given predicate. Returns true if the stream is empty without evaluating the predicate!
  • These may or may not terminate for infinite streams. It depends on the data
  • Like the find methods, they are not reductions because they do not
  • Necessarily look at all of the elements.
List<String> list = Arrays.asList("monkey", "2", "chimp");

Stream<String> infinite = Stream.generate(() -> "chimp");
Stream<String> infinite2 = Stream.generate(() -> "chimp");

Predicate<String> pred = x -> Character.isLetter(x.charAt(0));

System.out.println(list.stream().anyMatch(pred)); // true
System.out.println(list.stream().allMatch(pred)); // false
System.out.println(list.stream().noneMatch(pred));// false

// infinitive
System.out.println(infinite.anyMatch(pred)); // true
// System.out.println(infinite.allMatch(pred)); // true -> endless loop

System.out.println(infinite2.noneMatch(pred)); // false

forEach()

  • The forEach() method on the stream is the terminal operation in the pipeline.
  • Java provides a new method forEach() to iterate the elements.
void forEach(Consumer<? super T> action);

For example;

Stream<String> s = Stream.of("Monkey", "Gorilla", "Bonobo");
s.forEach(System.out::print); // MonkeyGorillaBonobo
// Consumer
// void accept(T t);

List<String> list = new ArrayList<>();
s.forEach((str) ->
{
String sUpper = str.toUpperCase();
list.add(sUpper);
});

reduce()

  • The reduce() method combines a stream into a single object. As you can tell from the name, it is a reduction.
 T reduce(T identity, BinaryOperator<T> accumulator);
Optional<T> reduce(BinaryOperator<T> accumulator);

For Example;

Stream<Integer> stream = Stream.of(3, 5, 6);
Stream<Integer> stream2 = Stream.of(3, 5, 6);

// T reduce(T identity, BinaryOperator<T> accumulator);
System.out.println(stream.reduce(10, (a, b) -> a * b));
//Sout: 900

// Optional<T> reduce(BinaryOperator<T> accumulator);
System.out.println(stream2.reduce((a, b) -> a * b));
//Sout: 900Optional[90]

For Example;

  • Instead of writing for forEach for concating process , we can use reduce() and the words will be concat.
Stream<String> stream = Stream.of("w1", "o2", "l3", "f4");
String word = stream.reduce("X", String::concat);
System.out.println(word); // Xw1o2l3f4

or more complicated example;

Stream<String> stream2 = Stream.of("w", "o", "l", "f");
String word2 = stream2.reduce("", (s, c) -> s + c +"--");
System.out.println(word2); //w--o--l--f--

collect()

  • The collect() method is a special type of reduction called a mutable reduction.
  • It is more efficient than a regular reduction because we use the same mutable object while accumulating.
<R,A> R collect(Collector<? super T, A,R> collector)

For example;

Collectors.toList()

String frenchCounting = "un:deux:trois:quatre";
List<String> gmailList = Pattern.compile(":")
.splitAsStream(frenchCounting)
.collect(Collectors.toList());
//[un, deux, trois, quatre]

Collectors.toSet()

String[] roseQuote = "a rose is a rose is a rose".split(" ");
Set<String> words = Arrays.stream(roseQuote).collect(Collectors.toSet());

toMap()

Map<String, Integer> nameLength = Stream.of("Arnold", "Alois", "Schwarzenegger")
.collect(Collectors.toMap(name -> name, String::length));

StringBuilder()

Stream<String> stream = Stream.of("w", "o", "l", "f");
StringBuilder word = stream.collect(StringBuilder::new, StringBuilder::append, StringBuilder::append);

treeSet()

Stream<String> stream = Stream.of("w", "o", "l", "f");
TreeSet<String> set = stream.collect(Collectors.toCollection(TreeSet::new));

I would like to finish my examples,
I hope it helps to understand stream flow :)

Github: Stream

References:
Stream
Oracle Study Books

--

--