Exploring Java Streams
Tags: java, lambdas, streams, functional
Published: 2022-05-23 17:44:50

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.
```java
// 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.
```java
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<T>_ 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.
```java
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.
```java
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:
```java
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:
```java
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.
```java
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.