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 code which is more readable. 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 be called in this code?

1
2
3
4
5
Stream.of("ananas", "oranges", "apple", "pear", "banana")
      .map(String::toUpperCase)        // 1. Process
      .sorted((l, r) -l.compareTo(r))  // 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:

1
2
3
4
5
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.

1
2
3
4
5
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.

Conclusion

Things to consider when using streams:

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