ProMind
SearchFor TeachersFor Parents
ProMind
Privacy PolicyTerms of ServiceRefund Policy

© 2025 DataGrid Softwares LLP. All rights reserved.

    Lambdas and Streams

    Flashcards for topic Lambdas and Streams

    Intermediate61 cardsGeneral

    Preview Cards

    Card 1

    Front

    What fundamental problem does the lambda expression solve compared to anonymous classes in Java, and how does the syntax differ?

    Back

    Lambdas solve the verbosity problem of anonymous classes, making functional programming practical in Java:

    • Key difference: Lambdas are much more concise, eliminating boilerplate code
    • Type inference: Parameter types are automatically deduced from context
    • Syntax comparison:
      // 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.

    Card 2

    Front

    What are the six basic functional interfaces in java.util.function, their purpose, and variants for handling primitives?

    Back

    The six basic functional interfaces in Java 8 form the foundation of the functional programming API:

    1. UnaryOperator<T>

      • Purpose: Takes a T and returns a T (same type input/output)
      • Method: T apply(T t)
      • Example: String::toLowerCase
      • Primitive variants: IntUnaryOperator, LongUnaryOperator, DoubleUnaryOperator
    2. BinaryOperator<T>

      • Purpose: Takes two Ts and returns a T
      • Method: T apply(T t1, T t2)
      • Example: BigInteger::add
      • Primitive variants: IntBinaryOperator, LongBinaryOperator, DoubleBinaryOperator
    3. Predicate<T>

      • Purpose: Takes a T and returns a boolean
      • Method: boolean test(T t)
      • Example: Collection::isEmpty
      • Primitive variants: IntPredicate, LongPredicate, DoublePredicate
    4. Function<T,R>

      • Purpose: Takes a T and returns an R (different input/output types)
      • Method: R apply(T t)
      • Example: Arrays::asList
      • Primitive variants include:
        • Type to primitive: ToIntFunction<T>, ToLongFunction<T>, ToDoubleFunction<T>
        • Primitive to type: IntFunction<R>, LongFunction<R>, DoubleFunction<R>
        • Primitive to primitive: IntToLongFunction, IntToDoubleFunction, etc.
    5. Supplier<T>

      • Purpose: Takes no arguments and returns a T
      • Method: T get()
      • Example: Instant::now
      • Primitive variants: IntSupplier, LongSupplier, DoubleSupplier, BooleanSupplier
    6. Consumer<T>

      • Purpose: Takes a T and returns nothing (void)
      • Method: void accept(T t)
      • Example: System.out::println
      • Primitive variants: 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.

    Card 3

    Front

    What crucial performance issue arises when using basic functional interfaces with primitive types instead of specialized primitive functional interfaces?

    Back

    Using basic functional interfaces with boxed primitives instead of primitive-specific functional interfaces can have severe performance consequences:

    • Causes unnecessary autoboxing and unboxing operations
    • Creates excessive object allocation for bulk operations
    • Can lead to significant memory overhead
    • May trigger more frequent garbage collection
    • Can be "deadly" for performance in computation-intensive code

    This violates the advice to "prefer primitive types to boxed primitives" for performance-critical operations.

    Card 4

    Front

    What issue exists with the seemingly intuitive code "Hello world!".chars().forEach(System.out::print) and how should it be fixed?

    Back

    Issue: This code prints 721011081081113211911111410810033 (the integer values) instead of "Hello world!" because:

    • The chars() method returns a stream of int values (Unicode code points), not char values
    • When using System.out::print, the int overload is invoked, not the char overload

    Fix 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.

    Card 5

    Front

    What should you consider when choosing between Stream.iterate() and traditional iteration, using the example of generating Mersenne primes?

    Back

    When choosing between Stream.iterate() and traditional iteration:

    Stream.iterate() strengths (as shown in Mersenne primes example):

    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));
    • Well-suited for infinite sequences with clear progression rules
    • Enables declarative expression of the algorithm
    • Naturally supports laziness (computes only what's needed)
    • Makes the transformation pipeline explicit and readable
    • Handles concerns separately (generation, transformation, filtering, limiting)

    Traditional iteration might be better when:

    • You need mutable accumulation state during iteration
    • Control flow (break/continue) would simplify the algorithm
    • Multiple interdependent values need to be tracked together
    • Performance is critical and iteration overhead is a concern
    • The algorithm doesn't fit neatly into the map/filter/reduce paradigm

    Choose based on which approach makes your specific algorithm most readable and maintainable.

    Card 6

    Front

    What is wrong with using the forEach terminal operation as the main computation method in streams, and what is the correct approach?

    Back

    Problems with using forEach for main computation:

    • It's explicitly iterative, not functional
    • Not amenable to parallelization
    • Encourages side effects and mutable state
    • Provides no benefits over traditional iteration
    • Makes code longer, harder to read, and less maintainable

    Correct approach:

    • Use forEach only to report/present results of computations
    • Perform actual computation using proper stream operations and collectors
    • Maintain side-effect-free functions

    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.

    Card 7

    Front

    What are the collectors that should never be used directly on a Stream (only as downstream collectors), and why?

    Back

    Collectors that should only be used as downstream collectors:

    1. counting() - Use Stream.count() directly instead
    2. summingType methods (e.g., summingInt, summingLong, summingDouble)
    3. averagingType methods
    4. summarizingType methods
    5. All overloadings of reducing()
    6. filtering(), mapping(), flatMapping(), and collectingAndThen()

    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.

    Card 8

    Front

    How do you use the maxBy and minBy collectors? What alternatives exist for finding maximum and minimum elements in streams?

    Back

    Using maxBy and minBy collectors:

    1. As downstream 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))));
    1. Direct alternatives on Stream:
    // 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:

    • Stream's max/min methods return an Optional of the extreme element
    • Collectors' maxBy/minBy are designed for use in groupingBy and other collectors
    • maxBy/minBy always produce an Optional, even as a downstream collector

    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.

    Card 9

    Front

    What is the "locality of reference" concept and why is it critical for effective parallelization of Java streams?

    Back

    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:

    1. Memory access efficiency:

      • When elements are stored contiguously, processors can load chunks into cache more efficiently
      • Reduces cache misses, which are extremely expensive in terms of performance
      • Without good locality, threads spend significant time idle, waiting for data from main memory
    2. Impact on parallelization:

      • Modern CPUs are much faster than memory access
      • Memory access becomes the bottleneck in data-intensive operations
      • Good locality dramatically reduces thread starvation
    3. Data structures with best locality:

      • Primitive arrays (best): Data stored in continuous memory blocks
      • ArrayList: Elements stored in backing array
      • Arrays.asList results: Backed by original array
      • Linked data structures (poor): Nodes scattered throughout memory
    4. Practical implications:

      • Parallelizing operations on ArrayList is more efficient than LinkedList
      • Operations on primitive arrays generally parallelize most effectively
      • Collections with poor locality may see minimal or negative benefits from parallelization
    5. Optimizing for locality:

      • Prefer array-backed collections when parallelization is planned
      • Consider reorganizing data for better spatial locality before parallelizing
      • Be especially cautious with parallelizing operations on linked data structures

    Good locality of reference can often be the difference between significant speedups and disappointing slowdowns when parallelizing streams.

    Card 10

    Front

    What specific implementation details make the prime-counting example (pi(n)) particularly well-suited for parallelization?

    Back

    Implementation details making the prime-counting example ideal for parallelization:

    1. Computationally intensive core operation: isProbablePrime(50) is CPU-bound and expensive

      • Each primality test requires significant computation
      • No I/O or external resource bottlenecks
    2. Perfect independence: Testing whether one number is prime has no effect on other numbers

      • No shared state between operations
      • Results don't depend on processing order
    3. Naturally partitionable input: LongStream.rangeClosed(2, n) divides perfectly into ranges

      • Each core can work on a distinct range of numbers
    4. Simple reduction: count() operation combines results with minimal overhead

      • Just adding counts from each partition
    5. Uniform workload distribution: While primality testing gets more expensive for larger numbers, the range is wide enough to average out

    6. 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.