OCP Streams and Lambda Expressions Questions and Answers 1 — Questions and Answers
Question 1: Which of the following best describes the core difference between intermediate and terminal operations in the Java Stream API?
- Intermediate operations are optional, while every stream pipeline must end with a terminal operation.
- Intermediate operations are lazy and return a new stream, while terminal operations are eager and produce a result or side-effect. (Correct answer)
- Intermediate operations, like `map()` and `filter()`, can only be chained once, whereas terminal operations can be chained multiple times.
- Terminal operations can only be applied to parallel streams, while intermediate operations work for both sequential and parallel streams.
Correct answer: Intermediate operations are lazy and return a new stream, while terminal operations are eager and produce a result or side-effect.
The key distinction lies in their execution behavior. Intermediate operations (e.g., `filter`, `map`, `sorted`) are lazy; they don't execute until a terminal operation is invoked. They build up a pipeline of transformations, each returning a new `Stream`. Terminal operations (e.g., `forEach`, `collect`, `reduce`) are eager, triggering the processing of the entire pipeline and producing a final result, such as a collection, a single value, or a side-effect. Once a terminal operation is called, the stream is considered consumed and cannot be reused.
Question 2: A developer is working with a `Stream<String>` and wants to produce a `Map<Integer, List<String>>` where the keys are the lengths of the strings and the values are lists of strings of that length. Which `Collector` should be used?
- Collectors.toMap(String::length, s -> s)
- Collectors.partitioningBy(s -> s.length() > 0)
- Collectors.groupingBy(String::length) (Correct answer)
- Collectors.toMap(s -> s.length(), s -> List.of(s), (list1, list2) -> list1)
Correct answer: Collectors.groupingBy(String::length)
The `Collectors.groupingBy()` collector is specifically designed for this purpose. It takes a classifier function (in this case, `String::length`) and groups the elements of the stream into a `Map`. The keys of the map are the results of applying the classifier function, and the values are `List`s containing the elements that mapped to that key. `Collectors.partitioningBy` only separates elements into two groups based on a `Predicate`. The `toMap` collectors would throw an `IllegalStateException` on duplicate keys (strings with the same length) without a merge function, and the provided merge function in the incorrect option is flawed.
Question 3: Given the following code snippet, what is the output? ```java List<String> list = Arrays.asList("a", "b", "c"); Optional<String> result = list.stream() .filter(s -> s.equals("d")) .findFirst(); System.out.println(result.orElse("Not Found")); ```
- An empty Optional
- null
- Not Found (Correct answer)
- A NoSuchElementException is thrown
Correct answer: Not Found
The stream is filtered for the string "d", which does not exist in the list. Therefore, the `filter` operation results in an empty stream. The `findFirst()` terminal operation on an empty stream returns an empty `Optional`. The `orElse("Not Found")` method is then called on this empty `Optional`, which causes it to return the provided default value, "Not Found". A `NoSuchElementException` would only be thrown if `get()` were called on an empty `Optional` without checking for presence.
Question 4: Why would a developer choose to use a primitive stream like `IntStream` over a `Stream<Integer>`?
- To gain access to more powerful terminal operations like `collect()`.
- Because `IntStream` can handle a larger range of integer values than `Stream<Integer>`.
- To improve performance by avoiding the overhead of boxing and unboxing primitive values into wrapper objects. (Correct answer)
- Because primitive streams are the only way to perform parallel processing on numeric data.
Correct answer: To improve performance by avoiding the overhead of boxing and unboxing primitive values into wrapper objects.
Primitive streams (`IntStream`, `LongStream`, `DoubleStream`) are specialized versions of `Stream` that work directly with primitive data types. This avoids the automatic conversion (boxing) of a primitive (e.g., `int`) into its corresponding wrapper class (e.g., `Integer`) and the reverse process (unboxing). For large datasets, this can lead to significant performance improvements and reduced memory usage because it eliminates the creation of many wrapper objects.
Question 5: Which of the following is a valid and most concise lambda expression for a `java.util.function.Predicate<String>` that tests if a string is empty?
- (String s) -> { return s.isEmpty(); }
- s -> s.isEmpty()
- () -> "".isEmpty()
- String::isEmpty (Correct answer)
Correct answer: String::isEmpty
`String::isEmpty` is a method reference, which is a compact form of a lambda expression used to refer to a method without invoking it. In this case, it refers to the `isEmpty()` method of the `String` class. It is functionally equivalent to the lambda `s -> s.isEmpty()` but is generally preferred for its conciseness and clarity when the lambda simply calls an existing method. The other options are also functionally correct but are not the most concise syntax available.
Question 6: What is the result of the following stream pipeline? ```java long count = Stream.of("apple", "banana", "apricot", "cherry") .filter(s -> s.startsWith("a")) .peek(System.out::println) .count(); ```
- The code will print "apple" and "apricot", and `count` will be 2. (Correct answer)
- The code will not compile because `peek` is a terminal operation.
- The code will print "apple", "banana", "apricot", and "cherry", and `count` will be 4.
- The code will print nothing, and `count` will be 2.
Correct answer: The code will print "apple" and "apricot", and `count` will be 2.
The stream pipeline first filters the elements, keeping only those that start with "a" ("apple", "apricot"). The `peek()` operation is an intermediate operation that performs an action on each element as it passes through the stream; in this case, it prints the element. Since `peek` is an intermediate operation, it doesn't terminate the stream. Finally, the `count()` terminal operation is called, which consumes the stream and returns the number of elements remaining after the filter, which is 2. The `peek` operation will execute for each of those 2 elements.
Which of the following best describes the core difference between intermediate and terminal operations in the Java Stream API?