Corporate Training
Request Demo
Click me
Menu
Let's Talk
Request Demo

Tutorials

Streams API

20. Streams API  

The Streams API is a powerful feature introduced in Java 8 that enables functional-style, declarative data processing on collections of objects. It allows you to express complex data manipulations and transformations in a more concise and readable manner. In this Core Java tutorial, we'll explore the Streams API in detail, providing explanations and examples.

What is a Stream?

A stream is a sequence of elements that can be processed sequentially or in parallel. Streams can be created from various data sources, including collections, arrays, and I/O channels. The primary purpose of streams is to perform operations on their elements, such as filtering, mapping, sorting, and aggregation.

Key Features of Streams:

  • Lazy Evaluation: Streams perform operations only when necessary. Intermediate operations like 'filter' or 'map'are lazily evaluated, meaning they're computed on demand.

  • Pipelining: You can chain multiple stream operations together to create a pipeline of transformations.

  • Parallelism: Streams can be processed in parallel, which can lead to significant performance improvements on multi-core processors.

  • Immutable: Streams do not modify the underlying data source. Instead, they create new streams that represent the result of an operation.

Creating Streams:

You can create streams from various data sources:

  • Collections: You can obtain a stream from a collection using the 'stream()'method.
List<String> names = Arrays.asList("Alice", "Bob", "Charlie");
Stream<String> stream = names.stream();

 

  • Arrays: You can create a stream from an array using the 'stream.of()' method.
String[] namesArray = {"Alice", "Bob", "Charlie"};
Stream<String> stream = Arrays.stream(namesArray);

 

  • Stream Factory Methods: Java provides various factory methods in the 'stream' interface for creating streams.
Stream<String> stream = Stream.of("Alice", "Bob", "Charlie");

 

Stream Operations:

Streams support two types of operations: intermediate and terminal.

1. Intermediate Operations:

Intermediate operations are operations that transform a stream into another stream. These operations are lazy and do not execute until a terminal operation is invoked. Some common intermediate operations include:

  • 'filter(Predicate<T> predicate)': Returns a new stream containing only elements that match the given predicate.
Stream<Integer> numbers = Stream.of(1, 2, 3, 4, 5);
Stream<Integer> evenNumbers = numbers.filter(n -> n % 2 == 0);
 
  • 'map(Function<T, R> mapper)': Returns a new stream with elements transformed by the given function.
Stream<String> names = Stream.of("Alice", "Bob", "Charlie");
Stream<Integer> nameLengths = names.map(String::length);
 

2. Terminal Operations:

Terminal operations are operations that produce a result or side-effect and trigger the processing of the stream. Once a terminal operation is invoked, the stream is consumed and cannot be reused. Common terminal operations include:

  • 'forEach(Consumer<T> action)': Performs an action on each element of the stream.
Stream<String> names = Stream.of("Alice", "Bob", "Charlie");
names.forEach(System.out::println);
 
  • 'collect(Collectors.toList())': Collects the stream elements into a list.
Stream<String> names = Stream.of("Alice", "Bob", "Charlie");
List<String> nameList = names.collect(Collectors.toList());
 
  • 'filter', 'map', 'reduce', 'reduce', 'min', 'max', 'count', and many more.
Example: Filtering and Mapping:
List<Person> people = Arrays.asList(
    new Person("Alice", 25),
    new Person("Bob", 30),
    new Person("Charlie", 22)
);

List<String> namesOfYoungerThan30 = people.stream()
    .filter(person -> person.getAge() < 30)
    .map(Person::getName)
    .collect(Collectors.toList());

System.out.println(namesOfYoungerThan30); // Output: [Alice, Charlie]
 

 In this example, we filter the list of  'Person' objects to get only those younger than 30 and then map their names into a new list.

Parallel Streams:

Streams can be processed in parallel by simply converting them into parallel streams using the 'parallel()'method. This can improve performance on multi-core processors.

List<String> names = Arrays.asList("Alice", "Bob", "Charlie");
List<String> parallelResults = names.parallelStream()
    .filter(name -> name.length() > 3)
    .collect(Collectors.toList());
       
 

Conclusion:

The Streams API in Java 8 and later versions provides a powerful and expressive way to perform data manipulation and processing on collections and other data sources. By combining intermediate and terminal operations, you can create elegant and efficient data transformations. Understanding how to create and work with streams is a valuable skill for Java developers, as it leads to more readable and maintainable code for data processing tasks.