skip to content
everything.effectiveJava.lambdasAndStreams

lambdas and streams

items 42–48: lambdas, method refs, and using streams judiciously.

chapter
chapter 16.06
reading time
9 min read

java 8 added lambdas (functions as values) and streams (pipelines over data). items 42 to 48: use lambdas and method references instead of anonymous classes, reach for the standard functional interfaces first, use streams where they make the code clearer and loops where they don't, keep stream functions free of side effects, and treat parallel() as an experiment, not a switch.

items 42 to 48. the core advice holds; amber marks where later releases added better tools.

lambdas

item 42 · lambdas over anonymous classes

an anonymous class to pass one method is five lines of ceremony around one expression. a lambda is the expression. it only works for functional interfaces (exactly one abstract method), and that's most of the time.

don'tan anonymous class
java
names.sort(new Comparator<String>() {
    @Override
    public int compare(String a, String b) {
        return Integer.compare(
            a.length(), b.length());
    }
});
doa lambda, or better
java
names.sort((a, b) -> Integer.compare(
    a.length(), b.length()));
 
names.sort(Comparator
    .comparingInt(String::length));

keep lambdas short (one to three lines). they have no name and no documentation, so a long one is a puzzle. and they're not identical to anonymous classes: inside a lambda, this is the enclosing object.

A.java
public class A {
    String name = "outer";
 
    void show() {
        Runnable anon = new Runnable() {
            String name = "anon";
            public void run() { System.out.println(this.name); } // the anonymous object
        };
        Runnable lambda = () -> System.out.println(this.name);   // the enclosing A
        anon.run();
        lambda.run();
    }
 
    public static void main(String[] args) { new A().show(); }
}
output
anon
outer

anonymous classes still have jobs lambdas can't do: implementing an interface with several abstract methods, extending an abstract class, or referring to themselves. how lambdas are compiled (not as anonymous classes) is in how lambdas are compiled.

item 43 · method references when they're clearer

String::length beats s -> s.length(). x -> x is clearer than Function.identity(), though. the rule is just: use whichever reads better. the five kinds:

kindexamplesame as
staticInteger::parseInts -> Integer.parseInt(s)
boundInstant.now()::isAftert -> now.isAfter(t), receiver fixed up front
unboundString::toLowerCases -> s.toLowerCase(), receiver is the first argument
class constructorTreeMap::new() -> new TreeMap<>()
array constructorint[]::newn -> new int[n]

more in method references.

item 44 · use the standard functional interfaces

java.util.function has 43 interfaces, and they're all variations on six shapes. before declaring your own Validator interface with one boolean check(String) method, check whether it's just a Predicate<String>. standard ones come with useful defaults (and, negate, andThen, compose) and every library already accepts them.

method   R apply(T t)
example  Function<String, Integer> len = String::length;
variants IntFunction<R>, ToIntFunction<T>, IntToLongFunction, BiFunction<T, U, R>, ...

six shapes, 43 interfaces. reach for these before writing your own, and prefer the primitive variants (IntPredicate over Predicate<Integer>) to skip boxing.

click a shape. most of the 43 interfaces are one of these six, specialized to int/long/double or taking two arguments.

write your own only when it'll be widely used and benefits from a descriptive name, a strong contract, or custom defaults (Comparator is the JDK's example: it's structurally a ToIntBiFunction, but the name and the thenComparing defaults earn their place). annotate it @FunctionalInterface. the full family is in functional interfaces.

streams

item 45 · streams when they're clearer, loops when they're not

streams shine when the question is a sentence: "total cents per city, sorted by city". loops are better when you need to break out early, mutate several locals, throw checked exceptions, or look at more than one element at a time.

Main.java
record Order(String city, int cents) {}
 
void main() {
    var orders = List.of(new Order("pune", 900), new Order("goa", 400),
                         new Order("pune", 300), new Order("delhi", 700));
 
    // the stream version: a one-sentence question, a one-sentence answer
    Map<String, Integer> byCity = orders.stream()
        .collect(Collectors.groupingBy(Order::city, TreeMap::new,
                 Collectors.summingInt(Order::cents)));
    IO.println(byCity);
 
    // the loop version: early exit plus two things tracked at once
    int total = 0, firstBig = -1;
    for (int i = 0; i < orders.size(); i++) {
        total += orders.get(i).cents();
        if (firstBig < 0 && total > 1000) { firstBig = i; break; }
    }
    IO.println("over 1000 after order #" + firstBig);
}
output
{delhi=700, goa=400, pune=1200}
over 1000 after order #1

name lambda parameters well (the element type is invisible in a pipeline), extract helper methods, and avoid streaming over chars ("hi".chars() gives you ints). how a pipeline actually executes, lazily and element by element, is in how a stream pipeline runs.

item 46 · no side effects inside stream pipelines

a stream function should compute its result from its input and nothing else. if forEach is doing the real work (writing into an outside map or list), you've written a loop with extra steps, and one that breaks the moment someone adds .parallel().

don'tforEach writing to an outside map
java
Map<String, Integer> counts =
    new HashMap<>();
words.stream().forEach(w ->
    counts.merge(w, 1, Integer::sum));
// add .parallel(): HashMap from
// many threads at once, lost updates
doa collector does the accumulating
java
var counts = words.stream()
    .collect(Collectors.groupingBy(
        w -> w, Collectors.counting()));
// add .parallel(): still correct

forEach is for reporting the result (printing, logging), not computing it. the collectors to know: toList, toSet, toMap, groupingBy, partitioningBy, counting, joining, summingInt, teeing. they're all in collectors.

item 47 · return a collection, not a stream

a Stream return type forces callers to stream, and they can only do it once:

predict the output
java
void main() {
    Stream<String> names = Stream.of("ada", "bo", "cy");
    IO.println(names.count());
    IO.println(names.count());
}

Collection is a subtype of Iterable and has stream(), so returning one serves loop people and stream people alike. if the data is huge, return a view (an AbstractList over something lazy, like the squares list in item 20) instead of materializing it. return a Stream only when the sequence is truly lazy or infinite, or one-pass by nature (lines of a file).

item 48 · parallel is not a speed button

parallel() splits work across the common fork/join pool. it pays off when the source splits cheaply (arrays, ArrayList, IntStream.range), the work per element is real, and the result combines cheaply (sum, count, reduce). it can be slower, or wrong, otherwise:

range 20M, sequential17.7 ms
range 20M, parallel2.5 ms
iterate+limit 20M, sequential83.1 ms
iterate+limit 20M, parallel111.8 ms
count primes ≤ 3M, sequential221.3 ms
count primes ≤ 3M, parallel25.5 ms
best of 7 runs after warm-up, jdk 25, a 14-core arm mac. Stream.iterate + limit can't be split well, so going parallel made it slower. the prime count is the case parallel was built for.

the gotchas: limit and findFirst force ordering work; a stateful or side-effecting lambda (item 46) goes from "ugly" to "wrong"; and the common pool is shared by the whole JVM, so one slow parallel stream slows down every other. measure with JMH before and after, on the real hardware. the long version is in parallel streams.

keylines
  • a lambda is an anonymous class minus the ceremony. inside it, this is the enclosing object.
  • use method references when they read better, lambdas when they don't.
  • check java.util.function before inventing an interface. six shapes cover nearly everything.
  • streams for sentence-shaped questions, loops for early exits and multi-variable state.
  • stream functions should be pure. collectors accumulate, forEach only reports.
  • return collections. parallelize only after measuring, and only CPU-bound work.