In Java 8, one of the additional features that were introduced was Streams. Please note that Streams in Java 8 are not the same as Input/Output streams that are typically used to read and write to files. They created streams to process an enormous set or collection of objects quickly and efficiently. It’s an abstract layer with a sequence of objects that can help you process data in a declarative manner by using methods that can easily be pipelined to generate required results. 

Let’s understand this with the help of an example. In SQL, we use declarative statements such as - “Select max(ID), name_of_employee FROM Employee” to return the employee name who has the maximum value of ID. From a developer’s side, he does not have to perform any kind of computation like creating for loops and if-else conditions to loop through the entire database and perform checks. Nowadays, there are multi-core processors available quite easily and developers need to optimize their code and include preprocessor directives that allow their programs to process parallelly. This is quite efficient, however, as the code snippet becomes bulky, difficult to understand, and more prone to errors and bugs. 

To cater to these issues, it introduced the concept of Streams in Java 8. This allows developers and programmers to use the full potential of multi-core processors and write declarative statements just like SQL to achieve the desired results quickly, efficiently, doing no computation.

Also, please note that data can’t be stored in streams and hence, it won’t be wise to consider streams as data structures. Moreover, it never modifies the data source that it operates on. 

Some of the salient features of Streams in Java are - 

  1. They are not data structures but can take input from data sources and structures like collections, Input/Output Channels, Arrays, etc. 
  2. They can create pipelined methods that generate desired results by operating on these data structures without modifying them.
  3. The intermediate operation’s executions happen lazily, which returns a result stream. This allows the pipelining of intermediate operations. The end of a stream is marked by terminal operations.

Types of Streams Operations in Java

The operations that the streams perform on the data source objects can be divided into two types predominantly. They are - 

  1. Intermediate Operations - These operations are executed lazily and generate results as streams that can be pipelined to other intermediate operations only after processing the data source objects. Examples of Intermediate Operations are - map, filter, sorted, etc.
  2. Terminal Operations - These operations mark the end of a Stream in Java. Some examples are - collect, reduce, forEach, etc.

Here's How to Land a Top Software Developer Job

Full Stack Developer - MERN StackExplore Program
Here's How to Land a Top Software Developer Job

Lazy Evaluation

By lazy executions, we mean that the computations on the data structures or sources only take place when a terminal operation marking the end of the stream is initiated, and the elements of the data source are consumed as and when required, thus optimizing the entire process. The intermediate operations are not executed until and unless a processing result is required. This is called the lazy execution of intermediate operations. You can use the java.util.stream.* package to access all the stream operations in Java.

Method Types and Pipelines

Predominantly, streams in Java are divided into two types of operations. They are intermediate and terminal operations. Apart from that, you can sub-categorize them into other operations such as - 

  • Comparison-based operations
  • Specialized operations
  • Reduced operations
  • Infinite Stream operations
  • File operations

You will explore these operations in greater detail later on in this tutorial. As discussed earlier, streams in Java are mainly categorized into two broad categories - intermediate and terminal operations. The intermediate operations such as limit, filter, map, etc. return streams on which you can perform further processing. And terminal operations mark the completion of a stream.

A stream pipeline consists of a source of the stream which has some intermediate operations and one terminal operation. Also, as discussed earlier, short-circuit evaluation or lazy evaluation is adopted by intermediate operations to allow computations to be completed in finite time even for infinite streams.

Characteristics of Streams in Java

As discussed, Streams in Java are a sequence of objects from a data source that allows the execution of aggregate operations on them. The characteristics of Streams in Java are - 

  1. Declarative - You only need to specify what is to be done on the data source objects, and not how.
  2. A sequence of Objects - The stream never stores data, but it computes those elements or objects on-demand sequentially.
  3. Lazy evaluation - The stream won’t perform anything until the terminal operation has been called. An intermediate operation will only be executed if its processing results are required.
  4. Aggregate Operations - Similar to SQL, you can use aggregate operations like reduce, map, filter, find, match, limit, etc. on the objects.
  5. Pipelining - The output or results of the operations are streamed so that they can be pipelined. There are intermediate operations that get lazily evaluated by taking input, processing them, and returning the output result to the target object until the stream reaches a terminal operation that marks the end.
  6. Single Consumption - Once the terminal operation has been called, if you want to apply the same operations, you will have to generate a new stream.
  7. Parallelism - By default, the streams are sequential. However, compared to traditional multi-processing techniques in Java, parallelizing streams are quite easier.
  8. Auto-iteration - You don’t need to specify any iteration techniques such as loops, etc. to iterate over the objects. The streams will automatically do it for you.

How to Generate Streams in Java?

In Java 8, you can generate streams using the collection interface in two different ways - 

  1. Using the Stream() method - This method will consider the collection as the data source and generate a sequential stream.
  2. Using the parallelStream() method - Instead of generating a sequential stream, this method will generate a parallel stream.

In the rest of the tutorial, you will see the most common stream operations in Java. 

  1. Intermediate Stream Operations in Java
    1. Filter() Operation
    2. Map() Operation
    3. Limit() Operation
  2. Terminal Stream Operations in Java
    1. Collect() Operation
    2. forEach() Operation
  3. Comparison-based Stream operations in Java
    1. Sorted() Operation
    2. Min and Max Operation
    3. Distinct() Operation
    4. allMatch, anyMatch, noneMatch
  4. Specialized Stream Operations in Java
    1. Sum() Operation
    2. Average() Operation
  5. Reduction Stream Operation in Java
    1. Reduce() Operation
  6. Parallel Stream Processing in Java
  7. Infinite Stream Operations in Java
    1. Generate() Operation
    2. Iterate() Operation
  8. File Stream Operations in Java
    1. File write Stream Operation
    2. File read Stream Operation

So with no further ado, let’s discuss each of them in detail with examples.

Intermediate Stream Operations in Java

1. Filter() Operation

The filter operation is an intermediate operation. This can be used to segregate elements of a data source based on some criteria. The predicate has to be passed as an argument. Consider the example below.

import java.util.*;

import java.util.stream.*;

class Streams{

   public static void main(String args[]){

       List<String> names = Arrays.asList("Mike", "David", "", "John", "Jane", "Charlie", "Alan", "", "Joanna", "");

       int count = (int) names.stream().filter(name -> name.isEmpty()).count();

       System.out.println("The total number of empty names are: " + count);

   }

}

In the above program, you have created a list of names with some empty strings as well. You have created a stream on the list of names and added a filter method that takes each name as input and checks whether the name is an empty string or not. You have used the count method as a terminal operation here. It returns the count of all empty strings in the names list. Let’s verify the output.

StreamOperationsinJava_1

2. Map() Operation

The map() operation is used to generate a stream of results after applying a function to all the objects of the data source given to the stream. It’s a very useful stream operation and can be used for a variety of purposes like squaring the elements or adding a number to certain elements. Let’s write a program using the map operation that squares all the elements passed to a stream, and then return the output as a list of the squared numbers.

import java.util.*;

import java.util.stream.*;

class Streams{

   public static void main(String args[]){

       List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5, 6);

       List<Integer> listSquaredNumbers = numbers.stream().map(number -> number*number).collect(Collectors.toList());

       System.out.println("The squared numbers are - " + listSquaredNumbers);

   }

}

Let’s verify the output below.

StreamOperationsinJava_2

You can see that using the map intermediate option, you have generated the squares of all the numbers passed to the stream, and returned the output stream as a list using the collect terminal operation.

3.  Limit() Operation

The limit() operation is an intermediate operation that is often used to reduce or limit the final size of the stream output. For example, let’s create a random object and use the limit operation to generate 10 random numbers. You will also use the ints() intermediate operation to generate only integer numbers. Finally, you will use the forEach() terminal operation to print each element from the stream.

import java.util.*;

import java.util.stream.*;

class Streams{

   public static void main(String args[]){

       Random myRan = new Random();

       myRan.ints().limit(10).forEach(num -> System.out.println(num));

   }

}

Let’s see whether the output is generated as per our expectations or not.

StreamOperationsinJava_3.

You can see that the stream has returned 10 integers as the output.

Terminal Stream Operations in Java

1. Collect() Operation

The collect() method is a terminal operation. It can be used to return the results of the intermediate operations that have been performed on a stream. Along with the collect terminal operation, it is common to use collectors() that combine the processing results and return them as a list or string. You can use the Collectors.toList() method or Collectors.joining() method as per your requirements. Let’s see an example for this operation below.

import java.util.*;

import java.util.stream.*;

class Streams{

   public static void main(String args[]){

       List<String> names = Arrays.asList("Mike", "David", "", "John", "Jane", "Charlie", "Alan", "", "Joanna", "");

       List<String> listFilteredNames = names.stream().filter(name -> !name.isEmpty()).collect(Collectors.toList());

       String mergedFilteredNames = names.stream().filter(name -> !name.isEmpty()).collect(Collectors.joining(", "));

       System.out.println("The filtered names in List are - " + listFilteredNames);

       System.out.println("The filtered names as a string is - " + mergedFilteredNames);

   }

}

In the above program, you have defined a list of names with some empty strings as well. You have created two streams. The first one uses a filter intermediate operation to filter out those names which are not empty strings and uses the collect terminal operation along with the Collectors.toList() method, to convert the output stream to a list. In the second stream, everything remains the same, however, instead of returning the output stream as a list, you have used the Collectors.joining(separator) method to merge the stream of names to a single string. Let’s see the output.

TerminalStreamOperationsinJava_1

You can see that we have returned both the list of filtered names as well as the merged string.

2. forEach() Operation

The forEach stream operation is a terminal operation. This can be used to iterate each element of the given stream. For example, use the forEach terminal operation to print all the numbers from the generated stream after using the map intermediate operation to square the elements.

import java.util.*;

import java.util.stream.*;

class Streams{

   public static void main(String args[]){

       List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5, 6);

       numbers.stream().map(number -> number*number).forEach(number -> System.out.println(number));

   }

}

Here, instead of converting the stream into a list after mapping them and printing them using a separate line of code, you have simply used the forEach terminal operation to print them directly.

TerminalStreamOperationsinJava_2

You can see that the output has been generated as expected.

Comparison-based Stream operations in Java

Comparison-based stream operations are used to perform those operations in a data source that requires comparing elements with each other. Such operations include sorting, finding the minimum and maximum elements, finding distinct elements, matching, etc. Let’s discuss all these one-by-one.

1. Sorted() Operation

The Sorted() intermediate operation for streams in Java is often used to sort a stream of elements. You can sort numbers as well as string objects. Let’s see an example to demonstrate the use of sorted() intermediate operation.

import java.util.*;

import java.util.stream.*;

class Streams{

   public static void main(String args[]){

       Random myRan = new Random();

       myRan.ints().limit(10).sorted().forEach(num -> System.out.println(num));

   }

}

Here, you have created a random object and used the ints() and limit() intermediate operation to generate a stream of 10 random integers. You have then used the sorted() intermediate operation to sort the stream of integers and printed each of them using the forEach() terminal operation. Let’s check the output.

Comparison-basedStreamoperations_1

2. Min and Max Operation

Based on a comparator, min/max operations are used to return the minimum and maximum elements in a stream of elements. Let’s see how to do so.

import java.util.*;

class Streams{

  public static void main(String args[]){

      List<Integer> nums = Arrays.asList(4, 1, 2, 9, 10, 23, 65, 6, 98, -14, 33);

      Optional<Integer> minNumber = nums.stream().min((num1, num2) -> num1.compareTo(num2));

      System.out.println(minNumber.get());

  }

}

In the above example, you have a stream of numbers and on that, you have invoked the min intermediate operation which takes as input a comparator, and then you have returned the minimum element using the get() terminal operation.

Comparison-basedStreamoperations_2

You can see that it has successfully returned the minimum element, which is -14.

You can also use the same example to demonstrate the max operation. 

import java.util.*;

class Streams{

  public static void main(String args[]){

      List<Integer> nums = Arrays.asList(4, 1, 2, 9, 10, 23, 65, 6, 98, -14, 33);

      Optional<Integer> maxNumber = nums.stream().max((num1, num2) -> num1.compareTo(num2));

      System.out.println(maxNumber.get());

  }

}

Instead of the min operation, you have used the max operation here. 

Comparison-basedStreamoperations_2_2

3. Distinct() Operation

The distinct() operation is used to return a stream of all the distinct elements from the input stream. It eliminates the duplicate elements using the equals() method on the elements. Let’s understand this with the help of the program below.

import java.util.*;

import java.util.stream.*;

class Streams{

  public static void main(String args[]){

      List<Integer> nums = Arrays.asList(4, 1, 2, 9, 2, 4, 13, 0, -14, 22, -14, 0, 4);

      List<Integer> distinctNumbers = nums.stream().distinct().collect(Collectors.toList());

      System.out.println(distinctNumbers);

  }

}

Here, you have an input list of elements containing duplicates as well. You have invoked the distinct() intermediate operations and used the collect() terminal operation to out the stream as a list.

Comparison-basedStreamoperations_3

You can see that a list of all the distinct elements in the input array has been returned.  

4. allMatch, anyMatch, noneMatch Operations

These operations are used to return a boolean result based on the input predicate. It leverages short-circuiting to determine the results. The allMatch() operation is used to determine if the predicate holds true for all the elements in the input stream or not. The anyMatch return tries if the predicate holds true for any of the elements. And the noneMatch operation returns true if none of the elements returns true for the predicate. 

Let’s understand all three operations with the help of an example.

import java.util.*;

import java.util.stream.*;

class Streams{

  public static void main(String args[]){

      List<Integer> nums = Arrays.asList(-4, -8, 0, -2, -10);

      boolean allNeg = nums.stream().allMatch(num -> num<0);

      boolean anyNeg = nums.stream().anyMatch(num -> num<0);

      boolean mulofthree = nums.stream().noneMatch(num -> num%3==0);

      System.out.println("Are all the numbers negative? - " + allNeg);

      System.out.println("Are any of the numbers negative? - " + anyNeg);

      System.out.println("Are all the numbers a multiple of 3? - " + mulofthree);

  }

}

You can see that here, there is a list of numbers and you need to check whether all the numbers or any of the numbers are negative and if all the numbers are a multiple of 3. You used the allMatch, anyMatch, and noneMatch operations to do so.

Comparison-basedStreamoperations_4

Become a Certified UI UX Expert in Just 5 Months!

UMass Amherst UI UX BootcampExplore Program
Become a Certified UI UX Expert in Just 5 Months!

Specialized Stream Operations in Java

Instead of using the standard streams which contain a stream of object references, you can convert each element of the stream to integer, double, long, etc. There are streams such as DoubleStream, IntStream, LongStream which are simply the primitive specializations for their corresponding types. They extend the BaseStream instead of Stream. Let’s try to create an IntStream by using the mapToInt() operation on any Stream. The mapToInt returns OptionalInt values based on the intermediate operations that you perform.

import java.util.*;

class Streams{

 public static void main(String args[]){

     List<String> nums = Arrays.asList("4", "1", "2", "9", "10", "23", "65", "6", "98", "-14", "33");

     OptionalInt maxNumber = nums.stream().mapToInt(i -> Integer.parseInt(i)).max();

     System.out.println("The maximum number is - " + maxNumber);

 }

}

SpecializedStreamOperations

Specialized Streams allows you to perform many other specialized operations that you can’t perform on standard streams. These are - sum(), range(), average(). Let’s check them out with the following example.

1. Sum() Operation

You need to first convert the standard stream to a specialized stream, only then can you apply specialized operations such as sum, average, etc. on them. Let’s check out the example below.

import java.util.*;

class Streams{

 public static void main(String args[]){

     List<String> nums = Arrays.asList("4", "1", "2", "9", "10", "23", "65", "6", "98", "-14", "33");

     Double sum = nums.stream().mapToDouble(i -> Double.parseDouble(i)).sum();

     System.out.println("The sum is - " + sum);

 }

}

SpecializedStreamOperations_1

Here, you have converted the standard stream into a DoubleStream and then performed the sum operation on it.

2. Average() operation

Instead of finding the sum of the specialized stream, try to find out the average. 

import java.util.*;

class Streams{

 public static void main(String args[]){

     List<String> nums = Arrays.asList("4", "1", "2", "9", "10", "23", "65", "6", "98", "-14", "33");

     OptionalDouble average = nums.stream().mapToDouble(i -> Double.parseDouble(i)).average();

     System.out.println("The average is - " + average);

 }

}

SpecializedStreamOperations_2 

Reduction Stream Operations in Java

Reduction operations leverage repeated use of a combining operation to combine a sequence of elements from the stream into a single result. Previously, you saw min and max operations which were also reduction operations. 

1. Reduce() Operation

You can use the reduce() terminal operation to reduce the stream elements into a particular single value. As a parameter, you need to input a binary operator. Let’s create a program to add all the even numbers to a list.

import java.util.*;

import java.util.stream.*;

class Streams{

   public static void main(String args[]){

       List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5, 6, 7, 8, 9, 0);

       int evenNumbers = numbers.stream().filter(num -> num%2==0).reduce(0, (sum, x) -> sum + x);

       System.out.println("The sum of the even numbers is: " + evenNumbers);

   }

}

Here, you have used the filter() intermediate operation to filter out all the even numbers from the stream of numbers and then used the reduced terminal operation to calculate the sum of the resulting stream. You have initialized the sum variable to 0, and for each element in the stream, you have to keep on adding the element to the sum variable and return it at the end of the terminal operation.

ReductionStreamOperations

Parallel Stream Processing in Java

Now, instead of using the Stream() operation to create a sequential stream, you must use the parallelStream() method to generate streams for parallel processing. Let’s print all the non-empty strings in a list using parallelStream(). This way is a lot faster than sequential stream because it efficiently utilizes the multi-core processing capabilities of your system.

import java.util.*;

import java.util.stream.*;

class Streams{

   public static void main(String args[]){

       List<String> names = Arrays.asList("Mike", "David", "", "John", "Jane", "Charlie", "Alan", "", "Joanna", "");

       List<String> listFilteredNames = names.parallelStream().filter(name -> !name.isEmpty()).collect(Collectors.toList());

       System.out.println("The list of filtered names is: " + listFilteredNames);

   }

}

Let’s check the output of the above program.

ParallelStreamProcessing

Infinite Stream Operations in Java

In some cases, you might want to operate on a stream of elements as a data source instead of a limited size data structure. You might not know how many elements you might want to perform the operations on, beforehand. In such cases, you can use unbounded or Infinite streams. Here, you can generate infinite streams in two ways.

1. Generate() operation

With the generate operation, you need to specify a supplier which will generate the infinite stream of elements. You also need to apply a condition that will help to terminate the processing eventually. You can use the limit() operation to do so. Let’s check out an example program.

import java.util.*;

import java.util.stream.Stream;

class Streams{

 public static void main(String args[]){

     Stream.generate(Math::random).limit(10).forEach(num -> System.out.println(num));

 }

}

InfiniteStreamOperations_1

Here, you have defined a generate operation with a random integer supplier limited to 10 numbers using the limit operation. 

2. Iterate() operation

You can define a seed element (starting value) and a function that uses the previous value to generate new values inside the iterate operation. Even here, you need an operation that can eventually terminate the process. Let’s check out the below example.

import java.util.stream.Stream;

class Streams{

 public static void main(String args[]){

     Stream.iterate(3, i -> i + 3).limit(10).forEach(num -> System.out.println(num));

 }

}

InfiniteStreamOperations_2.

Here, you have defined an iterate operation with a seed value 3 and a lambda function which returns all the multiple of 3 and you have limited the stream size to 10.

File Stream Operations in Java

You can even use streams in Java to operate on files. You can read and write to files using Java streams. Let’s see how to do so.

1. File write Stream operation

Let’s try to write an array of strings using streams to a file.

import java.io.IOException;

import java.io.PrintWriter;

import java.nio.file.Files;

import java.nio.file.Paths;

import java.util.stream.Stream;

class Streams{

 public static void main (String args[]) throws IOException{

       String[] message = {"Welcome", "to", "Simplilearn"};

       try(PrintWriter pw = new PrintWriter(

           Files.newBufferedWriter(Paths.get("hello.txt"))

       )){

           Stream.of(message).forEach(pw::println);

       }

   }

}

Here, you have used the PrintWriter along with Streams in Java to write to files. Let’s check the output.

FileStreamOperations_1 

FileStreamOperations_1_1.

2. File read Stream operation

Let’s see a program where you will read lines from a file, and print those that are palindromes. You will do all of this using streams and its operations.

import java.io.IOException;

import java.nio.file.Files;

import java.nio.file.Paths;

import java.util.List;

import java.util.stream.Collectors;

import java.util.stream.Stream;

class Streams{

 public static void main (String args[]) throws IOException{

       Stream<String> stream = Files.lines(Paths.get("hello.txt")); 

       List<String> palindromes = stream.filter(word -> word

                                               .compareToIgnoreCase(new StringBuilder(word)

                                               .reverse()

                                               .toString()) == 0)

                                               .collect(Collectors.toList());

       stream.close();

       System.out.println(palindromes);

   }

}

Here, we have read the lines of the input file into a stream and used the filter operation to filter those strings that are palindromes.

FileStreamOperations_3

You can see that it has printed all the palindromes from the input files.

Get a firm foundation in Java, the most commonly used programming language in software development with the Java Certification Training Course.

Wrapping Up!

In this comprehensive guide on streams in Java, you have learned that a stream consists of a data source which can be any data structure, and a series of one or more intermediate operations that have to be performed on the elements of the data source. These intermediate operations are pipelined and adopt lazy evaluation techniques. Finally, the stream ends with a terminal operation. Throughout the process, the stream does not modify the elements of the original data source.

We certainly hope that through this detailed guide, you will now be able to get hands-on with streams in Java. If you are looking to learn further and master Java on your way to becoming a Full Stack Java developer, Simplilearn’s Full Stack Java Developer Master’s Program is ideal for you. This comprehensive bootcamp covers everything you need to become a Java expert today. You will learn over 30 in-demand Java-related skills and perfect them with multiple projects as part of the learning process.

If you have any questions for us, please leave them in the comments section below. Our experts will answer them for you right away!

Our Software Development Courses Duration And Fees

Software Development Course typically range from a few weeks to several months, with fees varying based on program and institution.

Program NameDurationFees
Caltech Coding Bootcamp

Cohort Starts: 17 Jun, 2024

6 Months$ 8,000
Full Stack Developer - MERN Stack

Cohort Starts: 24 Apr, 2024

6 Months$ 1,449
Automation Test Engineer

Cohort Starts: 1 May, 2024

11 Months$ 1,499
Full Stack Java Developer

Cohort Starts: 14 May, 2024

6 Months$ 1,449