Lambdas, streams and the closing questions
Lambdas and method references, the stream pipeline and why it is lazy, Optional, and the garbage collection and memory questions that end a Java round.
Modern Java is where a candidate can show they have written code since college taught them Java 8. Then the last few minutes of a round are usually memory and garbage collection, so both are here.
Lambdas
A lambda is an instance of a functional interface — an interface with exactly one abstract method (lesson 7) — written as an expression:
Runnable r = () -> System.out.println("running");
Comparator<String> byLength = (a, b) -> a.length() - b.length();
Function<String, Integer> length = s -> s.length();
BiFunction<Integer, Integer, Integer> add = (a, b) -> a + b;
Supplier<List<String>> maker = ArrayList::new;Syntax rules: parentheses are optional for exactly one parameter, types are usually inferred, and a
single-expression body needs no braces or return.
s -> s.length() // one parameter, expression body
(String s) -> s.length() // explicit type
(a, b) -> a + b // two parameters
() -> 42 // none
s -> { return s.length(); } // block body needs returnA lambda can read local variables from the enclosing scope only if they are effectively final — assigned once and never reassigned:
int limit = 40;
Predicate<Integer> passed = m -> m >= limit; // fine
// limit = 50; // adding this breaks the line aboveInstance and static fields have no such restriction, because they live on the heap rather than on the stack frame the lambda outlives.
Method references
When a lambda does nothing but call an existing method, :: says so more clearly:
list.forEach(s -> System.out.println(s));
list.forEach(System.out::println); // static/instance method of a specific object
names.stream().map(s -> s.toUpperCase());
names.stream().map(String::toUpperCase); // instance method of an arbitrary receiver
list.stream().map(n -> Integer.parseInt(n));
list.stream().map(Integer::parseInt); // static method
Supplier<ArrayList<String>> s = ArrayList::new; // constructorFour forms: Type::staticMethod, object::instanceMethod, Type::instanceMethod (the receiver
becomes the first argument) and Type::new.
Streams
A stream is a pipeline over a source. Three parts, and the middle one is lazy:
List<String> names = List.of("Raja", "Anu", "Vikram", "Sai");
List<String> result = names.stream() // 1. source
.filter(n -> n.length() > 3) // 2. intermediate — lazy
.map(String::toUpperCase) // intermediate — lazy
.sorted() // intermediate — lazy
.collect(Collectors.toList()); // 3. terminal — runs the pipeline
System.out.println(result); // [RAJA, VIKRAM]Nothing happens until the terminal operation. Write the first four lines without collect and
no element is ever examined — a favourite exam question, easy to demonstrate:
names.stream().filter(n -> {
System.out.println("checking " + n);
return true;
});
// prints nothing at allLaziness is what makes findFirst cheap: the pipeline pulls one element through at a time and stops
as soon as the terminal operation is satisfied.
The operations worth knowing
// intermediate
.filter(predicate) .map(function) .flatMap(function)
.distinct() .sorted() .sorted(comparator)
.limit(n) .skip(n) .peek(consumer)
// terminal
.forEach(consumer) .collect(collector) .toList() // Java 16+
.count() .anyMatch(p) .allMatch(p) .noneMatch(p)
.findFirst() .findAny() .reduce(identity, accumulator)
.min(cmp) .max(cmp) .sum() // on IntStreamList<Integer> marks = List.of(87, 45, 92, 38, 66);
marks.stream().filter(m -> m >= 40).count(); // 4
marks.stream().mapToInt(Integer::intValue).sum(); // 328
marks.stream().mapToInt(Integer::intValue).average().orElse(0); // 65.6
marks.stream().max(Integer::compare).get(); // 92
marks.stream().anyMatch(m -> m < 40); // true
marks.stream().reduce(0, Integer::sum); // 328Note mapToInt before sum() and average(). Stream<Integer> has no sum — the primitive
specialisations IntStream, LongStream and DoubleStream do, and they avoid boxing every element
as well.
flatMap flattens one level, which is how you handle a list of lists:
List<List<Integer>> nested = List.of(List.of(1, 2), List.of(3, 4));
List<Integer> flat = nested.stream()
.flatMap(List::stream)
.toList(); // [1, 2, 3, 4]Grouping and joining, which cover most real reporting work:
record Student(String name, String branch, int marks) { }
List<Student> all = List.of(
new Student("Raja", "CSE", 87),
new Student("Anu", "ECE", 92),
new Student("Vik", "CSE", 66));
Map<String, List<Student>> byBranch =
all.stream().collect(Collectors.groupingBy(Student::branch));
Map<String, Double> avgByBranch =
all.stream().collect(Collectors.groupingBy(Student::branch,
Collectors.averagingInt(Student::marks)));
Map<Boolean, List<Student>> passFail =
all.stream().collect(Collectors.partitioningBy(s -> s.marks() >= 40));
String names = all.stream().map(Student::name)
.collect(Collectors.joining(", ")); // Raja, Anu, VikTwo rules about streams that get asked:
- A stream can be consumed once. Reusing one throws
IllegalStateException: stream has already been operated upon or closed. - Streams do not modify the source. They produce a new result.
list.stream().sorted()leaveslistuntouched;list.sort(...)does not.
And .parallelStream() splits the work across threads. It helps for large inputs with independent
work, and hurts for small ones, because thread coordination is not free. Never combine it with a
lambda that mutates shared state.
Optional
Optional<T> makes "there might be no value" part of the type, instead of returning null and
hoping:
Optional<Student> found = all.stream()
.filter(s -> s.marks() > 90)
.findFirst();
found.isPresent(); // true
found.get(); // NoSuchElementException if empty — avoid
found.orElse(defaultStudent);
found.orElseGet(() -> expensiveDefault());
found.orElseThrow(() -> new NotFoundException("nobody above 90"));
found.ifPresent(s -> System.out.println(s.name()));
found.map(Student::name).orElse("none");Use it as a return type for something that may be absent. Do not use it for fields or
parameters, and do not call get() without checking — that just moves the
NullPointerException and makes it look deliberate.
Memory and garbage collection
The closing questions in most Java rounds. Answers worth having ready.
Where things live. Objects on the heap; local variables, primitives and references on the stack, one stack per thread; class metadata and static fields in the metaspace.
What garbage collection is. The JVM automatically reclaims objects that are no longer
reachable from any live reference — a GC root being a local variable in an active frame, a static
field, or an active thread. You never free memory manually, and you cannot force collection:
System.gc() is a suggestion the JVM may ignore.
The generational idea. The heap is split into a young generation (Eden plus two survivor spaces) and an old generation. New objects go into Eden. A minor GC collects the young generation frequently and cheaply, because most objects die young; survivors are eventually promoted to the old generation, which is collected by a slower major GC. This split exists precisely because the vast majority of objects become garbage almost immediately.
Can an object be garbage collected while a reference exists? Not a strong reference. Setting the
only reference to null makes it eligible. Two objects referring only to each other are both
unreachable and both collected — reference counting would leak here, and tracing collectors do not.
Memory leaks in Java. They exist, despite the GC: a static collection that grows forever, a
listener never unregistered, a key whose hashCode changed so it can never be removed, an unclosed
resource. "Java cannot leak memory" is the wrong answer; "Java cannot leak unreachable memory" is
the right one.
final, finally, finalize — a standard three-way question:
final— a keyword: a variable cannot be reassigned, a method cannot be overridden, a class cannot be extended.finally— a block that always runs aftertry.finalize— a deprecatedObjectmethod the GC once called before reclaiming an object. Never use it: it is not guaranteed to run, and it delays collection. Use try-with-resources instead.
What to take into an interview
- A lambda implements a functional interface; captured locals must be effectively final.
- Method references have four forms, including
Type::new. - A stream pipeline is source → lazy intermediate operations → terminal. Nothing runs without the terminal.
mapToIntbeforesumoraverage; a stream is single-use; streams never modify the source.groupingBy,partitioningByandjoiningcover most real aggregation.Optionalis for return types. Do not callget()unchecked.- Objects are on the heap, locals on the stack. GC reclaims unreachable objects;
System.gc()is only a hint. - Generational GC: most objects die young, so the young generation is collected often and cheaply.
- Java can still leak — through reachable objects nobody uses any more.
finalis a keyword,finallyis a block,finalizeis deprecated.
Now find out whether it stuck.
Reading Java for placements and writing it under a timer are different skills, and a coding round tests the second one. The graded practice track compiles your code and runs it against hidden test cases, then shows you the step-through when it fails.