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.
- item 42lambdas over anonymous classes
- item 43method references when they're clearer
- item 44use the standard functional interfaces
- item 45streams when they're clearer, loops when they're nottoList, mapMulti, gatherers
- item 46no side effects inside stream pipelines
- item 47return a collection, not a streamStream.toList, sequenced collections
- item 48parallel is not a speed buttonGatherers.mapConcurrent
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.
names.sort(new Comparator<String>() {
@Override
public int compare(String a, String b) {
return Integer.compare(
a.length(), b.length());
}
});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.
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(); }
}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:
| kind | example | same as |
|---|---|---|
| static | Integer::parseInt | s -> Integer.parseInt(s) |
| bound | Instant.now()::isAfter | t -> now.isAfter(t), receiver fixed up front |
| unbound | String::toLowerCase | s -> s.toLowerCase(), receiver is the first argument |
| class constructor | TreeMap::new | () -> new TreeMap<>() |
| array constructor | int[]::new | n -> 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.
six shapes, 43 interfaces. reach for these before writing your own, and prefer the primitive variants (IntPredicate over Predicate<Integer>) to skip boxing.
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.
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);
}{delhi=700, goa=400, pune=1200}
over 1000 after order #1name 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().
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 updatesvar counts = words.stream()
.collect(Collectors.groupingBy(
w -> w, Collectors.counting()));
// add .parallel(): still correctforEach 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:
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:
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.
- a lambda is an anonymous class minus the ceremony. inside it,
thisis the enclosing object. - use method references when they read better, lambdas when they don't.
- check
java.util.functionbefore 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,
forEachonly reports. - return collections. parallelize only after measuring, and only CPU-bound work.