Lesson 12 · 11 min read · Java for placements

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 return

A 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 above

Instance 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; // constructor

Four 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 all

Laziness 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 IntStream
List<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);                         // 328

Note 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, Vik

Two 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() leaves list untouched; 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 after try.
  • finalize — a deprecated Object method 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.
  • mapToInt before sum or average; a stream is single-use; streams never modify the source.
  • groupingBy, partitioningBy and joining cover most real aggregation.
  • Optional is for return types. Do not call get() 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.
  • final is a keyword, finally is a block, finalize is deprecated.

You have finished the course

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.

Finished the course. The graded Java practice track is where reading turns into a score. See what that costs