Java 8 - Stream

April 10, 2021

In this post, we will see an overview of Java 8 streams with code examples.

What we are covering in this lesson

  1. Introduction
  2. Functional Interface
  3. Types of Stream operations
  4. Stream methods
  5. Lazy evaluation
  6. Primitive Streams
  7. Intermediate Operations
  8. Terminal Operations

Introduction

A Stream represents a sequence of elements supporting sequential and parallel aggregate operations. Stream does not store data, it operates on source data structures such as List, Collection, Array etc.

Most stream operations accept Functional Interfaces that make it a perfect candidate for Lambda expressions.

Functional Interface

Functional interfaces are those interfaces which have one and only one abstract method. It can have default methods, static methods and it can also override java.lang.Object class method. There are many functional interfaces already present like Runnable, Comparable.

Java can itself identify a Functional Interface but you can also denote interface as Functional Interface by annotating it with @FunctionalInterface. If you annotate @FunctionalInterface, you should have only one abstract method otherwise you will get compilation error.

Types of Stream operations

There are two types of Stream operations.

  • Intermediate operations - return a stream that can be chained with other intermediate operations with dot (.)
  • Terminal operations - return void or non stream output

Lets look at a simple example here

import java.util.Arrays;
import java.util.List;

public class StreamEx {
	public static void main(String[] args) {
		List<String> stringList = Arrays.asList("Alan", "Berta", "Charlie", "Jake");
		 
        stringList.stream()
                   .map((s) -> s.toUpperCase())
                   .forEach(System.out::println);
	}
}

Output

ALAN
BERTA
CHARLIE
JAKE

Lets understand what is actually happening in this code. To perform a computation, stream operations are built into Stream pipeline. A normal stream pipeline consists of

  • source
  • zero or more intermediate operations
  • terminal operations

In the above code example, Stream pipeline consists of:

  • Source - stringList
  • Intermediate operation - map
  • Terminal operation - forEach

An important thing to remember here is that to get the correct behavior, streams parameters should be non-interfering - Stream source should not be modified while execution of the stream pipeline. In simple words, the stream source should not be modified until the terminal operation is performed on the source.

Stream methods

There are multiple ways to create the Stream.

  • Empty Stream - It is generally used to return Stream with zero elements rather than null.

Stream s = Stream.empty()

  • Collection Stream - .stream() or .parallelStream(). It will return a regular object stream.

    stringList.stream()
    .map((s)->s.toUpperCase())
    .forEach(System.out::println);
  • Stream.of - Can be used when we want a stream instance created directly, without any existing Collection

Stream streamArray =Stream.of("X","Y","Z");

  • Stream.generate() - generate() method accepts Supplier for element generation. It creates infinite Stream and you can limit it by calling limit() function.

    Stream<Integer> intStream=Stream.generate(() -> 1).limit(5);
    intStream.forEach(System.out::println);
    // Output
    // 1
    // 1
    // 1
    // 1
    // 1

    This will create Integer stream with 10 elements with value 1.

  • Stream.iterate() - can also be used to generate infinite stream.

    Stream<Integer> intStream = Stream.iterate(100 , n -> n+1).limit(5);
    intStream.forEach(System.out::println);
    // Output
    // 100
    // 101
    // 102
    // 103
    // 104

    First parameter of iterate method represents first element of the Stream. All the following elements will be generated by lambda expression n->n+1 and limit() is used to convert infinite Stream to finite Stream with 5 elements.

Lazy evaluation

Streams are lazy; intermediate operation are not executed until terminal operation is encountered. Each intermediate operation generates a new stream, stores the provided operation or function. When terminal operation is invoked, stream pipeline execution starts and all the intermediate operations are executed one by one.

The below code will explain this in a better way. The .peek prints the stream in uppercase, .filter will filter out the values starting with “J”, but these will not get executed until the terminal operation .count gets executed. And so we could see in output Calling terminal operation: count comes first and then the peek output and then filter

peek() method is generally used for logging and debugging purpose only.

import java.util.stream.Stream;

public class StreamEx {
	public static void main(String[] args) {
		Stream<String> nameStream = Stream.of("Alan", "Berta", "Charlie", "Jake");
		Stream<String> nameStartJ = nameStream.map(String::toUpperCase)
		                                    .peek( e -> System.out.println(e))
		                                  .filter(s -> s.startsWith("J"));
		 
		System.out.println("Calling terminal operation: count");
		long count = nameStartJ.count();
		System.out.println("Count: "+ count);
	}
}

Output

Calling terminal operation: count
ALAN
BERTA
CHARLIE
JAKE
Count: 1

Primitive Streams

Apart from regular Stream, Java 8 also provides primitive Stream for int, long and double.

  • IntStream for int
  • LongStream for long
  • DoubleStream for double

Primitive streams are different as compared to regular streams as below

  • It supports few terminal aggregate functions such sum(), average(), etc.
  • It accepts specialized function interface such as IntPredicate instead of Predicate, IntConsumer instead of Consumer.

An example of intStream is shown below

int sum = Arrays.stream(new int[] {1,2,3})
                .sum();
System.out.println(sum);
 
// Output 
// 6

You may need to convert Stream to IntStream to perform terminal aggregate operations such as sum or average. You can use mapToInt(), mapToLong() or mapToDouble() method to convert Stream to primitive Streams.

Stream.of("10","20","30")
      .mapToInt(Integer::parseInt)
      .average()
      .ifPresent(System.out::println);
// Output
// 20.0

In the reverse case, you may need to convert IntStream to Stream to use it as any other datatype. You can use mapToObj() convert primitive Streams to regular Stream.

String collect = IntStream.of(10,20,30)
                          .mapToObj((i)->""+i)
                          .collect(Collectors.joining("-"));
System.out.println(collect);
// Output
// 10-20-30

For the next set, we will consider the below code example

Employee.java

import java.util.List;

public class Employee implements Comparable<Employee>{
 
    private String name;
    private int age;
    private List<String> listOfCities;
 
    public Employee(String name, int age,List<String> listOfCities) {
        super();
        this.name = name;
        this.age = age;
        this.listOfCities=listOfCities;
    }
 
    public String getName() {
        return name;
    }
 
    public void setName(String name) {
        this.name = name;
    }
 
    public int getAge() {
        return age;
    }
 
    public void setAge(int age) {
        this.age = age;
    }
 
    public List<String> getListOfCities() {
        return listOfCities;
    }
 
    public void setListOfCities(List<String> listOfCities) {
        this.listOfCities = listOfCities;
    }
 
    @Override
    public String toString() {
        return "Employee [name=" + name + ", age=" + age + "]";
    }
 
    @Override
    public int compareTo(Employee o) {
        return this.getName().compareTo(o.getName());
    }
}

StreamExamples.java

import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.Map;
import java.util.OptionalInt;
import java.util.stream.Collectors;

public class StreamExamples {
	public static void main(String[] args) {
		List<Employee> employeesList = getListOfEmployees();

		/*
		 * Given a list of employees, you need to find all the employees whose
		 * age is greater than 30 and print the employee names.(Java 8 APIs
		 * only)
		 */
		employeesList.stream().filter(e -> e.getAge() > 30).map(Employee::getName).forEach(System.out::println);

		/*
		 * Given the list of employees, find the count of employees with age
		 * greater than 25?
		 */
		long count = employeesList.stream().filter(e -> e.getAge() > 25).count();

		System.out.println(count);

		/*
		 * Given the list of employees, find the employee whose name is John.
		 */
		employeesList.stream()
				// .map(e -> e.getName())
				.filter(e -> e.getName().equals("Jake")).forEach(System.out::println);

		/*
		 * Given a list of employees, You need to find highest age of employee?
		 */
		OptionalInt maxAge = employeesList.stream().mapToInt(e -> e.getAge()).max();
		System.out.println(maxAge.getAsInt());

		/*
		 * Given a list of employees, you need sort employee list by age? Use
		 * java 8 APIs only
		 */
		employeesList.stream().sorted((e1, e2) -> e1.getAge() - e2.getAge()).forEach(System.out::println);

		/*
		 * Given the list of Employees, you need to join the all employee names
		 * with ","?
		 */
		List<String> empNames = employeesList.stream().map(e -> e.getName()).collect(Collectors.toList());

		System.out.println(String.join(",", empNames));

		// OR

		String empNamesStrJoined = employeesList.stream().map(e -> e.getName()).collect(Collectors.joining(",-"));

		System.out.println(empNamesStrJoined);

		/*
		 * Given the list of employees, you need to group them by name
		 */
		Map<String, List<Employee>> empNamesGroupedByName = employeesList.stream()
				.collect(Collectors.groupingBy(Employee::getName));
		empNamesGroupedByName.forEach((name, listByName) -> System.out.println(name + "-->" + listByName));
	}

	public static List<Employee> getListOfEmployees() {
		List<Employee> listOfEmployees = new ArrayList<>();

		Employee e1 = new Employee("Alan", 24, Arrays.asList("Newyork", "Banglore"));
		Employee e2 = new Employee("Berta", 27, Arrays.asList("Paris", "London"));
		Employee e3 = new Employee("Charlie", 32, Arrays.asList("Pune", "Seattle"));
		Employee e4 = new Employee("Jake", 22, Arrays.asList("Chennai", "Hyderabad"));

		listOfEmployees.add(e1);
		listOfEmployees.add(e2);
		listOfEmployees.add(e3);
		listOfEmployees.add(e4);
		listOfEmployees.add(new Employee("Alan", 25,Arrays.asList("Zurich","Switzerland")));

		return listOfEmployees;
	}
}

Output

Charlie
2
Employee [name=Jake, age=22]
32
Employee [name=Jake, age=22]
Employee [name=Alan, age=24]
Employee [name=Alan, age=25]
Employee [name=Berta, age=27]
Employee [name=Charlie, age=32]
Alan,Berta,Charlie,Jake,Alan
Alan,-Berta,-Charlie,-Jake,-Alan
Jake-->[Employee [name=Jake, age=22]]
Charlie-->[Employee [name=Charlie, age=32]]
Alan-->[Employee [name=Alan, age=24], Employee [name=Alan, age=25]]
Berta-->[Employee [name=Berta, age=27]]

Intermediate Operations

  • map() - operation is used to convert Stream to Stream. It produces one output result of type ‘R’ for each input value of type ‘T’. It takes Function interface as parameter. You can also use map even if it produces result of same type.
  • filter() - operation is used to filter stream based on conditions. Filter method takes Predicate() interface which returns boolean value.
  • sorted() - You can use sorted() method to sort list of objects. sorted method without arguments sorts list in natural order. Natural order means sorting the list based on comparable interface implemented by list element type, so List will be sorted on the basis of comparable interface implemented by Integer class. It also accepts comparator as parameter to support custom sorting.
  • limit() - You can use limit() to limit the number of elements in the stream. We have already used this in the previous examples
  • skip() - method is used to discard first n elements from the stream.
  • flatmap() - operation generates one output for each input element.

Terminal Operations

  • foreach() - used to iterate over collection/stream of objects. It takes Consumer as a parameter.
  • collect() - performs mutable reduction on the elements of Stream using Collector. Collectors is utility class which provides inbuilt Collector. Some example usage of Collectors is provided in the sample code above.
  • reduce() - The reduce operation combines all elements of Stream and produces single result.
  • count() - used to count number of elements in the stream.
  • allMatch() - returns true when all the elements in the stream meet provided condition. Thing to remember is that this is a short-circuiting terminal operation because operation stops as soon as it encounters any unmatched element.
  • nonMatch() - returns true when all the elements in the stream do not meet provided condition. This is also a short-circuiting terminal operation as operation stops as soon as it encounters any matched element.
  • anyMatch() - returns true when any element in the stream meets provided condition. Again, this is also a short-circuiting terminal operation because operation stops as soon as it encounters any matched element.
  • min() - returns minimum element in the stream based on the provided comparator. It returns an object which contains actual value.
  • max() - returns maximum element in the stream based on the provided comparator. It returns an object which contains actual value.

Thats all for Java stream.