DEV Community

Daniel Coturel
Daniel Coturel

Posted on

Streams in Java

Hi everyone.

This year I worked on learning more Javascript, but I continue to work with Java for my desktop works.

This week I needed to develop a small functionality extending the ERP we work with, to export a text file for compliance with a government agency.

In order to do this, and trying to use a more "functional" approach, I explored the Stream object.

This is a new functionality in the language. A Stream is a collection, that allows data processing declaratively. For instance, I needed to do a sum over an array of items. In the old days of ArrayList, this was my code:

BigDecimal total = BigDecimal.ZERO;
Iterator iterator = listOfValues.iterator();
while (iterator.hasNext()) {
    TotalSsn currentElement = iterator.next();
    if (currentElement.isChildrenOf(account)) {
    total.add(currentElement.getImporte());
    }
}

Enter fullscreen mode Exit fullscreen mode

With a little help of the Stream object, the code looks like this:

BigDecimal total = BigDecimal.ZERO;

Optional<BigDecimal> totalOptional = listOfValues.stream().filter(y -> y.isChildrenOf(account)).map(TotalSsn::getImporte).reduce((a, b) -> (a.add(b)));
if (totalOptional.isPresent()) {
    total = total.get();
}

Enter fullscreen mode Exit fullscreen mode

In the second approach, I'm using:

stream()

This method returns a stream. It's a method of the List interface, so getting a Stream to work with is pretty straightforward on legacy code.

filter()

This method takes a functional interface as a parameter and returns a new Stream with the results of applying this function to the original stream's elements.

map()

This method allows you to generate a new Stream whith another generic type. In this case I'm converting my TotalSsn to BigDecimal.

reduce()

Allows the calculation, in this case, of a sum of the Stream values.

This whole API, I think, is worthy to be explored.

Saludos,

Top comments (8)

Collapse
 
alainvanhout profile image
Alain Van Hout • Edited

When using streams, the use of 'isPresent' is considered an anti-pattern, because it's just a null-check in disguise. Instead, either use 'ifPresent', or 'orElse', or 'orElseThrow'. Otherwise you're using the tools but missing the point.

Collapse
 
elcotu profile image
Daniel Coturel

Hi Alain. Thanks for your feedback. Could you please elaborate wich point I'm missing so I learn a little more?

Collapse
 
alainvanhout profile image
Alain Van Hout • Edited

Hi Daniel,

I'd be glad to elaborate :).

The main point of stream and Optional is that you do not have to actively make an effort to recognize and deal with potential null values. It also promotes immutability of your variables, which makes it easier to reason about things and which reduces the chance of bugs (since you don't need to keep track of where a variable's value was changed last).

BigDecimal total = listOfValues.stream()
   .filter(y -> y.isChildrenOf(account))
   .map(TotalSsn::getImporte)
   .reduce((a, b) -> (a.add(b)))
   .orElse(BigDecimal.ZERO);

As you can see there, we don't actually deal with a null value, we just specify the desired default, and as a side-benefit, total does not get reassigned to a different value.

Since reduce also has a starting value, you could also rewrite that to

BigDecimal total = listOfValues.stream()
   .filter(y -> y.isChildrenOf(account))
   .map(TotalSsn::getImporte)
   .reduce(BigDecimal.ZERO, BigDecimal::add);;

Notably, all this is made somewhat more complicated that most use-cases, due to the use of BigDecimal (which of course might be essential in the context). If a regular Double would suffice, the above could be rewritten to

double total = listOfValues.stream()
   .filter(y -> y.isChildrenOf(account))
   .map(TotalSsn::getImporte)
   .sum();

As to orElse vs ifPresent, when you're dealing with default or fallback values, always try to rely on orElse (that's what it's for). The use of ifPresent isn't for when you want to do an assignment, but when you want to only perform an action if there is an actual value resulting from your stream.

myList.stream()
   .filter(MyClass::hasCertainTrait)
   .map(MyClass::getCertainChildField)
   .filter(MyChildClass::isRelevantForMe)
   .findFirst()
   .ifPresent(childField -> {
      // do something useful with childField, like switching the lights on :)
   });

Notice here, that I don't need to focus on the case where there isn't a value (i.e. I don't have to ward of a NullPointerException), because the approach side-steps that issue altogether.

Thread Thread
 
mx profile image
Maxime Moreau

Thanks for this explanation!
I have a question on filter/map ect (I'm a JS dev). Do they loop through the whole Collection again and again ? (i.e, the first filter do, then the map do, then the second filter do...).

Thread Thread
 
elcotu profile image
Daniel Coturel

Alain, thanks for your answer. It's really clear and enriching.

Thread Thread
 
alainvanhout profile image
Alain Van Hout

Hi Maxime,

Note that the following is specific to JS. It may work similar or quite different in other languages.

To answer your question, try the following code

[1,2,3,4,5]
  .map(x => {
    console.log("A", x);
    return x;
  })
  .map(y => console.log("B", y))

This outputs

A 1
A 2
A 3
A 4
A 5
B 1
B 2
B 3
B 4
B 5

That shows that in JS, a regular array map operation iterates over the entire array before passing it on to the next operation.

Collapse
 
prasadchillara_55 profile image
Prasad Chillara • Edited

Hi Dan,

ifPresent method of reduces/terminal Operators can also be used here.

listOfValues
.stream()
.filter(y -> y.isChildrenOf(account))
.map(TotalSsn::getImporte)
.reduce((a, b) -> (a.add(b)))
.ifPresent(totaloptional -> total = totaloptional);

Thanks
Prasad Chillara

Thread Thread
 
elcotu profile image
Daniel Coturel

Thanks