DEV Community

Cover image for Let's talk about STREAMS !!
ABHINAVA GHOSH (he/him)
ABHINAVA GHOSH (he/him)

Posted on • Updated on

Let's talk about STREAMS !!

wave


Hey there!

In this article I will be talking about Streams in Java and how they help in writing neat and compact production codes.

But before diving into Streams let us talk a little about Functional Programming

We as Java developers are constantly grilled about Java's verbose nature and how to even print a single word we have to write about 5-8 lines of code. ๐Ÿ˜ญ

Well this was the scenario before JAVA 8 came into the picture.
With the inclusion of Functional Programming the JAVA code's verbosity can be reduced to around 70%. ๐Ÿ˜ฎ

Coming back to my point lets see what is Functional Programming.


๐ŸŽฏ What is Functional Programming โ“

Functional programming (also called FP) is a way of thinking about software construction by creating pure functions. It avoid concepts of shared state, mutable data observed in Object Oriented Programming.

Functional languages emphasizes on expressions and declarations rather than execution of statements. Therefore, unlike other procedures which depend on a local or global state, value output in FP depends only on the arguments passed to the function.


๐ŸŽฏ Characteristics of Functional Programming:

Functional programming method focuses on results, not the process
Emphasis is on what is to be computed

  • Data is immutable
  • Functional programming Decompose the problem into 'functions
  • It is built on the concept of mathematical functions which uses conditional expressions and recursion to do perform the calculation
  • It does not support iteration like loop statements and conditional statements like If-Else

๐ŸŽฏ The benefits of functional programming:

  • Allows you to avoid confusing problems and errors in the code
  • Easier to test and execute Unit testing and debug FP Code.
  • Parallel processing and concurrency
  • Hot code deployment and fault tolerance
  • Offers better modularity with a shorter code
  • Increased productivity of the developer
  • Supports Nested Functions
  • Functional Constructs like Lazy Map & Lists, etc.
  • Allows effective use of Lambda Calculus.

So we have a basic idea of Functional Programming now!
Lets see how this is implemented in JAVA Streams

Simply put, streams are wrappers around a data source, allowing us to operate with that data source and making bulk processing convenient and fast.

A stream does not store data and, in that sense, is not a data structure. It also never modifies the underlying data source.

This functionality โ€“ java.util.stream โ€“ supports functional-style operations on streams of elements, such as map-reduce transformations on collections.


โœ… Before beginning with Streams lets note down some important concepts :

1.Stream is a flow of data derived from a collection
2.Stream can create a pipeline of function that can be evaluated
3.Data in Stream is Lazy Evaluated
4.Stream can transform data but cannot mutate it.


๐ŸŽฏ [NOTE] - Stream is not a Data Structure.Also the data structure on which you are creating the stream is not changed at all.The Stream just (kinda) makes a copy of it and performs some actions on its own copy.


Well that's all fine but how to create a Stream ? ๐Ÿ˜•

๐ŸŽฏ Stream Creation:

โœ”๏ธ Letโ€™s first obtain a stream from an existing array:

private static Dev[] arrayOfDevs = {
    new Dev(1, "Steve Rogers", 100000.0), 
    new Dev(2, "Anthony Stark", 200000.0), 
    new Dev(3, "Bruce Wayne", 300000.0)
};

Stream.of(arrayOfDevs);

Enter fullscreen mode Exit fullscreen mode

โœ”๏ธ We can also obtain a stream from an existing list:

private static List<Dev> devList = Arrays.asList(arrayOfDevs);
devList.stream();
Enter fullscreen mode Exit fullscreen mode
  1. And we can create a stream from individual objects using Stream.of():
Stream.of(arrayOfDevs[0], arrayOfDevs[1], arrayOfDevs[2]);
Enter fullscreen mode Exit fullscreen mode

โœ”๏ธ Or simply using Stream.builder():

Stream.Builder<Dev> devStreamBuilder = Stream.builder();

devStreamBuilder.accept(arrayOfDev[0]);
devStreamBuilder.accept(arrayOfDev[1]);
devStreamBuilder.accept(arrayOfDev[2]);

Stream<Dev> devStream = devStreamBuilder.build();

Enter fullscreen mode Exit fullscreen mode

Ok so now we know how to create a Stream. ๐Ÿ˜Ž
But whats the use of a Stream if we cant play around with it.


๐ŸŽฏ Stream Operations:

Letโ€™s now see some common usages and operations we can perform on and with the help of the stream support in the language.

โœ”๏ธ forEach:

forEach() is simplest and most common operation; it loops over the stream elements, calling the supplied function on each element.

The method is so common that is has been introduced directly in Iterable, Map etc:

@Test
public void whenIncrementSalaryForEachDev_thenApplyNewSalary() {    
    devList.stream().forEach(d -> d.salaryIncrement(10.0));

    assertThat(devList, contains(
      hasProperty("salary", equalTo(110000.0)),
      hasProperty("salary", equalTo(220000.0)),
      hasProperty("salary", equalTo(330000.0))
    ));
}
Enter fullscreen mode Exit fullscreen mode

This will effectively call the salaryIncrement() on each element in the devList.

forEach() is a terminal operation, which means that, after the operation is performed, the stream pipeline is considered consumed, and can no longer be used.


โœ”๏ธ map

map() produces a new stream after applying a function to each element of the original stream. The new stream could be of different type.

The following example converts the stream of Integers into the stream of Devs:


@Test
public void whenMapIdToDevs_thenGetDevStream() {
    Integer[] devIds = { 1, 2, 3 };

    List<Dev> devs = Stream.of(devIds)
      .map(devRepository::findById)
      .collect(Collectors.toList());

    assertEquals(devs.size(), devIds.length);
}
Enter fullscreen mode Exit fullscreen mode

Here, we obtain an Integer stream of dev ids from an array. Each Integer is passed to the function devRepository::findById() โ€“ which returns the corresponding Dev object; this effectively forms an Dev stream.


โœ”๏ธ collect
We saw how collect() works in the previous example; its one of the common ways to get stuff out of the stream once we are done with all the processing:

@Test
public void whenCollectStreamToList_thenGetList() {
    List<Dev> devs = devList.stream().collect(Collectors.toList());

    assertEquals(devList, devs);
}
Enter fullscreen mode Exit fullscreen mode

collect() performs mutable fold operations (repackaging elements to some data structures and applying some additional logic, concatenating them, etc.) on data elements held in the Stream instance.

The strategy for this operation is provided via the Collector interface implementation. In the example above, we used the toList collector to collect all Stream elements into a List instance.


โœ”๏ธ filter

Next, letโ€™s have a look at filter(); this produces a new stream that contains elements of the original stream that pass a given test (specified by a Predicate).

Letโ€™s have a look at how that works:

@Test
public void whenFilterDevs_thenGetFilteredStream() {
    Integer[] devIds = { 1, 2, 3, 4 };

    List<Dev> devs = Stream.of(devIds)
      .map(devRepository::findById)
      .filter(e -> e != null)
      .filter(e -> e.getSalary() > 200000)
      .collect(Collectors.toList());

    assertEquals(Arrays.asList(arrayOfDevs[2]), devs);
}
Enter fullscreen mode Exit fullscreen mode

In the example above, we first filter out null references for invalid dev ids and then again apply a filter to only keep devs with salaries over a certain threshold.


โœ”๏ธ toArray
We saw how we used collect() to get data out of the stream. If we need to get an array out of the stream, we can simply use toArray():

@Test
public void whenStreamToArray_thenGetArray() {
    Dev[] devs = devList.stream().toArray(Dev[]::new);

    assertThat(devList.toArray(), equalTo(devs));
}
Enter fullscreen mode Exit fullscreen mode

The syntax Dev[]::new creates an empty array of Devs โ€“ which is then filled with elements from the stream.


๐ŸŽฏ [NOTE]

Lazy Evaluation

One of the most important characteristics of streams is that they allow for significant optimizations through lazy evaluations.

Computation on the source data is only performed when the terminal operation is initiated, and source elements are consumed only as needed.

All intermediate operations are lazy, so theyโ€™re not executed until a result of a processing is actually needed.

For example, consider the findFirst() example we saw earlier. How many times is the map() operation performed here? 4 times, since the input array contains 4 elements?

@Test
public void whenFindFirst_thenGetFirstDevInStream() {
    Integer[] devIds = { 1, 2, 3, 4 };

    Dev devs= Stream.of(devIds)
      .map(devRepository::findById)
      .filter(e -> e != null)
      .filter(e -> e.getSalary() > 100000)
      .findFirst()
      .orElse(null);

    assertEquals(devs.getSalary(), new Double(200000));
}
Enter fullscreen mode Exit fullscreen mode

Stream performs the map and two filter operations, one element at a time.

It first performs all the operations on id 1. Since the salary of id 1 is not greater than 100000, the processing moves on to the next element.

Id 2 satisfies both of the filter predicates and hence the stream evaluates the terminal operation findFirst() and returns the result.

No operations are performed on id 3 and 4.

Processing streams lazily allows avoiding examining all the data when thatโ€™s not necessary. This behavior becomes even more important when the input stream is infinite and not just very large.


Java Streams Api is a very large topic and i have tried my best to fit as much as possible in this post.
Please refer to the below links for more in depth knowledge.

๐Ÿ‘‡

Link for reference-1

Oracle docs

Baeldung link

Devoxx Youtube


Some of my other posts:

Concept link
Java Access Modifiers goto Article
Java Generics goto Article
Java Regex goto Article
Java Streams Api goto Article

Please leave a โค๏ธ if you liked this article.
A ๐Ÿฆ„ would be great.
And let me know in the discussions panel if you have any suggestions for me.
And do leave a link to any resource on Java Streams Api that you know of ,that can help others and i will add it to the post.

Have a Good Day! ๐Ÿ˜ƒ

Oldest comments (9)

Collapse
 
jackydev profile image
JacKyDev

the article was good but forEach is not really part of streams. it is an implementation of the functional or iterable interface and can be used for any list even without a stream.

List.stream().forEach would be less performing than List.forEach if used directly. If you use Filter and Map operations before forEach its a benefit to use it on Streams.

Collapse
 
the_unconventional_coder profile image
ABHINAVA GHOSH (he/him)

agreed!
Actually we use .forEach() so frequently with Streams that i thought of mentioning it, under Streams.

Collapse
 
thorstenhirsch profile image
Thorsten Hirsch

Hi Abhinava, I'm glad someone's writing about Java and FP. Doesn't seem to be a popular combination on dev.to ...or anywhere. ๐Ÿคฃ

I've only started to work again with Java recently and couldn't find much besides streams that I can use for FP in Java. So my programs are still very OO-ish, and I can only make use of streams in some methods. But what about enforcing immutability? What about currying? And monads? I know there are libraries like vavr.io that implement these things for Java, but I haven't touched them, yet. Do you know such libraries? I'm afraid there might be a heavy performance impact when using such libraries.

Collapse
 
the_unconventional_coder profile image
ABHINAVA GHOSH (he/him)

Using FP in monads for example is not very common , and as far as i have experienced Streams are the only widespread use of FP in JAVA currently.And its not bcoz of performance issues but bcoz there are better alternatives to it.

Collapse
 
aleksandarskrbic profile image
Aleksandar Skrbic

Most you can get from Vavr is some mix of OOP and FP and it works great. It's probably the best FPish library for Java. Pure FP is not possible in Java, like in Scala with ZIO, Cats-Effect, and Monix.
I wrote a blog about FP in Java and Vavr, if you are interested here is the link: aleksandarskrbic.github.io/a-taste...

Collapse
 
pentacular profile image
Info Comment hidden by post author - thread only accessible via permalink
pentacular

It does not support iteration like loop statements and conditional statements like If-Else

This is simply untrue.

Iteration can be purely functional -- key to understanding this is that iteration is a class of recursion.

And conditional operations are also fine in purely functional systems.

Unless your point is purely about syntax and you meant to say, "It does not support statements," which is true, but not very interesting.

Collapse
 
190245 profile image
Dave

I'd argue most of the "benefits of FP" listed here apply to non-FP code too, subject to principles being followed properly.

I'd also argue that if you're using Streams specifically for performance benefits, without going into the details of stream() versus parallelStream() then you're writing FP simply because it is the "new cool toy." I've seen many developers using Streams, then having to re-write to a traditional for-loop because their data wasn't actually immutable - the stated reason is always "I wanted to use it because it's new."

Nothing about Streams helps concurrency more than a well designed OO code either, and a developer trying Streams for the first time is certainly less productive than just using Collections that they are already familiar with.

There are times when Streams is positively harmful. There are times (such as Reactive applications) where it's a great benefit.

Other than the debate about why we should be writing FP code, a good article though.

Collapse
 
the_unconventional_coder profile image
ABHINAVA GHOSH (he/him) • Edited

Glad you liked the article.
But just one thing to point out that writing code via streams is not necessarily for "being cool" but streams actually make your code more presentable and understandable. Compared to " spaghetti code" , and traditional for loops and etc , streams make the code quite compact and readable, even if they do not always give better performance compared to traditional style.

And yes coming to the point of parallelStream vs normal streams , yes i agree that one should use parallelStream very cautiously , as it may lead up to higher resource consumption. But i dont think junior devs face many situations where they are to take a decision between what to choose, but anyway i provided links for anyone wanting to dive deep.

Collapse
 
190245 profile image
Dave

I would say that Streams offers no more clarity of code than well designed OO classes. Nothing about well thought out OO gives us "spaghetti."

Indeed, use of map() is probably a code smell that's tells us the method is doing more than one thing (and is a violation of SRP). Unless, of course, immediately after map() you call collect(), and you're inside a Transformer or Adaptor pattern.

Some comments have been hidden by the post's author - find out more