Introduction to Java Stream API

Java Stream API was introduced in java 8, is a powerful tool for processing sequences of elements in a functional style. It allows you to perform operations such as filtering, mapping, and reducing on collections of data. The Stream API can work with collections, arrays, and I/O channels, making it a versatile tool for modern Java development.

What is Stream?

A stream is a sequence of element that supports various methods which can be pipelined to produce the desired result. Streams are not data structures; instead, they convey elements from a source (like collections) though a pipeline of computations operations.

Characteristics of Streams

  • No Storage: Streams do not store elements; they are computed on demand.
  • Functional: Operations on streams do not modify the underlying data structure; they produce a new stream or result.
  • Laziness: Many stream operations are lazy, meaning they not executed until a terminal operation is invoked.
  • Possibly Infinite: Streams can be finite or potentially infinite, depending on the source.

Creating Streams

Streams can be created from various data sources such as collections, arrays, or I/O channels

From Collections

List<String> myList = Arrays.asList("a", "b", "c", "d");
Stream<String> myStream = myList.stream();

From Arrays

String[] myArray = {"a", "b", "c", "d"};
Stream<String> myStream = Arrays.stream(myArray);

From Values

Stream<String> myStream = Stream.of("a", "b", "c", "d");

From Files

Path path = Paths.get("file.txt");
Stream<String> lines = Files.lines(path);

Infinite Streams

Stream<Integer> infiniteStream = Stream.iterate(0, n -> n + 2);

Termination Operation

Collect

List<String> resultList = myStream.collect(Collectors.toList());

forEach

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

Reduce

Optional<String> concatenated = myStream.reduce((s1, s2) -> s1 + s2);

Count

long count = myStream.count();

FindFirst

Optional<String> firstElement = myStream.findFirst();

Stream API in Action: Examples

Example 1: Filter and Collect

List<String> names = Arrays.asList("John", "Jane", "Jack", "Doe");
List<String> filteredNames = names.stream()
    .filter(name -> name.startsWith("J"))
    .collect(Collectors.toList());

This will filter all names and return filteredNames = [“John”, “Jane”, “Jack”]

Example 2: Map and Reduce

List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5);
int sum = numbers.stream()
    .map(n -> n * n)
    .reduce(0, Integer::sum);

This will combine all elements to single result. In this case above streams will transform each element to its square [1, 4, 9, 16, 25]. After that will plus each element together with the start value 0, the result will be 55

Example 3: Grouping By

List<String> items = Arrays.asList("apple", "banana", "apple", "orange", "banana", "banana");
Map<String, Long> itemCount = items.stream()
    .collect(Collectors.groupingBy(Function.identity(), Collectors.counting()));

This will group and count each element in an arrays {orange=1, banana=3, apple=2}

Example 4: Parallel Streams

List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5);
numbers.parallelStream()
    .forEach(System.out::println);

This will execute in parallel. We can see it with the result not be ordered [3,5,4,2,1]

Advanced Stream Concepts

Infinite Streams

An Infinite stream is a stream that can potentially produce an unbounded number of elements. Such streams typically generated using methods like `Stream.iterate` or `Stream.generate`. These methods allow you to define a potentially infinite series of elements, which can be processed lazily.

Stream.iterate

The Stream.iterate method generates an infinite sequential ordered stream produced by iterative application of a function to an initial element, typically used to produce a sequence of elements

static <T> Stream<T> iterate(T seed, UnaryOperator<T> f)
  • seed: The initial element
  • f: A function to be applied to the previous element to produce a new element
Stream<Integer> infiniteStream = Stream.iterate(0, n -> n + 1);
infiniteStream.limit(10).forEach(System.out::println);

This will generate an array with size of 10 and next element generate by current + 1 so result will be [0, 1,2,3,4,5,6,7,8,9]

Custom Collectors

The Collector Interface

A Collector in java is a mutable reduction operation that accumulates input elements into a mutable container, optionally transforming the accumulated result into a final representation after all input elements have been processed. The Collector interface provides methods to create such collectors.

Component of a Custom Collector

The `Collector.of` method is a factory method that creates a new collector with three or four functions

  1. Supplier: Creates a new result container
  2. Accumulator: Incorporates a new element into the result container
  3. Combiner: Merges two result containers
  4. Finisher: Transform the final result container into the desired result type
List<String> list = Arrays.asList("apple", "banana", "cherry");
Stream<String> myStream = list.stream();

Collector<String, StringJoiner, String> customCollector = 
    Collector.of(
        () -> new StringJoiner(", "),
        (j, s) -> j.add(s.toUpperCase()),
        StringJoiner::merge,
        StringJoiner::toString
    );

String result = myStream.collect(customCollector);
System.out.println(result);

Detailed Breakdown

  1. Supplier: () -> new StringJoiner(“, “)
    • This will create new StringJoiner with a comma and space (“, “). This provides the initial result container.
  2. Accumulator: (j, s) -> j.add(s.toUpperCase())
    • This lambda expression takes a StringJoiner (j) and a string (s), converts s to uppercase, and adds it to the StringJoiner
  3. Combiner: StringJoiner::merge
    • The combiner function merges two StringJoiner instances, which is important for parallel stream processing where the stream is divided into substreams.
  4. Finisher: StringJoiner::toString
    • The finisher function converts the StringJoiner into its final string representation.

Output

APPLE, BANANA, CHERRY

FlatMap

The flatmap method is used to flatten a stream of streams into a single stream

List<List<String>> list = Arrays.asList(
    Arrays.asList("a", "b"),
    Arrays.asList("c", "d"));
Stream<String> flatStream = list.stream()
    .flatMap(Collection::stream);
flatStream.forEach(System.out::println);

Here we have a list [[“a”, “b”], [“c”, “d”]] -> [“a”, “b”, “c”, “d”]

Conclusion

The Java Stream API is a powerful and flexible tool for processing data in a functional style. By understanding its concepts and utilizing its features, developer can write cleaner, more concise, and efficient code. Whether filtering data, transforming elements, or aggregating results, the Stream API provides a rich set of operations that make data manipulation straightforward and expressive.

Leave a Comment

Your email address will not be published. Required fields are marked *

Scroll to Top