A Beginner’ Guide — JAVA 8 Streams

Darryl Leong
5 min readSep 8, 2020

I’ve only recently came across Streams despite first learning Java about 4 years ago (at the start of uni really). Which begs the question — did I really learn Java? If not, what was I actually learning? But that’s really a topic for another day.

When I first discovered Streams, I was pretty intrigued. Upon reading the Javadoc, I found that some of the methods resembled methods I frequently used in JS (filter, map, reduce) — to solve programming puzzles of course, which got me interested in better understanding what Streams were all about. Although I initially had planned to write a full article on the ins-and-outs of streams, a simple google search will be able to reveal many great articles on this topic, explained in ways far better than I can hope to articulate myself. But I guess that’s what happens when you’re about 6 years late to the party.

So I decided instead of all of that, I’ll probably link some of the more interesting articles I found on streams at the bottom of this article for those interested and instead summarize very briefly what I understand about Streams as well as provide an implementation example to most of the methods within this package — really, this is just to help consolidate what I had learnt.

Before we begin, I would like to point out that I will only be covering the basic concepts of Streams and some of it’s implementation. I’ll be leaving out parallel streams.

So.. Should I use Streams?

I spent a bit of time looking for arguments for and against the use of Streams but it seems like most of the proponents and opponents can be summarized in the following 2 points:

Use Streams for:

Better code readability — Take a look at the code below and try to figure out what the for-loop is doing:

// for loop
for(int i = 0; i < list.size(); i++){
if(list.get(i) < 2){
list.remove(i);
} else {
list.set(i, list.get(i) * 2);
}
}
// stream
list2 = list2.stream()
.filter(n -> n > 1)
.map(n -> n * 2)
.collect(Collectors.toList());

Even through a simple example, we are able to see how the usage of streams is able to make code much more readable and concise!

Do not use Streams for:

Better Performance — I’m not going into details on this because an article I read illustrates this point very well. I recommend reading this article if you’re interested as it also discusses another aspect of streams which I won’t be getting into [parallel streams]

But as a general rule of thumb, if you’re looking for better performance, you’re probably better off sticking to for-loops. Of course there are some nuances but I think its accurate to say that performance shouldn’t be the reason you’re picking streams over loops.

Types of Stream Operations

The methods in the Stream API can be classified into 2 types of operations:

  1. Intermediate Operations — As the name suggests, these operations are used before a terminal operation is called. They mainly serve to transform elements in the existing stream before returning a new stream.
  2. Terminal Operations —Last operation called on a stream before returning a non-stream value (void is also a potential return option).

*Computation on the source data will occur only when a terminal operation is invoked on a Stream. The reason being: Streams are designed to be ‘lazy’ — which means source data is only used as needed. That is to say that if a stream does not contain a terminal function, when invoked, the intermediate operations will not be evaluated.

Code Implementations

Intermediate Operations

List<Integer> list = new ArrayList<>(Arrays.asList(5, 4, 3, 2, 1));List<Integer> result = list.stream() // reads List into a stream
.sorted()
.filter(n -> n > 1)
.map(n -> n * 2)
.distinct()
.limit(2)
.collect(Collectors.toList());
// result -> [4, 6]
  1. sorted() — sorts elements according it’s natural order
  2. filter() — removes elements not satisfying the given condition
  3. map() — transforms each element into another (And in the case above: doubling the value of each element)
  4. distinct() — removes duplicate elements in stream (think sets)
  5. limit() — limits the number of elements in stream (pretty self-explanatory, I know)

Terminal Operations

I’ll be dividing these into 3 broad categories: Value, Optionals, void (representing the return values)

Value

allMatch(), anyMatch(), noneMatch() — these terminal operations return a boolean value based on passed the predicate passed in them. As you can imagine:

  1. allMatch()— checks whether all elements in the stream fulfills the specified condition
  2. anyMatch() — checks whether at least 1 element fulfills the condition
  3. nonMatch() — opposite of allMatch
List<Integer> list = new ArrayList<>(Arrays.asList(5,4,3,2,1));// checks if all elements are smaller than 6
boolean isAllMatch = list.stream()
.allMatch(n -> n < 6);
// returns true
// checks if any elements has length > 20
List<String> words= new ArrayList<>(Arrays.asList("hello","world"));
boolean isAnyMatch = words.stream()
.anyMatch(n -> n.length > 20);
// returns false

Optionals

Optionals are containers used to store non-null objects. The reason why some of these methods return Optional Objects is to handle cases in which no result can be found. Don’t worry, it’s easier to understand with examples.

4. findAny() — returns any element in the stream (when order doesn’t matter)

List<Integer> list = new ArrayList<>();
Optional<Integer> findAny = list.stream().findAny();
System.out.println(findAny.isPresent()); // prints false

The above case illustrates a situation in which the return value is potentially null (when the list is empty).

5. findFirst() — returns the first element in the stream (when order matters)

6. min() — returns minimum element in the stream based on the passed Comparator.

List<Integer> list = new ArrayList<>(Arrays.asList(5,4,3,2,1));#1
Optional<Integer> minImpl1 = list.stream()
.min((a,b) -> a.compareTo(b));
#2
Optional<Integer> minImpl2 = list.stream()
.min((a,b) -> {
return a < b ? -1 : (a == b) ? 0 : 1
});

7. max() — opposite of min.

8. reduce() — Reduces the list into a single element.

Note*the above mentioned operations — min(), max() are also examples of reduce functions. However, using reduce() explicitly requires you to define how the list should be reduced.

List<Integer> list = new ArrayList<>(Arrays.asList(5,4,3,2,1));Optional<Integer> reduce = list.stream()
.reduce((val, combVal) -> {
return combVal + val;
});
// computes sum of all values in the list

Void

9. forEach() — As the name suggests, this iterates through each element in the stream. And as you might’ve realized, a similar forEach method is also available in the List API (which was released in Java 8 as well)

List<Integer> list = new ArrayList<>(Arrays.asList(5,4,3,2,1));// stream API implementation
list.stream().forEach(num-> System.out.print(num));
// list API implementation
list.forEach(num -> {
System.out.print(num);
});

The above 2 code snippets have identical functionalities.

Conclusion

All in all, Streams seem pretty cool and are definitely something I’ll look to use on my own. While they might not be the most efficient at times, they really do help improve the readability of code quite substantially (at least in my opinion). If you have any questions or critiques, feel free to leave them in the comments.

Useful Links:

Java Stream Performance

Stream Javadoc

Sign up to discover human stories that deepen your understanding of the world.

Free

Distraction-free reading. No ads.

Organize your knowledge with lists and highlights.

Tell your story. Find your audience.

Membership

Read member-only stories

Support writers you read most

Earn money for your writing

Listen to audio narrations

Read offline with the Medium app

Darryl Leong
Darryl Leong

Written by Darryl Leong

I write sometimes because its fun

No responses yet

Write a response