15 Java Stream API Interview Questions and Answers
Prepare for your Java interview with this guide on Java Stream API, featuring common questions and detailed answers to enhance your understanding.
Prepare for your Java interview with this guide on Java Stream API, featuring common questions and detailed answers to enhance your understanding.
Java Stream API is a powerful feature introduced in Java 8 that allows developers to process collections of objects in a functional programming style. It provides a concise and efficient way to perform operations such as filtering, mapping, and reducing on data sets, making code more readable and maintainable. The Stream API is particularly useful for handling large data sets and parallel processing, which can significantly improve performance.
This article offers a curated selection of interview questions focused on the Java Stream API. By working through these questions and their detailed answers, you will gain a deeper understanding of how to leverage this feature effectively, enhancing your problem-solving skills and technical proficiency for your upcoming interview.
The Java Stream API offers a functional approach to processing sequences of elements, allowing for operations like filtering, mapping, and reducing. This enables complex data processing tasks to be performed in a readable and concise manner.
To filter out even numbers from a list of integers and square each remaining number, use the following stream operations:
import java.util.Arrays; import java.util.List; import java.util.stream.Collectors; public class StreamExample { public static void main(String[] args) { List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5, 6, 7, 8, 9, 10); List<Integer> result = numbers.stream() .filter(n -> n % 2 != 0) .map(n -> n * n) .collect(Collectors.toList()); System.out.println(result); } }
In this example, filter
excludes even numbers, and map
squares the remaining numbers. The collect
method gathers the results into a new list.
To collect all unique words from a list of sentences into a set, use the Stream API to process the sentences, split them into words, and collect the unique words:
import java.util.Arrays; import java.util.List; import java.util.Set; import java.util.stream.Collectors; public class UniqueWordsCollector { public static void main(String[] args) { List<String> sentences = Arrays.asList( "This is a sentence", "This is another sentence", "And this is yet another sentence" ); Set<String> uniqueWords = sentences.stream() .flatMap(sentence -> Arrays.stream(sentence.split(" "))) .collect(Collectors.toSet()); System.out.println(uniqueWords); } }
When a stream operation might return an empty result, use the Optional
class to handle it gracefully. This avoids NullPointerException
and allows for a default value or alternative action.
Example:
import java.util.Arrays; import java.util.List; import java.util.Optional; public class StreamExample { public static void main(String[] args) { List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5); Optional<Integer> maxNumber = numbers.stream() .filter(n -> n > 10) .max(Integer::compareTo); maxNumber.ifPresentOrElse( System.out::println, () -> System.out.println("No value found") ); } }
In this example, the stream filters numbers greater than 10, resulting in an empty stream. The max()
operation returns an Optional<Integer>
, and ifPresentOrElse
is used to print the value if present or “No value found” if not.
To implement a custom collector for concatenating strings with a delimiter, define the supplier, accumulator, combiner, and finisher methods:
import java.util.stream.Collector; import java.util.function.Supplier; import java.util.function.BiConsumer; import java.util.function.BinaryOperator; import java.util.function.Function; import java.util.Set; import java.util.HashSet; import java.util.stream.Stream; public class StringConcatenationCollector implements Collector<String, StringBuilder, String> { private final String delimiter; public StringConcatenationCollector(String delimiter) { this.delimiter = delimiter; } @Override public Supplier<StringBuilder> supplier() { return StringBuilder::new; } @Override public BiConsumer<StringBuilder, String> accumulator() { return (sb, s) -> { if (sb.length() > 0) { sb.append(delimiter); } sb.append(s); }; } @Override public BinaryOperator<StringBuilder> combiner() { return (sb1, sb2) -> { if (sb1.length() > 0 && sb2.length() > 0) { sb1.append(delimiter); } sb1.append(sb2); return sb1; }; } @Override public Function<StringBuilder, String> finisher() { return StringBuilder::toString; } @Override public Set<Characteristics> characteristics() { return new HashSet<>(); } public static void main(String[] args) { Stream<String> stream = Stream.of("apple", "banana", "cherry"); String result = stream.collect(new StringConcatenationCollector(", ")); System.out.println(result); // Output: apple, banana, cherry } }
To generate an infinite stream of random numbers and limit it to 10 elements, use Stream.generate
with limit
:
import java.util.Random; import java.util.stream.Stream; public class RandomNumberStream { public static void main(String[] args) { Random random = new Random(); Stream.generate(random::nextInt) .limit(10) .forEach(System.out::println); } }
In this example, Stream.generate(random::nextInt)
creates an infinite stream of random integers, and limit(10)
truncates it to the first 10 elements.
To calculate the product of all elements in a list of integers, use the reduce
operation:
import java.util.Arrays; import java.util.List; public class StreamProductExample { public static void main(String[] args) { List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5); int product = numbers.stream() .reduce(1, (a, b) -> a * b); System.out.println("Product of all elements: " + product); } }
In this example, reduce
multiplies all elements in the list, with an initial value of 1.
To handle checked exceptions within a stream pipeline, wrap the checked exception in an unchecked exception or use a custom functional interface:
import java.util.Arrays; import java.util.List; import java.util.stream.Collectors; public class StreamExceptionHandling { @FunctionalInterface public interface CheckedFunction<T, R> { R apply(T t) throws Exception; } public static <T, R> java.util.function.Function<T, R> wrap(CheckedFunction<T, R> function) { return t -> { try { return function.apply(t); } catch (Exception e) { throw new RuntimeException(e); } }; } public static void main(String[] args) { List<String> data = Arrays.asList("1", "2", "a", "4"); List<Integer> result = data.stream() .map(wrap(s -> Integer.parseInt(s))) .collect(Collectors.toList()); System.out.println(result); } }
In this example, the wrap
method handles the checked exception thrown by Integer.parseInt
.
The peek
method allows you to perform an action on each element of the stream, useful for debugging:
import java.util.Arrays; import java.util.List; public class StreamPeekExample { public static void main(String[] args) { List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5); numbers.stream() .peek(n -> System.out.println("Original number: " + n)) .map(n -> n * 2) .peek(n -> System.out.println("After map: " + n)) .filter(n -> n > 5) .peek(n -> System.out.println("After filter: " + n)) .forEach(n -> System.out.println("Final number: " + n)); } }
In this example, peek
is used to print elements at different stages of the stream pipeline.
A custom Spliterator can split a stream of integers into chunks of a specified size:
import java.util.Spliterator; import java.util.function.Consumer; public class ChunkSpliterator implements Spliterator<int[]> { private final int[] array; private int currentIndex = 0; private final int chunkSize; public ChunkSpliterator(int[] array, int chunkSize) { this.array = array; this.chunkSize = chunkSize; } @Override public boolean tryAdvance(Consumer<? super int[]> action) { if (currentIndex < array.length) { int end = Math.min(currentIndex + chunkSize, array.length); int[] chunk = new int[end - currentIndex]; System.arraycopy(array, currentIndex, chunk, 0, chunk.length); action.accept(chunk); currentIndex = end; return true; } return false; } @Override public Spliterator<int[]> trySplit() { int remainingSize = array.length - currentIndex; if (remainingSize <= chunkSize) { return null; } int splitSize = remainingSize / 2; int splitEnd = currentIndex + splitSize; ChunkSpliterator split = new ChunkSpliterator(array, chunkSize); split.currentIndex = splitEnd; return split; } @Override public long estimateSize() { return (array.length - currentIndex + chunkSize - 1) / chunkSize; } @Override public int characteristics() { return ORDERED | SIZED | SUBSIZED; } }
The Stream.Builder
class allows for the construction of a stream in a flexible manner:
import java.util.stream.Stream; public class StreamBuilderExample { public static void main(String[] args) { Stream<String> stringStream = Stream.<String>builder() .add("Hello") .add("World") .add("Stream") .add("Builder") .build(); stringStream.forEach(System.out::println); } }
To generate the first 10 Fibonacci numbers using the iterate
method:
import java.util.stream.Stream; public class Fibonacci { public static void main(String[] args) { Stream.iterate(new int[]{0, 1}, fib -> new int[]{fib[1], fib[0] + fib[1]}) .limit(10) .map(fib -> fib[0]) .forEach(System.out::println); } }
In this example, iterate
starts with {0, 1}
and updates the pair to the next Fibonacci numbers.
To optimize a stream operation that filters, maps, and collects data, use parallel streams and ensure efficient chaining of operations:
import java.util.List; import java.util.stream.Collectors; import java.util.stream.IntStream; public class StreamOptimization { public static void main(String[] args) { List<Integer> numbers = IntStream.range(1, 1000000).boxed().collect(Collectors.toList()); List<Integer> result = numbers.parallelStream() .filter(n -> n % 2 == 0) .map(n -> n * 2) .collect(Collectors.toList()); } }
In this example, parallelStream()
allows operations to be executed in parallel, leveraging multiple CPU cores.
Handling null values in a stream pipeline can be done by filtering them out or using default values:
import java.util.Arrays; import java.util.List; import java.util.stream.Collectors; public class NullHandlingExample { public static void main(String[] args) { List<String> items = Arrays.asList("apple", null, "banana", "cherry", null); List<String> filteredItems = items.stream() .filter(item -> item != null) .collect(Collectors.toList()); System.out.println(filteredItems); // Output: [apple, banana, cherry] } }
To combine multiple streams into one, use Stream.concat()
or Stream.of()
:
Example using Stream.concat()
:
import java.util.stream.Stream; public class CombineStreams { public static void main(String[] args) { Stream<String> stream1 = Stream.of("A", "B", "C"); Stream<String> stream2 = Stream.of("D", "E", "F"); Stream<String> combinedStream = Stream.concat(stream1, stream2); combinedStream.forEach(System.out::println); } }
Example using Stream.of()
:
import java.util.stream.Stream; public class CombineStreams { public static void main(String[] args) { Stream<String> stream1 = Stream.of("A", "B", "C"); Stream<String> stream2 = Stream.of("D", "E", "F"); Stream<String> stream3 = Stream.of("G", "H", "I"); Stream<String> combinedStream = Stream.of(stream1, stream2, stream3) .flatMap(s -> s); combinedStream.forEach(System.out::println); } }
To optimize stream performance, consider these techniques:
– Use parallel streams judiciously to leverage multiple CPU cores.
– Minimize intermediate operations to reduce overhead.
– Use primitive streams to avoid boxing and unboxing.
– Avoid stateful operations like distinct
, sorted
, and limit
when possible.
– Utilize short-circuiting operations like findFirst
, findAny
, and anyMatch
.
– Reuse streams by creating a new one for each operation to avoid overhead.