Streams API in Java

Streams API in Java

Introduction

Java Streams API, introduced in Java 8, provides a powerful abstraction for processing collections of data in a declarative and functional style. Streams allow us to perform operations such as filtering, mapping, reducing, and collecting, making it easier to work with collections efficiently.




In this article, we will explore the Streams API in detail, including its features, benefits, operations, and real-world use cases.

What is a Stream?

A stream in Java is a sequence of elements that can be processed in a functional manner. Unlike collections, streams do not store data but operate on the source data such as arrays, lists, or I/O channels. Streams support lazy evaluation and allow method chaining to write concise and readable code.

Characteristics of Streams

  • Declarative: Express computations without specifying how they are executed.

  • Pipelining: Support method chaining for performing multiple operations efficiently.

  • Lazy Evaluation: Operations are performed only when a terminal operation is invoked.

  • Parallel Execution: Streams can be executed in parallel to enhance performance.

Creating Streams

Streams can be created from various data sources, including:

1. From Collections

List<String> names = Arrays.asList("Alice", "Bob", "Charlie");
Stream<String> stream = names.stream();

2. From Arrays

String[] array = {"Java", "Python", "C++"};
Stream<String> stream = Arrays.stream(array);

3. Using Stream.of()

Stream<Integer> stream = Stream.of(1, 2, 3, 4, 5);

4. Generating Streams

Stream<Double> randomNumbers = Stream.generate(Math::random).limit(5);

5. Using Stream.iterate()

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

Stream Operations

Stream operations are classified into two categories:

1. Intermediate Operations

These operations transform the stream into another stream. They are lazy and executed only when a terminal operation is invoked.

Filter

Filters elements based on a predicate:

List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5, 6);
numbers.stream().filter(n -> n % 2 == 0).forEach(System.out::println);

Map

Transforms elements using a function:

List<String> words = Arrays.asList("hello", "world");
words.stream().map(String::toUpperCase).forEach(System.out::println);

Sorted

Sorts elements in natural or custom order:

List<Integer> numbers = Arrays.asList(5, 3, 8, 1);
numbers.stream().sorted().forEach(System.out::println);

Distinct

Removes duplicate elements:

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

2. Terminal Operations

These operations consume the stream and produce a result.

ForEach

Applies a function to each element:

List<String> names = Arrays.asList("Alice", "Bob");
names.stream().forEach(System.out::println);

Collect

Collects elements into a collection:

List<String> names = Arrays.asList("Alice", "Bob");
List<String> uppercaseNames = names.stream().map(String::toUpperCase).collect(Collectors.toList());

Reduce

Performs a reduction operation:

List<Integer> numbers = Arrays.asList(1, 2, 3, 4);
int sum = numbers.stream().reduce(0, Integer::sum);
System.out.println(sum);

Count

Counts elements in a stream:

long count = Stream.of("A", "B", "C").count();
System.out.println(count);

Parallel Streams

Parallel streams enable concurrent execution for improved performance:

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

Real-World Use Case: Processing Employee Data

Consider an employee database where we filter employees earning more than $50,000 and collect their names:

class Employee {
    String name;
    double salary;

    Employee(String name, double salary) {
        this.name = name;
        this.salary = salary;
    }

    public double getSalary() { return salary; }
    public String getName() { return name; }
}

public class StreamExample {
    public static void main(String[] args) {
        List<Employee> employees = Arrays.asList(
            new Employee("Alice", 60000),
            new Employee("Bob", 45000),
            new Employee("Charlie", 75000)
        );

        List<String> highEarners = employees.stream()
            .filter(e -> e.getSalary() > 50000)
            .map(Employee::getName)
            .collect(Collectors.toList());

        System.out.println(highEarners);
    }
}

Conclusion

Java Streams API provides a functional and concise approach to handling collections. By leveraging streams, developers can write expressive and efficient code. Whether filtering data, transforming elements, or performing aggregations, the Streams API enhances productivity and improves application performance. 

Previous
Next Post »