Flashcards for topic Lambdas and Streams
What fundamental problem does the lambda expression solve compared to anonymous classes in Java, and how does the syntax differ?
Lambdas solve the verbosity problem of anonymous classes, making functional programming practical in Java:
// Anonymous class (verbose) Collections.sort(words, new Comparator<String>() { public int compare(String s1, String s2) { return Integer.compare(s1.length(), s2.length()); } }); // Lambda (concise) Collections.sort(words, (s1, s2) -> Integer.compare(s1.length(), s2.length()));
Best practice: Omit parameter types in lambdas unless their presence makes your program clearer.
What are the six basic functional interfaces in java.util.function
, their purpose, and variants for handling primitives?
The six basic functional interfaces in Java 8 form the foundation of the functional programming API:
UnaryOperator<T>
T apply(T t)
String::toLowerCase
IntUnaryOperator
, LongUnaryOperator
, DoubleUnaryOperator
BinaryOperator<T>
T apply(T t1, T t2)
BigInteger::add
IntBinaryOperator
, LongBinaryOperator
, DoubleBinaryOperator
Predicate<T>
boolean test(T t)
Collection::isEmpty
IntPredicate
, LongPredicate
, DoublePredicate
Function<T,R>
R apply(T t)
Arrays::asList
ToIntFunction<T>
, ToLongFunction<T>
, ToDoubleFunction<T>
IntFunction<R>
, LongFunction<R>
, DoubleFunction<R>
IntToLongFunction
, IntToDoubleFunction
, etc.Supplier<T>
T get()
Instant::now
IntSupplier
, LongSupplier
, DoubleSupplier
, BooleanSupplier
Consumer<T>
void accept(T t)
System.out::println
IntConsumer
, LongConsumer
, DoubleConsumer
Key point: There are 43 interfaces in java.util.function
, but most are variations of these six basic interfaces. When designing APIs that accept functional objects, prefer these standard interfaces over creating custom functional interfaces.
What crucial performance issue arises when using basic functional interfaces with primitive types instead of specialized primitive functional interfaces?
Using basic functional interfaces with boxed primitives instead of primitive-specific functional interfaces can have severe performance consequences:
This violates the advice to "prefer primitive types to boxed primitives" for performance-critical operations.
What issue exists with the seemingly intuitive code "Hello world!".chars().forEach(System.out::print)
and how should it be fixed?
Issue: This code prints 721011081081113211911111410810033
(the integer values) instead of "Hello world!" because:
chars()
method returns a stream of int
values (Unicode code points), not char
valuesSystem.out::print
, the int
overload is invoked, not the char
overloadFix option 1: Use a cast in a lambda expression to force correct method invocation:
"Hello world!".chars().forEach(x -> System.out.print((char) x));
Fix option 2 (preferred): Avoid using streams for char
processing altogether due to Java's lack of proper support for primitive char
streams.
What should you consider when choosing between Stream.iterate()
and traditional iteration, using the example of generating Mersenne primes?
When choosing between Stream.iterate()
and traditional iteration:
static Stream<BigInteger> primes() { return Stream.iterate(TWO, BigInteger::nextProbablePrime); } // Using the stream to find Mersenne primes primes().map(p -> TWO.pow(p.intValueExact()).subtract(ONE)) .filter(mersenne -> mersenne.isProbablePrime(50)) .limit(20) .forEach(mp -> System.out.println(mp.bitLength() + ": " + mp));
Choose based on which approach makes your specific algorithm most readable and maintainable.
What is wrong with using the forEach terminal operation as the main computation method in streams, and what is the correct approach?
Problems with using forEach for main computation:
Correct approach:
Example of improper use:
// BAD: Using streams API but not the paradigm Map<String, Long> freq = new HashMap<>(); words.forEach(word -> { freq.merge(word.toLowerCase(), 1L, Long::sum); // Side effect! });
Correct approach:
// GOOD: Proper use of streams Map<String, Long> freq = words .collect(groupingBy(String::toLowerCase, counting()));
Occasionally, forEach can be used for other purposes like adding stream results to a pre-existing collection.
What are the collectors that should never be used directly on a Stream (only as downstream collectors), and why?
Collectors that should only be used as downstream collectors:
Reason: These collectors duplicate functionality that's already available directly on Stream. Using them as top-level collectors would be redundant and potentially less efficient.
Example of incorrect usage:
// Incorrect: Using counting() directly long count = stream.collect(counting());
Correct alternatives:
// Correct: Using Stream.count() directly long count = stream.count(); // Correct: Using counting() as a downstream collector Map<Category, Long> countsByCategory = stream .collect(groupingBy(Item::getCategory, counting()));
These methods exist primarily to support the collector API's role in downstream operations, allowing "mini-streams" within the larger collection operation.
How do you use the maxBy and minBy collectors? What alternatives exist for finding maximum and minimum elements in streams?
Using maxBy and minBy collectors:
// Find the best-selling album for each artist Map<Artist, Album> topHits = albums.collect( toMap(Album::artist, a->a, maxBy(comparing(Album::sales)))); // Find the cheapest product in each category Map<Category, Product> cheapestByCategory = products.collect( groupingBy(Product::getCategory, minBy(comparing(Product::getPrice))));
// Using max() on the stream directly Optional<Album> bestSeller = albums.max(comparing(Album::sales)); // Using min() on the stream directly Optional<Product> cheapest = products.min(comparing(Product::getPrice));
Key differences:
Implementation detail:
// How maxBy is used with a BinaryOperator BinaryOperator<Album> findBestSeller = BinaryOperator.maxBy(comparing(Album::sales)); // This can be used in reduction operations Album bestSeller = albums.reduce(first, findBestSeller);
Note: maxBy and minBy are statically imported from BinaryOperator, while max and min are methods on Stream. Always choose the most direct approach for your context.
What is the "locality of reference" concept and why is it critical for effective parallelization of Java streams?
Locality of reference:
Definition: The property where data elements that are accessed together are also stored physically close together in memory.
Why it's critical for parallel streams:
Memory access efficiency:
Impact on parallelization:
Data structures with best locality:
Practical implications:
Optimizing for locality:
Good locality of reference can often be the difference between significant speedups and disappointing slowdowns when parallelizing streams.
What specific implementation details make the prime-counting example (pi(n)
) particularly well-suited for parallelization?
Implementation details making the prime-counting example ideal for parallelization:
Computationally intensive core operation: isProbablePrime(50)
is CPU-bound and expensive
Perfect independence: Testing whether one number is prime has no effect on other numbers
Naturally partitionable input: LongStream.rangeClosed(2, n)
divides perfectly into ranges
Simple reduction: count()
operation combines results with minimal overhead
Uniform workload distribution: While primality testing gets more expensive for larger numbers, the range is wide enough to average out
No ordering requirements: Order of processing doesn't matter when counting primes
Showing 10 of 61 cards. Add this deck to your collection to see all cards.