Exploring Java Streams
salmanetamo
Posted on May 23, 2022
In order to understand this tutorial, you should be familiar with the basics of writing code in Java. If you want to follow along and try some of the code, you may use your favorite IDE; I personally use IntelliJ.
What are streams?
A Stream in Java is essentially a sequence of elements from a given data source (an array or Collection). The Stream does not store the data but rather acts a pipeline that allows us to perform some operations on that data.
Those such operations are of two types: intermediate and terminal. Intermediate operations always return a Stream thus allowing us to keep chaining multiple intermediate operations; the most common ones being map() and filter(). Terminal operations, however, return either void or some other return type and, as the name suggests, terminate the pipelining. The most common terminal operation is forEach().
How do we create streams?
There are a multiple ways to create a Stream.
The most common of these is probably using the stream() method from the Collection Interface for collections and from the Arrays class for arrays.
// Stream from a list
List<Integer> numList = List.of(3, 56, 7, 6);
Stream<Integer> numListStream = numList.stream();
// Stream from an array
Integer[] numArray = new Integer[]{3, 56, 7, 6};
Stream<Integer> numArrayStream = Arrays.stream(numArray);
You can also create a Stream using Stream.of() which takes in the elements in the stream as a parameter.
Stream<Integer> numbersStream = Stream.of(3, 56, 7, 6);
Another way to create Streams is by using Stream.generate() or Stream.iterate(). Note that these two methods create infinite streams so always make sure to limit the number of elements to be part of the streams through the limit() method. These methods both take a Supplier as a parameter in order to generate the elements. The Stream.iterate() method additionally takes an initial element as parameter, also used to generate subsequent elements.
Stream<String> stringStream = Stream.generate(() -> "Streams are cool").limit(10);
// Print "Streams are cool" 10 times
stringStream.forEach(System.out::println);
Stream<Integer> multiplesOfThreeStream = Stream.iterate(3, num -> num + 3).limit(10);
// Print the first 10 multiples of 3
multiplesOfThreeStream.forEach(System.out::println);
For the primitive types int, long, and double, you can also use the following three special interfaces respectively to create Streams for those types: IntStream, LongStream, and DoubleStream. These come with some handy methods that enables the creation of ordered streams with values within a given range namely the range() and rangeClosed() methods.
IntStream intStream = IntStream.of(3, 56, 7, 6);
LongStream longStream = LongStream.of(3L, 56L, 7L);
DoubleStream doubleStream = DoubleStream.of(3.2, 5.6, 7.0);
IntStream intStreamRange = IntStream.range(1, 11);
// Print 1 2 3 4 5 6 7 8 9 10
intStreamRange.forEach(System.out::println);
IntStream intStreamRangeClosed = IntStream.rangeClosed(1, 10);
// Print 1 2 3 4 5 6 7 8 9 10
intStreamRangeClosed.forEach(System.out::println);
What’s the big deal about streams anyway?
One of the biggest advantage of Streams is that it allows us to move from imperative programming to declarative programming. In Java, this is achieved through the usage of Functional style programming, that leverages Lambdas leading to powerful and more concise code.
Let’s illustrate this with an example. Imagine you have a list of strings on which you wish to do the following:
- Sort the list alphabetically
- Remove duplicates
- Capitalize all the letters
- Keep strings ending with letter “x”
- Print to the console
The list:
List<String> words = new ArrayList<>(List.of("track", "enfix", "spell", "complex","impulse", "comedy", "magnetic", "complex", "witch", "plead", "spell", "thesis", "thesis"));
In an imperative approach here’s how you might go about it:
Collections.sort(words);
Set<String> wordsWithoutDuplicates = new LinkedHashSet<>(words);
for(String word : wordsWithoutDuplicates) {
if (word.endsWith("x")) {
String wordCapitalized = word.toUpperCase();
System.out.println(wordCapitalized);
}
}
With a declarative approach, here’s a potential solution. Notice how much cleaner the code is.
words.stream()
.sorted()
.distinct()
.filter(word -> word.endsWith("x"))
.map(String::toUpperCase)
.forEach(System.out::println);
This was a very brief overview of Streams, that were introduced in Java 8, which provide us with powerful means to manipulate data.
Posted on May 23, 2022
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.