Java Streams: Order Matters

 · 3 min
Photo by Nathan Dumlao on Unsplash

Streams are one of my most used features of Java 8, especially in combination with lambdas. They make code more concise, and we all love shorter, more readable code. But a lot can go wrong along the way.

The beauty of Java streams is the ability to concat multiple operations to a “pipeline”. It can replace most for-loops in your code, especially the ones that are just pushing data from one data structure to another (e.g., from List<YourObject> to Map<String, YourObject>).

But you have to remember one thing: every step in the stream will be called until an item is rejected.


Filter first, map later

How many operations will this code call?

java
Stream.of("ananas", "oranges", "apple", "pear", "banana")
      .map(String::toUpperCase)        // 1. Process
      .sorted()                        // 2. Sort
      .filter(s -> s.startsWith("A"))  // 3. Reject
      .forEach(System.out::println);   // 4. Do stuff

This code will run map 5 times, sorted 8 times, filter 5 times, and forEach 2 times. We got 20 operations to output 2 values.

That’s ridiculous!

Well, we can do better than that:

java
Stream.of("ananas", "oranges", "apple", "pear", "banana")  
      .filter(s -s.startsWith("a"))    // 1. Filter first  
      .map(String::toUpperCase)        // 2. Process  
      .sorted((l, r) -l.compareTo(r))  // 3. Sort  
      .forEach(System.out::println);   // 4. Do stuff

By filtering first, we are going to restrict the map/sorted operations to a minimum: filter 5 times, map 2 times, sort 1 time, and forEach 2 times, which saves us 10 operations in total. In this example, it might seem like not a big deal. But usually, we deal with more than just 5 items. And the map operation might be expensive to do, so doing less is always better.

Prepare first, then filter, and do stuff later

What if you can’t filter first? Well, then, it often helps to prepare your data in the first step just a little so you can filter in the next step and only do the heavy processing on the remaining items.

java
Stream.of(obj1, obj2, obj3, obj4, obj5)  
      .map(this::prepareSoWeCanFilter)   // 1. Make filterable  
      .filter(o -> o.isValid())          // 2. Filter  
      .map(this::doSomeHeavyProcessing)  // 3. Heavy processing  
      .forEach(System.out::println);     // 4. Do stuff

This way, you can reduce the heavy operations to be only applied to the objects that actually need it.


A Functional Approach to Java Cover Image
Interested in using functional concepts and techniques in your Java code?
Check out my book!
Available in English, Polish, and soon, Chinese.

Conclusion

Things to consider when using streams:

  • If necessary, prepare data to be more easily filterable.
  • Filter first, if possible. Fewer items equal fewer operations along the way.
  • If not possible to filter first, try to use cheaper operations first, filter, then more expensive ones.