Java Lambda Expressions & Streams (Java 8+)
Write clean functional Java. Master lambda shorthand, stream filters, collectors, map-reductions, and parallel pipelines.
Downloaded from PrepIQ (https://prepiq.online)
Q1. What is a lambda expression in Java and what problem does it solve?
A lambda expression is a concise way to represent an anonymous function - a block of code that can be passed around as a value. It solves the verbosity of anonymous inner classes for simple, single-method implementations.
// BEFORE Java 8 - verbose anonymous inner class
Runnable oldWay = new Runnable() {
@Override
public void run() {
System.out.println('Running');
}
};
// WITH lambda (Java 8+) - concise
Runnable newWay = () -> System.out.println('Running');
// Lambda syntax variations
Comparator<Integer> comp1 = (a, b) -> a - b; // expression body
Comparator<Integer> comp2 = (a, b) -> { return a - b; }; // block body
Runnable r = () -> System.out.println('No parameters'); // no params
Function<Integer, Integer> square = x -> x * x; // single param, no parens needed
Function<Integer, Integer> square2 = (x) -> x * x; // parens optional here
// Using lambdas with Collections
List<String> names = new ArrayList<>(List.of('Charlie', 'Alice', 'Bob'));
// BEFORE - anonymous class for sorting
Collections.sort(names, new Comparator<String>() {
public int compare(String a, String b) {
return a.compareTo(b);
}
});
// WITH lambda
Collections.sort(names, (a, b) -> a.compareTo(b));
// Even shorter with method reference
Collections.sort(names, String::compareTo);
Q2. What is a functional interface in Java?
A functional interface is an interface with EXACTLY ONE abstract method, making it eligible to be implemented using a lambda expression. The @FunctionalInterface annotation enforces this at compile time.
@FunctionalInterface
interface Calculator {
int calculate(int a, int b); // exactly one abstract method
// default and static methods are allowed, don't count against the 'one method' rule
default void printInfo() {
System.out.println('Calculator interface');
}
}
Calculator add = (a, b) -> a + b;
Calculator multiply = (a, b) -> a * b;
System.out.println(add.calculate(3, 4)); // 7
System.out.println(multiply.calculate(3, 4)); // 12
// @FunctionalInterface catches mistakes at compile time
// @FunctionalInterface
// interface Broken {
// void method1();
// void method2(); // COMPILE ERROR - more than one abstract method
// }
// Java's built-in functional interfaces (java.util.function package)
Function<Integer, Integer> square = x -> x * x; // takes T, returns R
Predicate<Integer> isEven = x -> x % 2 == 0; // takes T, returns boolean
Consumer<String> printer = s -> System.out.println(s); // takes T, returns nothing
Supplier<String> greeting = () -> 'Hello'; // takes nothing, returns T
BiFunction<Integer, Integer, Integer> add2 = (a, b) -> a + b; // two inputs, one output
System.out.println(square.apply(5)); // 25
System.out.println(isEven.test(4)); // true
printer.accept('Hello World'); // Hello World
System.out.println(greeting.get()); // Hello
Q3. What is the Stream API in Java and how does it differ from Collections?
| Aspect | Collection | Stream |
|---|---|---|
| Storage | Stores data in memory | No storage - computes elements on demand |
| Mutability | Can add/remove elements | Doesn't modify the source |
| Traversal | Can iterate multiple times | Consumed once, single-use |
| Evaluation | Eager - all operations happen immediately | Lazy - intermediate ops run only when a terminal op is invoked |
import java.util.stream.*;
List<String> names = List.of('Charlie', 'Alice', 'Bob', 'Anna');
// A stream pipeline: source -> intermediate operations -> terminal operation
List<String> result = names.stream() // create stream from collection
.filter(name -> name.startsWith('A')) // intermediate: lazy, returns a stream
.map(String::toUpperCase) // intermediate: lazy, returns a stream
.sorted() // intermediate: lazy
.collect(Collectors.toList()); // terminal: triggers execution
System.out.println(result); // [ALICE, ANNA]
// Streams are single-use - reusing a consumed stream throws an exception
Stream<String> stream = names.stream();
stream.forEach(System.out::println);
// stream.forEach(System.out::println); // IllegalStateException: stream has already been operated upon
// Lazy evaluation demonstration
Stream<String> lazyStream = names.stream()
.filter(name -> {
System.out.println('Filtering: ' + name);
return name.length() > 3;
});
// Nothing printed yet - filter() hasn't actually run because there's
// no terminal operation yet
long count = lazyStream.count(); // NOW the filter actually executes
Q4. What is the difference between map() and flatMap() in Java Streams?
| Method | Transforms to | Use case |
|---|---|---|
| map() | Stream<R> - one output per input, may be nested | Simple 1-to-1 transformation |
| flatMap() | Stream<R> - flattens nested streams into one | When each input produces MULTIPLE outputs (or a stream of them) |
import java.util.stream.*;
// map() - simple 1-to-1 transformation
List<String> words = List.of('hello', 'world');
List<Integer> lengths = words.stream()
.map(String::length)
.collect(Collectors.toList());
System.out.println(lengths); // [5, 5]
// PROBLEM: map() with a function returning a Stream creates NESTED streams
List<List<Integer>> nested = words.stream()
.map(word -> word.chars().boxed().collect(Collectors.toList()))
.collect(Collectors.toList());
// Result: List<List<Integer>> - a list of lists, not flattened
// flatMap() - flattens the nested structure into a single stream
List<String> sentences = List.of('Hello World', 'How are you');
List<String> allWords = sentences.stream()
.flatMap(sentence -> Arrays.stream(sentence.split(' '))) // each sentence -> multiple words
.collect(Collectors.toList());
System.out.println(allWords); // [Hello, World, How, are, you]
// Practical example: flattening a list of lists
List<List<Integer>> listOfLists = List.of(
List.of(1, 2, 3),
List.of(4, 5),
List.of(6)
);
List<Integer> flatList = listOfLists.stream()
.flatMap(List::stream) // each inner list becomes a stream, all merged into one
.collect(Collectors.toList());
System.out.println(flatList); // [1, 2, 3, 4, 5, 6]
// Rule of thumb: use flatMap() whenever your mapping function
// itself returns a Stream/Collection that needs to be merged, not nested
Q5. What are common Stream terminal operations like collect(), reduce(), and forEach()?
Terminal operations trigger the actual execution of a stream pipeline and produce a result (or side effect), after which the stream cannot be reused.
import java.util.stream.*;
List<Integer> numbers = List.of(1, 2, 3, 4, 5);
// collect() - accumulates elements into a collection or other structure
List<Integer> evenList = numbers.stream()
.filter(n -> n % 2 == 0)
.collect(Collectors.toList());
System.out.println(evenList); // [2, 4]
Set<Integer> evenSet = numbers.stream()
.filter(n -> n % 2 == 0)
.collect(Collectors.toSet());
String joined = numbers.stream()
.map(String::valueOf)
.collect(Collectors.joining(', ', '[', ']'));
System.out.println(joined); // [1, 2, 3, 4, 5]
// reduce() - combines elements into a single value
int sum = numbers.stream()
.reduce(0, (a, b) -> a + b); // identity value + combining function
System.out.println(sum); // 15
Optional<Integer> max = numbers.stream()
.reduce((a, b) -> a > b ? a : b); // no identity - returns Optional
System.out.println(max.get()); // 5
// forEach() - performs a side-effecting action per element, returns nothing
numbers.stream().forEach(n -> System.out.println('Number: ' + n));
// Other useful terminal operations
long count = numbers.stream().filter(n -> n > 2).count(); // 3
boolean anyEven = numbers.stream().anyMatch(n -> n % 2 == 0); // true
boolean allPositive = numbers.stream().allMatch(n -> n > 0); // true
Optional<Integer> first = numbers.stream().findFirst(); // Optional[1]
// Collectors.groupingBy - grouping elements by a classifier function
Map<Boolean, List<Integer>> groups = numbers.stream()
.collect(Collectors.groupingBy(n -> n % 2 == 0));
System.out.println(groups); // {false=[1, 3, 5], true=[2, 4]}
Q6. What are method references in Java, and what are their different types?
Method references are a shorthand for lambda expressions that simply call an existing method, using the :: syntax for extra readability.
| Type | Syntax | Example |
|---|---|---|
| Static method | ClassName::staticMethod | Integer::parseInt |
| Instance method of a particular object | instance::instanceMethod | str::toUpperCase (where str is a variable) |
| Instance method of an arbitrary object | ClassName::instanceMethod | String::toUpperCase |
| Constructor | ClassName::new | ArrayList::new |
import java.util.function.*;
import java.util.stream.*;
// Static method reference
Function<String, Integer> parser1 = s -> Integer.parseInt(s); // lambda
Function<String, Integer> parser2 = Integer::parseInt; // method reference - same effect
System.out.println(parser2.apply('42')); // 42
// Instance method reference on a PARTICULAR object
String greeting = 'Hello World';
Supplier<String> upper1 = () -> greeting.toUpperCase(); // lambda
Supplier<String> upper2 = greeting::toUpperCase; // method reference
System.out.println(upper2.get()); // HELLO WORLD
// Instance method reference on an ARBITRARY object of a type
// (the object itself becomes the first argument)
List<String> names = List.of('charlie', 'alice', 'bob');
List<String> upperNames1 = names.stream()
.map(s -> s.toUpperCase()) // lambda
.collect(Collectors.toList());
List<String> upperNames2 = names.stream()
.map(String::toUpperCase) // method reference - equivalent
.collect(Collectors.toList());
// Constructor reference
Supplier<ArrayList<String>> listCreator1 = () -> new ArrayList<>(); // lambda
Supplier<ArrayList<String>> listCreator2 = ArrayList::new; // method reference
Function<String, StringBuilder> sbCreator = StringBuilder::new;
StringBuilder sb = sbCreator.apply('Hello');
// Method references with sort/forEach
names.forEach(System.out::println); // instead of s -> System.out.println(s)
Q7. What is Optional in Java and how does it help avoid NullPointerException?
Optional<T> is a container object that may or may not hold a non-null value, forcing callers to explicitly handle the 'no value' case instead of accidentally dereferencing null.
import java.util.Optional;
// WITHOUT Optional - null checks scattered everywhere, easy to forget
public User findUserById(int id) {
return null; // caller might forget to check for null
}
User user = findUserById(5);
// user.getName(); // NullPointerException if findUserById returned null
// WITH Optional - the return type itself signals 'might be absent'
public Optional<User> findUserByIdSafe(int id) {
User user = database.lookup(id);
return Optional.ofNullable(user); // wraps null safely
}
Optional<User> result = findUserByIdSafe(5);
// Checking presence explicitly
if (result.isPresent()) {
System.out.println(result.get().getName());
}
// Better: functional-style handling, no explicit null checks needed
result.ifPresent(u -> System.out.println(u.getName()));
String name = result.map(User::getName).orElse('Unknown');
User defaultUser = result.orElseGet(() -> new User('Guest'));
User required = result.orElseThrow(() -> new RuntimeException('User not found'));
// Chaining operations safely
Optional<String> upperName = result
.map(User::getName)
.filter(n -> !n.isEmpty())
.map(String::toUpperCase);
// Creating Optionals
Optional<String> empty = Optional.empty();
Optional<String> present = Optional.of('Hello'); // throws if value is null
Optional<String> maybeNull = Optional.ofNullable(possiblyNullValue);
// Anti-pattern: don't just call .get() without checking - defeats the purpose
// String bad = result.get(); // still throws NoSuchElementException if empty!
Q8. What is the difference between intermediate and terminal operations in Java Streams?
| Type | Returns | Execution | Examples |
|---|---|---|---|
| Intermediate | Another Stream | Lazy - not executed until a terminal op is called | filter, map, sorted, distinct, limit |
| Terminal | A non-stream result or void | Eager - triggers the whole pipeline to run | collect, forEach, reduce, count, anyMatch |
import java.util.stream.*;
List<Integer> numbers = List.of(5, 3, 8, 1, 9, 2);
// Chaining multiple intermediate operations - nothing executes yet
Stream<Integer> pipeline = numbers.stream()
.filter(n -> n > 2) // intermediate
.map(n -> n * 2) // intermediate
.sorted(); // intermediate
System.out.println('Pipeline built, nothing executed yet');
// Only when a TERMINAL operation is called does the pipeline actually run
List<Integer> result = pipeline.collect(Collectors.toList()); // terminal - triggers execution
System.out.println(result); // [6, 10, 16, 18]
// A stream can only have ONE terminal operation - after that it's consumed
// pipeline.count(); // IllegalStateException - stream already consumed
// Short-circuiting operations - some terminals stop early once satisfied
boolean hasLarge = numbers.stream()
.peek(n -> System.out.println('Checking: ' + n))
.anyMatch(n -> n > 8);
// Stops as SOON as it finds a match - doesn't process every remaining element
// limit() - an intermediate operation that's also short-circuiting
List<Integer> firstThree = numbers.stream()
.filter(n -> n > 0)
.limit(3) // stops the pipeline early once 3 elements pass
.collect(Collectors.toList());
// Why laziness matters for performance: filter+map+limit(3) on a huge
// stream only processes as many elements as needed to satisfy limit(3),
// not the entire source collection
Q9. How do you use parallel streams in Java, and when should you avoid them?
parallelStream() splits the source data across multiple threads (using the common ForkJoinPool) to process elements concurrently, but it's only beneficial under specific conditions.
import java.util.stream.*;
List<Integer> numbers = IntStream.rangeClosed(1, 1_000_000)
.boxed()
.collect(Collectors.toList());
// Sequential stream - single thread
long sum1 = numbers.stream()
.mapToLong(Integer::longValue)
.sum();
// Parallel stream - splits work across multiple threads
long sum2 = numbers.parallelStream()
.mapToLong(Integer::longValue)
.sum();
// For large, CPU-bound, independent computations, this can be faster
// DANGER: parallel streams with STATEFUL or ORDER-DEPENDENT operations
List<Integer> results = Collections.synchronizedList(new ArrayList<>());
numbers.parallelStream().forEach(results::add);
// Works but order is NOT guaranteed - use forEachOrdered() if order matters
// DANGER: shared mutable state without synchronization causes race conditions
int[] counter = {0};
numbers.parallelStream().forEach(n -> counter[0]++); // UNSAFE - race condition!
System.out.println(counter[0]); // often less than 1,000,000, unpredictable
// When to AVOID parallel streams:
// - Small datasets (thread coordination overhead exceeds any benefit)
// - I/O-bound operations (parallel streams are meant for CPU-bound work)
// - Operations with side effects on shared mutable state
// - When maintaining element order matters and isn't handled explicitly
// When parallel streams HELP:
// - Large datasets with independent, CPU-intensive per-element work
// - Purely functional operations with no shared mutable state
long primeCount = numbers.parallelStream()
.filter(n -> isPrime(n)) // CPU-intensive, independent per element
.count();
Q10. What is the difference between Collectors.toList() and Stream.toList() in modern Java?
| Method | Available since | Returned list mutability |
|---|---|---|
| collect(Collectors.toList()) | Java 8 | Typically mutable (ArrayList), but not officially guaranteed |
| .toList() | Java 16+ | Unmodifiable - guaranteed immutable |
import java.util.stream.*;
List<String> names = List.of('Charlie', 'Alice', 'Bob');
// Java 8 style - via Collectors
List<String> result1 = names.stream()
.filter(n -> n.length() > 3)
.collect(Collectors.toList());
result1.add('Extra'); // usually works - typically returns a mutable ArrayList
// Java 16+ shorthand - more concise
List<String> result2 = names.stream()
.filter(n -> n.length() > 3)
.toList();
// result2.add('Extra'); // UnsupportedOperationException - immutable by design
// Also useful in modern Java: Collectors.toUnmodifiableList() (Java 10+)
// gives the SAME guarantee as .toList() but with more explicit intent
List<String> result3 = names.stream()
.collect(Collectors.toUnmodifiableList());
// Practical implication: if you build a list with .toList() and later
// need to modify it, wrap it explicitly:
List<String> mutableCopy = new ArrayList<>(result2);
mutableCopy.add('Extra'); // now works
// Choose .toList() for concise, safe-by-default code in Java 16+ projects;
// use Collectors.toList() when you specifically need a mutable result
// or need to target compatibility with Java 8-15
Java Lambda Expressions & Streams (Java 8+)
Write clean functional Java. Master lambda shorthand, stream filters, collectors, map-reductions, and parallel pipelines.
What is a lambda expression in Java and what problem does it solve?
A lambda expression is a concise way to represent an anonymous function - a block of code that can be passed a...
What is a functional interface in Java?
A functional interface is an interface with EXACTLY ONE abstract method, making it eligible to be implemented...
What is the Stream API in Java and how does it differ from Collections?
AspectCollectionStreamStorageStores data in memoryNo storage - computes elements on demandMutabilityCan add/re...
What is the difference between map() and flatMap() in Java Streams?
MethodTransforms toUse casemap()Stream<R> - one output per input, may be nestedSimple 1-to-1 transformationfla...
What are common Stream terminal operations like collect(), reduce(), and forEach()?
Terminal operations trigger the actual execution of a stream pipeline and produce a result (or side effect), a...
What are method references in Java, and what are their different types?
Method references are a shorthand for lambda expressions that simply call an existing method, using the :: syn...
What is Optional in Java and how does it help avoid NullPointerException?
Optional<T> is a container object that may or may not hold a non-null value, forcing callers to explicitly han...
What is the difference between intermediate and terminal operations in Java Streams?
TypeReturnsExecutionExamplesIntermediateAnother StreamLazy - not executed until a terminal op is calledfilter,...
How do you use parallel streams in Java, and when should you avoid them?
parallelStream() splits the source data across multiple threads (using the common ForkJoinPool) to process ele...
What is the difference between Collectors.toList() and Stream.toList() in modern Java?
MethodAvailable sinceReturned list mutabilitycollect(Collectors.toList())Java 8Typically mutable (ArrayList),...