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);
โ๏ธ We can also obtain a stream from an existing list:
private static List<Dev> devList = Arrays.asList(arrayOfDevs);
devList.stream();
- And we can create a stream from individual objects using Stream.of():
Stream.of(arrayOfDevs[0], arrayOfDevs[1], arrayOfDevs[2]);
โ๏ธ 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();
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))
));
}
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);
}
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);
}
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);
}
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));
}
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));
}
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.
๐
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! ๐
Top comments (9)
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.
agreed!
Actually we use .forEach() so frequently with Streams that i thought of mentioning it, under Streams.
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()
versusparallelStream()
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.
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.
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 aftermap()
you callcollect()
, and you're inside a Transformer or Adaptor pattern.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.
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...
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.
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.
Some comments have been hidden by the post's author - find out more