Java Interview Questions

30 Questions
Java

Java

Web DevelopmentBackend

Question 28

What is the Stream API and how is it used?

Answer:

The Stream API in Java, introduced in Java 8, provides a powerful and efficient way to process sequences of elements. It allows you to perform operations such as filtering, mapping, and reducing on collections of data in a functional style. Streams are designed to work with data in a more declarative manner, making code more readable and concise.

Key Features of the Stream API

  1. Declarative: Allows you to write code that specifies what you want to achieve rather than how to achieve it.
  2. Pipelining: Operations can be chained together to form a pipeline.
  3. Internal Iteration: Handles the iteration of elements internally, freeing you from the need to write explicit loops.
  4. Laziness: Stream operations are lazy; they are not executed until a terminal operation is encountered.

Key Components of the Stream API

  1. Stream Source: The source of data, such as a collection, array, or I/O channel.
  2. Intermediate Operations: Operations that transform a stream into another stream, such as filter, map, sorted, etc. These operations are lazy and do not trigger the stream processing.
  3. Terminal Operations: Operations that produce a result or side-effect, such as forEach, collect, reduce, etc. These operations trigger the stream processing.

Basic Usage of the Stream API

1. Creating Streams

Streams can be created from various sources, such as collections, arrays, or generator functions.

Example:

import java.util.Arrays;
import java.util.List;
import java.util.stream.Stream;

public class StreamCreationExample {
    public static void main(String[] args) {
        // Creating a stream from a collection
        List<String> list = Arrays.asList("a", "b", "c");
        Stream<String> streamFromList = list.stream();

        // Creating a stream from an array
        String[] array = {"a", "b", "c"};
        Stream<String> streamFromArray = Arrays.stream(array);

        // Creating a stream using Stream.of
        Stream<String> streamFromValues = Stream.of("a", "b", "c");

        // Creating an infinite stream using Stream.generate
        Stream<Double> randomNumbers = Stream.generate(Math::random).limit(5);
        randomNumbers.forEach(System.out::println);
    }
}

2. Intermediate Operations

Intermediate operations transform a stream into another stream and are lazy, meaning they are not executed until a terminal operation is called.

Common Intermediate Operations:

  • filter(Predicate<T> predicate): Filters elements based on a predicate.
  • map(Function<T, R> mapper): Transforms elements by applying a function.
  • sorted(): Sorts the elements of the stream.
  • distinct(): Removes duplicate elements.

Example:

import java.util.Arrays;
import java.util.List;
import java.util.stream.Collectors;

public class IntermediateOperationsExample {
    public static void main(String[] args) {
        List<String> list = Arrays.asList("a", "b", "c", "a", "b", "c");

        List<String> result = list.stream()
            .filter(s -> !s.equals("b")) // Filter out "b"
            .distinct()                  // Remove duplicates
            .sorted()                    // Sort the elements
            .map(String::toUpperCase)    // Convert to uppercase
            .collect(Collectors.toList());

        System.out.println(result); // Output: [A, C]
    }
}

3. Terminal Operations

Terminal operations produce a result or a side-effect and trigger the stream processing.

Common Terminal Operations:

  • forEach(Consumer<T> action): Performs an action for each element.
  • collect(Collector<T, A, R> collector): Collects the elements into a collection.
  • reduce(BinaryOperator<T> accumulator): Reduces the elements to a single value.
  • count(): Returns the number of elements.
  • anyMatch(Predicate<T> predicate): Returns true if any elements match the predicate.

Example:

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

public class TerminalOperationsExample {
    public static void main(String[] args) {
        List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5);

        // forEach
        numbers.stream()
            .forEach(System.out::println); // Output: 1 2 3 4 5

        // collect
        List<Integer> evenNumbers = numbers.stream()
            .filter(n -> n % 2 == 0)
            .collect(Collectors.toList());
        System.out.println(evenNumbers); // Output: [2, 4]

        // reduce
        Optional<Integer> sum = numbers.stream()
            .reduce(Integer::sum);
        sum.ifPresent(System.out::println); // Output: 15

        // count
        long count = numbers.stream()
            .filter(n -> n > 3)
            .count();
        System.out.println(count); // Output: 2

        // anyMatch
        boolean hasEven = numbers.stream()
            .anyMatch(n -> n % 2 == 0);
        System.out.println(hasEven); // Output: true
    }
}

Benefits of the Stream API

  1. Declarative Code: Allows you to write more readable and expressive code by focusing on the logic of the operations rather than the control flow.
  2. Improved Readability: Makes complex operations on collections easier to understand and maintain.
  3. Parallelism: Simplifies parallel processing of collections with minimal changes to the code using parallelStream.
  4. Efficiency: Provides efficient data processing through internal iteration and lazy evaluation.

Conclusion

The Stream API in Java is a powerful tool for working with collections and other sequences of data. It provides a functional approach to processing data, making the code more concise, readable, and maintainable. By using streams, you can perform complex data manipulations with ease, leveraging the power of functional programming in Java.

Recent job openings