Java 8 : Lambda Streams

Reading Time: 4 minutes

The official Java 8 release came with a myriad of features, the most prominent of which are undoubtedly lambdas and the Java stream API. Many projects upgraded to Java 8 just to leverage the sweet lambda syntax, or because existing frameworks updated themselves to use them. Java streams are no less important.

What Are Streams in Java?

The whole idea of Java streams is to enable functional-style operations on streams of elements. A stream is an abstraction of a non-mutable collection of functions applied in some order to the data.

A stream is not a collection where you can store elements. The most important difference between a stream and a structure is that a stream doesn’t hold the data. For example you cannot point to a location in the stream where a certain element exists. You can only specify the functions that operate on that data. And when performing operations on a stream, it will affect the original stream.

When to Use Java Streams

Java streams represent a pipeline through which the data will flow and the functions to operate on the data. As such, they can be used in any number of applications that involve data-driven functions. In the example below, the Java stream is used as a fancy iterator:


List numbers = Arrays.asList(1, 2, 3, 4); 
List result = numbers.stream()
  .filter(e -> (e % 2) == 0)
  .map(e -> e * 2)
  .collect(toList());


In this example we select only even values, by using the filter method and doubled them by mapping the function that doubles the input. What does this provide us? The streams API gives us the power to specify a sequence of operations on the data in individual steps. We don’t specify any conditional processing code, we are not tempted to write large complex functions, we don’t care about the data flow. In fact, we only bother ourselves with one data processing step at a time: we compose the functions and the data flows through the functions by itself by the power of the streams framework. The example above shows one of the most important pattern you’ll end up using with the streams:

  • Raise a collection to a stream
  • Ride the stream: filter values, transform values, limit the output
  • Compose small individual operations
  • Collect the result back into a concrete collection

Common Operations in Java Streams

In Java 8 you can easily obtain a stream from any collection by calling the stream() method. After that there are a couple of fundamental functions that you’ll encounter all the time.

Here are some common operations in Java streams:

  • Filter – returns a new stream that contains some of the elements of the original. It accepts the predicate to compute which elements should be returned in the new stream and removes the rest. In the imperative code we would employ the conditional logic to specify what should happen if an element satisfies the condition. In the functional style we don’t bother with ifs, we filter the stream and work only on the values we require.
  • Map – transforms the stream elements into something else, it accepts a function to apply to each and every element of the stream and returns a stream of the values the parameter function produced. This is the bread and butter of the Java streaming API, map allows you to perform a computation on the data inside a stream.
  • Reduce – (also sometimes called a fold) performs a reduction of the stream to a single element. You want to sum all the integer values in the stream – you want to use the reduce function. You want to find the maximum in the stream – reduce is your friend.
  • Collect – is the way to get out of the streams world and obtain a concrete collection of values, like a list in the example above.

Of course you won’t use all of these functions every time you encounter a stream, but you have them available to use at will.

Potential Issues With Java Streams

There are some caveats of using the Java streaming API though, and Venkat showed us a great example of the stream processing getting a tad out of hands. Imagine we have the following class Person:


class Person { 
   Gender gender; String name; 
   public Gender getGender() { return gender; }
   public String getName() { return name; }
}
enum Gender { MALE, FEMALE, OTHER }


This is a typical Java bean with some getters on the fields. Now, suppose we have a list of these persons and want to get the list of uppercase names of all the “FEMALE” people in that list. Easy you say, right?


List names = new ArrayList(); 
List people = …
people.stream()
  .filter(p -> p.getGender() == Gender.FEMALE)
  .map(Person::getName)
  .map(String::toUpperCase)
  .forEach(name -> names.add(name)); 


The code is so natural, we just follow the specification of what we have to do at every step. The problem is though in the mutation of the shared state. We know nothing of the nature of the stream at our hands and if the stream is parallel, the concurrent addition of the elements into the stream can lead to errors.

Using Java Stream Collect to Avoid Concurrency Errors

Instead, we should have collected the stream into the resulting list, making worrying about the concurrency and mutability the responsibility of the streams framework. Here’s the example of how to do so:


List people = …
List names = people.stream()
  .filter(p -> p.getGender() == Gender.FEMALE)
  .map(Person::getName)
  .map(String::toUpperCase)
  .collect(Collectors.toList()); 


In general, the Collectors class provides almost all necessary primitives to transform a stream into a concrete collection. One of the examples Venkat showed was the toMap() collector. You might be confused about how can an element be transformed into a key-value pair required for the map. Easy, you specify a function that turns the element into the key and another function that creates the value. Here’s an example that collects the same stream of people into a map:


List people = …
Map<String, Person> names = people.stream()
  .collect(Collectors.toMap(p -> p.getName(), p -> p)); 


The first function given to the toMap method transforms the element into the key and the second to the value for the map.

Intermediate and Terminal Operations

One of the virtues of Java streams is that they are lazily evaluated. Some operations on the streams, particularly the functions that return an instance of the stream: filter, map, are called intermediate. This means that they won’t be evaluated when they are specified. Instead the computation will happen when the result of that operation is necessary. This means that if we just specify the code like:


Stream names = people.stream()
  .filter(p -> p.getGender() == Gender.FEMALE)
  .map(Person::getName)
  .map(String::toUpperCase);

Additional Resources

Want to learn more about the latest features in Java? Be sure to visit our resource hub, Exploring New Features in Java. It covers everything from JEPs, to JDK enhancement projects for releases since Java 8.

Refrences

For more information on Streams and related methods, Click here.

Written by 

He is a Software Consultant at Knoldus Inc. He has done B.Tech from Dr. APJ Kalam Technical University Uttar-Pradesh. He is passionate about his work and having the knowledge of various programming languages like Java, C++, Python. But he is passionate about Java development and curious to learn Java Technologies. He is always impatient and enthusiastic to learn new things. He is good skills of Critical thinking and problem solving and always enjoy to help others. He likes to play outdoor games like Football, Volleyball, Hockey and Kabaddi. Apart from the technology he likes to read scriptures originating in ancient India like Veda,Upanishad,Geeta etc.

Discover more from Knoldus Blogs

Subscribe now to keep reading and get access to the full archive.

Continue reading