Post

Stream API in Java 8



Introduction

Java 8 introduced the Stream API, a powerful tool for processing sequences of elements in a functional style. This API simplifies many common operations on collections, such as filtering, mapping, and reducing, making code more readable and expressive. In this article, we will explore the Stream API in depth, covering its basic concepts, intermediate and terminal operations, parallel streams, and practical examples.

What is a Stream?

A stream is a sequence of elements that supports various operations to process those elements. Unlike collections, streams are not data structures; they do not store elements. Instead, they convey elements from a source (such as a collection, array, or I/O channel) through a pipeline of computational operations.

Creating Streams

Streams can be created from various data sources. Here are some common ways to create streams:

  • From Collections:
    1
    2
    
    List<String> list = Arrays.asList("a", "b", "c");
    Stream<String> stream = list.stream();
    
  • From Arrays:
    1
    2
    
    String[] array = {"a", "b", "c"};
    Stream<String> stream = Arrays.stream(array);
    
  • From Values:
    1
    
    Stream<String> stream = Stream.of("a", "b", "c");
    
  • From Files:
    1
    2
    3
    4
    5
    
    try (Stream<String> stream = Files.lines(Paths.get("file.txt"))) {
      stream.forEach(System.out::println);
    } catch (IOException e) {
      e.printStackTrace();
    }
    

Stream Operations

Stream operations are divided into intermediate and terminal operations.

Intermediate Operations

Intermediate operations return a new stream, allowing multiple operations to be chained together. These operations are lazy; they are not executed until a terminal operation is invoked.

  • filter: Filters elements based on a predicate.
    1
    
    Stream<String> stream = list.stream().filter(s -> s.startsWith("a"));
    
  • map: Transforms each element using a provided function.
    1
    
    Stream<String> stream = list.stream().map(String::toUpperCase);
    
  • sorted: Sorts the elements.
    1
    
    Stream<String> stream = list.stream().sorted();
    
  • distinct: Removes duplicate elements.
    1
    
    Stream<String> stream = list.stream().distinct();
    
  • limit: Truncates the stream to the first n elements.
    1
    
    Stream<String> stream = list.stream().limit(2);
    
  • skip: Skips the first n elements.
    1
    
    Stream<String> stream = list.stream().skip(2);
    

Terminal Operations

Terminal operations produce a result or a side-effect and mark the end of the stream. After a terminal operation is executed, the stream is considered consumed and cannot be used further.

  • forEach: Performs an action for each element.
    1
    
    list.stream().forEach(System.out::println);
    
  • collect: Transforms the stream into a collection or another data structure.
    1
    
    List<String> result = list.stream().filter(s -> s.startsWith("a")).collect(Collectors.toList());
    
  • reduce: Combines the elements into a single result.
    1
    
    Optional<String> concatenated = list.stream().reduce((s1, s2) -> s1 + s2);
    
  • count: Returns the number of elements in the stream.
    1
    
    long count = list.stream().count();
    
  • anyMatch: Returns true if any elements match the provided predicate.
    1
    
    boolean anyStartsWithA = list.stream().anyMatch(s -> s.startsWith("a"));
    
  • allMatch: Returns true if all elements match the provided predicate.
    1
    
    boolean allStartWithA = list.stream().allMatch(s -> s.startsWith("a"));
    
  • noneMatch: Returns true if no elements match the provided predicate.
    1
    
    boolean noneStartWithZ = list.stream().noneMatch(s -> s.startsWith("z"));
    
  • findFirst: Returns the first element in the stream, if any.
    1
    
    Optional<String> first = list.stream().findFirst();
    
  • findAny: Returns any element in the stream, if any.
    1
    
    Optional<String> any = list.stream().findAny();
    

Advanced Stream Operations

FlatMap

The flatMap method is used to flatten nested streams. For example, if you have a list of lists and want to create a single list of all elements, you can use flatMap.

1
2
3
4
5
6
7
8
List<List<String>> listOfLists = Arrays.asList(
    Arrays.asList("a", "b", "c"),
    Arrays.asList("d", "e", "f")
);

List<String> flatList = listOfLists.stream()
    .flatMap(List::stream)
    .collect(Collectors.toList());

Grouping and Partitioning

Streams provide powerful ways to group and partition data using the Collectors utility class.

  • Grouping By: Groups elements by a classifier function.
    1
    2
    
    Map<Integer, List<String>> groupedByLength = list.stream()
        .collect(Collectors.groupingBy(String::length));
    
  • Partitioning By: Partitions elements into two groups based on a predicate.
    1
    2
    
    Map<Boolean, List<String>> partitionedByA = list.stream()
        .collect(Collectors.partitioningBy(s -> s.startsWith("a")));
    

Parallel Streams

Java 8 streams can be executed in parallel to leverage multi-core processors. This is done using the parallelStream method or by calling parallel on a stream.

1
2
3
4
5
List<String> list = Arrays.asList("a", "b", "c", "d", "e", "f");

List<String> result = list.parallelStream()
    .filter(s -> s.startsWith("a"))
    .collect(Collectors.toList());

Parallel streams can significantly improve performance for large datasets, but they come with some overhead. It’s important to measure and test performance to ensure that parallel processing is beneficial for your specific use case.

Practical Examples

Example 1: Filtering and Collecting

Suppose you have a list of integers and you want to filter out the even numbers and collect the result into a new list.

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

List<Integer> evenNumbers = numbers.stream()
    .filter(n -> n % 2 == 0)
    .collect(Collectors.toList());

System.out.println(evenNumbers); // Output: [2, 4, 6, 8, 10]

Example 2: Mapping and Reducing

Consider a list of strings representing numbers. You want to convert them to integers and find their sum.

1
2
3
4
5
6
7
List<String> numberStrings = Arrays.asList("1", "2", "3", "4", "5");

int sum = numberStrings.stream()
    .map(Integer::parseInt)
    .reduce(0, Integer::sum);

System.out.println(sum); // Output: 15

Example 3: Grouping By

Given a list of strings, group them by their length.

1
2
3
4
5
6
7
List<String> words = Arrays.asList("apple", "banana", "cherry", "date", "elderberry");

Map<Integer, List<String>> groupedByLength = words.stream()
    .collect(Collectors.groupingBy(String::length));

System.out.println(groupedByLength); 
// Output: {5=[apple], 6=[banana, cherry], 4=[date], 10=[elderberry]}

Example 4: Partitioning By

Partition a list of strings into those that start with ‘a’ and those that do not.

1
2
3
4
5
6
7
List<String> words = Arrays.asList("apple", "banana", "avocado", "cherry");

Map<Boolean, List<String>> partitionedByA = words.stream()
    .collect(Collectors.partitioningBy(s -> s.startsWith("a")));

System.out.println(partitionedByA); 
// Output: {false=[banana, cherry], true=[apple, avocado]}

Conclusion

The Stream API in Java 8 is a powerful tool for processing collections in a functional style. It provides a clear and concise way to perform operations such as filtering, mapping, and reducing. By leveraging streams, you can write more readable, maintainable, and efficient code. However, it is important to understand the differences between sequential and parallel streams and to measure performance to ensure that you are getting the desired benefits. Mastering the Stream API will greatly enhance your Java programming skills and enable you to tackle complex data processing tasks with ease.

© 2024 Java Tutorial Online. All rights reserved.