Core Java

Java Stream – How to use Java 8 streams

1. Introduction

In this post, we will learn about how to use a Stream in Java, which was introduced as part of Java 8 version. Let us understand what actually the word streaming means with an example and what the java 8 API provides with regard to methods.

When you start watching a video, a small portion of the file is first loaded into your computer and start playing. You don’t need to download the complete video before you start playing it. This is called streaming.

java stream

In the programming world, a Stream is a conceptually fixed data structure, in which elements are computed on demand. This gives rise to significant programming benefits. The idea is that a user will extract only the values they require from a Stream, and these elements are only produced—invisibly to the user—as and when required. This is a form of a producer-consumer relationship.

In java, java.util.stream represents a stream on which one or more operations can be performed. Stream operations are either intermediate or terminal. While terminal operations return a result of a certain type, intermediate operations return the stream itself so you can chain multiple method calls in a row. Streams are created on a source, e.g. a java.util.Collection like lists or sets (maps are not supported). Stream operations can either be sequential or parallel.

2. Characteristics of a Java Stream

  • It is not a Data Structure.
  • It is designed for Lambdas
  • It does not support index access.
  • It can easily be output as arrays or lists.
  • It supports Lazy access.
  • It is Parallelizable.

3. Intermediate Operations

Intermediate operations return the stream itself so you can chain multiple method calls in a row.

Stream abstraction has a long list of useful functions for you. I am not going to cover them all, but I plan here to list down all the most important ones, which you must know first hand.

3.1. Java 8 API stream.filter()

  • This is an intermediate operation.
  • Returns a stream consisting of the elements of this stream that match the given predicate.
  • The filter() argument shall be a stateless predicate to apply to each element to determine if it should be included.
  • Predicate is a functional interface. So, we can also pass a lambda expression.
  • It returns a new stream so we can use other operations applicable to any stream.

Let us understand the method with the following example

Filter.java

import java.util.Arrays;
import java.util.List;
 
class Test 
{
    public static void main(String[] args) 
    {
        List<Integer> list = Arrays.asList(1, 2, 3, 4, 5, 6, 7, 8, 9, 10);
 
        list.stream()
            .filter(n -> n % 2 == 0)
            .forEach(System.out::println);
    }
}

Output

2
4
6
8
10

3.2. Java 8 API stream.map()

  • It is an intermediate operation and returns another stream as a method output return value.
  • Returns a stream consisting of the results of applying the given function to the elements of this stream.
  • The map operation takes a Function, which is called for each value in the input stream and produces one result value, which is sent to the output stream.
  • The function used for transformation in map() is a stateless function and returns only a single value.
  • map() method is used when we want to convert a stream of X to stream of Y.
  • Each mapped stream is closed after its contents have been placed into a new output stream.
  • map() operation does not flatten the stream as flatMap() operation does.

Let us understand with an example given below

Map.java

import java.util.Arrays;
import java.util.List;
import java.util.stream.Collectors;
 
class Test 
{
    public static void main(String[] args) 
    {
        List<String> listOfStrings = Arrays.asList("1", "2", "3", "4", "5");
         
        List<Integer> listOfIntegers = listOfStrings.stream()
                                        .map(Integer::valueOf)
                                        .collect(Collectors.toList());
         
        System.out.println(listOfIntegers);
    }
}

Output

[1, 2, 3, 4, 5]

3.3 Java 8 API stream.sorted()

  • This is a stateful intermediate operation that returns a new stream.
  • Returns a stream consisting of the elements of this stream, sorted according to the natural order.
  • If the elements of this stream are not Comparable, a java.lang.ClassCastException may be thrown when the terminal operation is executed.
  • For ordered streams, the sort is stable.
  • For unordered streams, no stability guarantees are made.

Let us use this method in an example for better understanding.

Sorted.java

import java.util.Arrays;
import java.util.List;
import java.util.stream.Collectors;
 
class Test 
{
    public static void main(String[] args) 
    {
        List<Integer> list = Arrays.asList(2, 4, 1, 3, 7, 5, 9, 6, 8);
 
        List<Integer> sortedList = list.stream()
                                    .sorted()
                                    .collect(Collectors.toList());
 
        System.out.println(sortedList);
    }
}

Output

[1, 2, 3, 4, 5, 6, 7, 8, 9]

4. Terminal operations

Terminal operations return a result of a certain type instead of again a Stream.

Before moving ahead, let’s build a collection of String beforehand. We will build out an example on this list so that it is easy to relate and understand.

List<String>  memberNames = new ArrayList();
memberNames.add("Amitabh");
memberNames.add("Shekhar");
memberNames.add("Rahul");
memberNames.add("Shahrukh");
memberNames.add("Salman");
memberNames.add("Yana");
memberNames.add("Lokesh");

4.1 Stream.forEach()

This method helps in iterating over all elements of a stream and perform some operation on each of them. The operation is passed as a lambda expression parameter.

Snippet-1

memberNames.forEach(System.out::println);

Output

Amitabh
Shekhar
Rahul
Shahrukh
Salman
Yana
Lokesh

4.2 Stream.collect()

collect() method used to receive elements from steam and store them in a collection and mentioned in the parameter function.

Snippet-2

List<String> memNamesInUppercase = memberNames.stream().sorted()
                            .map(String::toUpperCase)
                            .collect(Collectors.toList());
         
System.out.print(memNamesInUppercase);

Output

[AMAN, AMITABH, LOKESH, RAHUL, SALMAN, SHAHRUKH, SHEKHAR, YANA]

4.3 Stream.reduce()

This terminal operation performs a reduction on the elements of the stream with the given function. The result is an Optional holding the reduced value.

Snippet-3

Optional<String> reduced = memberNames.stream()
                    .reduce((s1,s2) -> s1 + "#" + s2);

Output

Amitabh#Shekhar#Aman#Rahul#Shahrukh#Salman#Yana#Lokesh

5. When to use Streams

Streams are a more declarative style. Or a more expressive style. It may be considered better to declare your intent in code than to describe how it’s done.

Streams have a strong affinity with functions. Java 8 introduces lambdas and functional interfaces, which opens a whole toybox of powerful techniques. Streams provide the most convenient and natural way to apply functions to sequences of objects.

Streams encourage less mutability. This is sort of related to the functional programming aspect — the kind of programs you write using streams tend to be the kind of programs where you don’t modify objects.

Streams encourage looser coupling. Your stream-handling code doesn’t need to know the source of the stream or its eventual terminating method.

6. When not to use Streams

Performance: A for loop through an array is extremely lightweight both in terms of heap and CPU usage. If raw speed and memory thriftiness is a priority, using a stream is worse.

Familiarity: The world is full of experienced procedural programmers, from many language backgrounds, for whom loops are familiar and streams are novel. In some environments, you want to write code that’s familiar to that kind of person.

Cognitive overhead. Because of its declarative nature, and increased abstraction from what’s happening underneath, you may need to build a new mental model of how code relates to execution. Actually you only need to do this when things go wrong, or if you need to deeply analyze performance or subtle bugs. When it “just works”, it just works.

Debuggers are improving, but even now, when you’re stepping through stream code in a debugger, it can be harder work than the equivalent loop, because a simple loop is very close to the variables and code locations that a traditional debugger works with.

7. Summary

In this post, we started with the definition of a stream in Java and then we understood the characteristics of streams.

Then we learned about two types of operations namely Intermediate operations and Terminal Operations. In detail, we have used different methods belonging to both types of operations to have a clear idea about the usage of Streams in Java. Finally, we understood when to use streams and when to avoid using them.

8. Download the Source Code

This is an example of how to use a Stream in Java 8.

Download
You can download the full source code of this example here: Java Stream – How to use Java 8 streams

Shaik Ashish

He completed his Bachelors Degree in Computer Applications. He is a freelancer, writer, Microsoft Certified in Python and Java. He enjoys Writing, Teaching, Coding in Java and Python, Learning New Technologies and Music.
Subscribe
Notify of
guest

This site uses Akismet to reduce spam. Learn how your comment data is processed.

0 Comments
Inline Feedbacks
View all comments
Back to top button