0% found this document useful (0 votes)
12 views48 pages

Lambdas Streams Bloch

Uploaded by

mcknucklecuck
Copyright
© © All Rights Reserved
We take content rights seriously. If you suspect this is your content, claim it here.
Available Formats
Download as PDF, TXT or read online on Scribd
0% found this document useful (0 votes)
12 views48 pages

Lambdas Streams Bloch

Uploaded by

mcknucklecuck
Copyright
© © All Rights Reserved
We take content rights seriously. If you suspect this is your content, claim it here.
Available Formats
Download as PDF, TXT or read online on Scribd
You are on page 1/ 48

Principles of Software Construction:

Objects, Design, and Concurrency

Lambdas and streams

Josh Bloch Charlie Garrod

17-214 1
Administrivia

• HW5C (plugins for others’ frameworks) due Tuesday 4/27


• Final exam schedule (tentative) – released 5/13, evening, due
5/14 11:59pm EDT

17-214 2
Today’s topics

• Two features added in Java 8


I. Lambdas: language feature
II. Streams: library feature
• Designed to work together

17-214 4
I. What is a lambda?

• Term comes from λ-Calculus


– Formal logic introduced by Alonzo Church in the 1930's
– Everything is a function!
• A lambda (λ) is simply an anonymous function
– A function without a corresponding identifier (name)
• Originally limited to academic languages (e.g., Lisp)
• Popularity exploded in the ‘90s (JavaScript, Ruby, etc.)
• Now ubiquitous in functional and mainstream languages

17-214 5
When did Java get lambdas?

A. It’s had them since the beginning


B. It’s had them since anonymous classes were added (JDK 1.1, 1997)
C. It’s had them since Java 8 (2014) – the spec says so
D. Never had ’em, never will

17-214 6
Function objects in Java 1.0 (1996)

class StringLengthComparator implements Comparator {


// Singleton
private StringLengthComparator() { }
public static final StringLengthComparator INSTANCE =
new StringLengthComparator();

public int compare(Object o1, Object o2) {


String s1 = (String) o1, s2 = (String) o2;
return s1.length() - s2.length();
}
}

Arrays.sort(words, StringLengthComparator.INSTANCE);

17-214 7
Function objects in Java 1.1 (1997) – anonymous classes

Arrays.sort(words, new Comparator() {


public int compare(Object o1, Object o2) {
String s1 = (String) o1, s2 = (String) o2;
return s1.length() - s2.length();
}
});

Class Instance Creation Expression (CICE)

17-214 8
Function objects in Java 5 (2004)

Arrays.sort(words, new Comparator<String>() {


public int compare(String s1, String s2) {
return s1.length() - s2.length(); // No casts!
}
});

Generics made things looks a bit better

17-214 9
Function objects in Java 8 (2014)

Arrays.sort(words, (s1, s2) -> s1.length() - s2.length());

• They feel like lambdas, and they’re called lambdas


– They’re no more anonymous than 1.1 CICE’s!
– but the method name does not appear in code

17-214 10
Lambda syntax

Syntax Example
parameter -> expression x -> x * x
parameter -> block n -> { int result = 1;
for (int i = 1; i <= n; i++) result *= i;
return result; }
(parameters) -> expression (x, y) -> Math.sqrt(x*x + y*y)
(parameters) -> block (n, r) -> { int result = 1;
for (int k = r; k <= n; k++) result *= k;
return result; }
(parameter decls) -> expression (double x, double y) -> Math.sqrt(x*x + y*y)
(parameters decls) -> block (int n, int r) -> { int result = 1;
for (int k = r; k < n; k++) result *= k;
return result; }

17-214 11
Java has no function types, only functional interfaces

• Interfaces with only one explicit abstract method


• Optionally annotated with @FunctionalInterface
– Do it, for the same reason you use @Override
• A lambda is essentially a functional interface literal
• Some functional interfaces you already know:
– Runnable, Callable, Comparator, ActionListener
• Many, many more in package java.util.function

17-214 12
Java has 43 standard functional interfaces
Luckily, there is a fair amount of structure

BiConsumer<T,U> IntUnaryOperator
BiFunction<T,U,R> LongBinaryOperator
BinaryOperator<T> LongConsumer
BiPredicate<T,U> LongFunction<R>
BooleanSupplier LongPredicate
Consumer<T> LongSupplier
DoubleBinaryOperator LongToDoubleFunction
DoubleConsumer LongToIntFunction
DoubleFunction<R> LongUnaryOperator
DoublePredicate ObjDoubleConsumer<T>
DoubleSupplier ObjIntConsumer<T>
DoubleToIntFunction ObjLongConsumer<T>
DoubleToLongFunction Predicate<T>
DoubleUnaryOperator Supplier<T>
Function<T,R> ToDoubleBiFunction<T,U>
IntBinaryOperator ToDoubleFunction<T>
IntConsumer ToIntBiFunction<T,U>
IntFunction<R> ToIntFunction<T>
IntPredicate ToLongBiFunction<T,U>
IntSupplier ToLongFunction<T>
IntToDoubleFunction UnaryOperator<T>
IntToLongFunction

17-214 13
The 6 basic standard functional interfaces

Interface Function Signature Example


UnaryOperator<T> T apply(T t) s -> s.tolowerCase()

BinaryOperator<T> T apply(T t1, T t2) (i, j) -> i.add(j)

Predicate<T> boolean test(T t) c -> c.isEmpty()

Function<T,R> R apply(T t) a -> Arrays.asList(a)

Supplier<T> T get() Instant.now()

Consumer<T> void accept(T t) o -> System.out.println(o)

Most of the remaining 37 interfaces provide support for primitive


types. Use them or pay the price!

17-214 14
A subtle difference between lambdas & anonymous classes

class Enclosing {
Supplier<Object> lambda() {
return () -> this;
}

Supplier<Object> anon() {
return new Supplier<Object>() {
public Object get() { return this; }
};
}

public static void main(String[] args) {


Enclosing enclosing = new Enclosing();
Object lambdaThis = enclosing.lambda().get();
Object anonThis = enclosing.anon().get();
System.out.println(anonThis == enclosing); // false
System.out.println(lambdaThis == enclosing); // true
}
}
17-214 15
Method references – a more succinct alternative to lambdas

• Lambdas are succinct


map.merge(key, 1, (count, incr) -> count + incr);
• But method references can be more so
map.merge(key, 1, Integer::sum);
• The more parameters, the bigger the win
– But parameter names may provide documentation
– If you use a lambda, choose parameter names carefully!

17-214 16
Occasionally, lambdas are more succinct

service.execute(() -> action());

is preferable to
service.execute(GoshThisClassNameIsHumongous::action);

17-214 17
Know all five kinds of method references
They all have their uses

Type Example Lambda Equivalent*


Static Integer::parseInt str -> Integer.parseInt(str)

Bound Instant.now()::isAfter Instant then = Instant.now();


t -> then.isAfter(t)

Unbound String::toLowerCase str -> str.toLowerCase()

Class Constructor TreeMap<K,V>::new () -> new TreeMap<K,V>()

Array Constructor int[]::new len -> new int[len]

17-214 18
The 6 basic functional interfaces redux – method refs

Interface Function Signature Example


UnaryOperator<T> T apply(T t) String::toLowerCase

BinaryOperator<T> T apply(T t1, T t2) BigInteger::add

Predicate<T> boolean test(T t) Collection::isEmpty

Function<T,R> R apply(T t) Arrays::asList

Supplier<T> T get() Instant::now

Consumer<T> void accept(T t) System.out::println

17-214 19
Lambdas vs. method references – the bottom line

• (Almost) anything you can do with a method reference, you can


also do with a lambda
• Method references are usually more succinct
• But sometimes lambdas are clearer
• Use your best judgment
– You can always change your mind
– Which you use is an implementation detail

17-214 20
II. What is a stream?

• A bunch of data objects (typically from a collection, array, or


input device) for bulk data processing
• Processed by a pipeline
– A single stream generator (data source)
– Zero or more intermediate stream operations
– A single terminal stream operation
• Supports mostly-functional data processing
• Enables painless* parallelism
– Simply replace stream with parallelStream
• Uses ForkJoinPool under the covers
– You may or may not see a performance improvement

17-214 21
Streams are processed lazily

• Data is “pulled” by terminal operation, not pushed by source


– Infinite streams are not a problem (lazy evaluation)
• Intermediate operations can be fused
– Multiple intermediate operations usually don’t result in multiple traversals
• Intermediate results typically not stored
– But there are exceptions (e.g., sorted)

17-214 22
Simple stream examples – mapping, filtering, sorting, etc.

List<String> longStrings = stringList.stream()


.filter(s -> s.length() > 3)
.collect(Collectors.toList());

17-214 23
Simple stream examples – mapping, filtering, sorting, etc.

List<String> longStrings = stringList.stream()


.filter(s -> s.length() > 3)
.collect(Collectors.toList());

List<String> firstLetters = stringList.stream()


.map(s -> s.substring(0,1))
.collect(Collectors.toList());

17-214 24
Simple stream examples – mapping, filtering, sorting, etc.

List<String> longStrings = stringList.stream()


.filter(s -> s.length() > 3)
.collect(Collectors.toList());

List<String> firstLetters = stringList.stream()


.map(s -> s.substring(0,1))
.collect(Collectors.toList());

List<String> firstLettersOfLongStrings = stringList.stream()


.filter(s -> s.length() > 3)
.map(s -> s.substring(0,1))
.collect(Collectors.toList());

17-214 25
Simple stream examples – mapping, filtering, sorting, etc.

List<String> longStrings = stringList.stream()


.filter(s -> s.length() > 3)
.collect(Collectors.toList());

List<String> firstLetters = stringList.stream()


.map(s -> s.substring(0,1))
.collect(Collectors.toList());

List<String> firstLettersOfLongStrings = stringList.stream()


.filter(s -> s.length() > 3)
.map(s -> s.substring(0,1))
.collect(Collectors.toList());

List<String> sortedFirstLettersWithoutDups = stringList.stream()


.map(s -> s.substring(0,1))
.distinct()
.sorted()
.collect(Collectors.toList());

17-214 26
Simple stream examples – file input

// Prints a file, one line at a time


try (Stream<String> lines = Files.lines(Paths.get(fileName))) {
lines.forEach(System.out::println);
}

17-214 27
Simple stream examples – file input

// Prints a file, one line at a time


try (Stream<String> lines = Files.lines(Paths.get(fileName))) {
lines.forEach(System.out::println);
}

// Prints sorted list of non-empty lines in file (trimmed)


try (Stream<String> lines = Files.lines(Paths.get(fileName))) {
lines.map(String::trim)
.filter(s -> !s.isEmpty())
.sorted()
.forEach(System.out::println);
}

17-214 28
Simple stream examples – bulk predicates

boolean allStringsHaveLengthThree = stringList.stream()


.allMatch(s -> s.length() == 3);

17-214 29
Simple stream examples – bulk predicates

boolean allStringsHaveLengthThree = stringList.stream()


.allMatch(s -> s.length() == 3);

boolean anyStringHasLengthThree = stringList.stream()


.anyMatch(s -> s.length() == 3);

17-214 30
Stream example – the first twenty Mersenne Primes

Mersenne number is a number of the form 2p − 1


If p is prime, the corresponding Mersenne number may be prime
If it is, it’s a Mersenne prime
Named after Marin Mersenne, a French friar in the early 17th century
The largest known prime (282,589,933 − 1) is a Mersenne prime

static Stream<BigInteger> primes() {


return Stream.iterate(TWO, BigInteger::nextProbablePrime);
}

public static void main(String[] args) {


primes().map(p -> TWO.pow(p.intValueExact()).subtract(ONE))
.filter(mersenne -> mersenne.isProbablePrime(50))
.limit(20)
.forEach(System.out::println);
}

17-214 31
Iterative program to print large anagram groups in a dictionary
Review: you saw this Collections Framework case study

public static void main(String[] args) throws IOException {


File dictionary = new File(args[0]);
int minGroupSize = Integer.parseInt(args[1]);

Map<String, Set<String>> groups = new HashMap<>();


try (Scanner s = new Scanner(dictionary)) {
while (s.hasNext()) {
String word = s.next();
groups.computeIfAbsent(alphabetize(word),
(unused) -> new TreeSet<>()).add(word);
}
}

for (Set<String> group : groups.values())


if (group.size() >= minGroupSize)
System.out.println(group.size() + ": " + group);
}

17-214 32
Helper function to alphabetize a word
Word nerds call the result an alphagram

private static String alphabetize(String s) {


char[] a = s.toCharArray();
Arrays.sort(a);
return new String(a);
}

17-214 33
Streams gone crazy
Just because you can doesn’t mean you should!

public static void main(String[] args) throws IOException {


Path dictionary = Paths.get(args[0]);
int minGroupSize = Integer.parseInt(args[1]);

try (Stream<String> words = Files.lines(dictionary)) {


words.collect(groupingBy(word -> word.chars().sorted()
.collect(StringBuilder::new,
(sb, c) -> sb.append((char) c),
StringBuilder::append).toString()))
.values().stream()
.filter(group -> group.size() >= minGroupSize)
.map(group -> group.size() + ": " + group)
.forEach(System.out::println);
}
}

17-214 34
A happy medium
Tasteful use of streams enhances clarity and conciseness

public static void main(String[] args) throws IOException {


Path dictionary = Paths.get(args[0]);
int minGroupSize = Integer.parseInt(args[1]);

try (Stream<String> words = Files.lines(dictionary)) {


words.collect(groupingBy(word -> alphabetize(word)))
.values().stream() // Terminal op; create new stream
.filter(group -> group.size() >= minGroupSize)
.forEach(g -> System.out.println(g.size() + ": " + g));
}
}

private static String alphabetize(String s) {


char[] a = s.toCharArray();
Arrays.sort(a);
return new String(a);
}

17-214 35
A minipuzzler - what does this print?

"Hello world!".chars()
.forEach(System.out::print);

17-214 36
Puzzler solution

"Hello world!".chars()
.forEach(System.out::print);

Prints 721011081081113211911111410810033

Why does it do this?

17-214 37
Puzzler solution

"Hello world!".chars()
.forEach(System.out::print);

Prints 721011081081113211911111410810033

Because String’s chars method returns an IntStream

17-214 38
How do you fix it?

"Hello world!".chars()
.forEach(x -> System.out.print((char) x));

Now prints Hello world!

Moral
Streams only for object ref types, int, long, and double
“Minor primitive types” (byte, short, char, float, boolean) absent
String’s chars method is horribly named!
Avoid using streams for char processing

17-214 39
Streams – the bottom line

• Streams are great for many things…


– But they’re not a panacea
• When you first learn streams, you may want to convert all of
your loops. Don’t!
– It may make your code shorter, but not clearer
• Exercise judgment
– Properly used, streams increase brevity and clarity
– Most programs should combine iteration and streams
• It’s not always clear at the outset
– If you don’t know, take a guess and start hacking
– If it doesn’t feel right, try the other approach

17-214 40
Use caution making streams parallel
Remember our Mersenne primes program?

static Stream<BigInteger> primes() {


return Stream.iterate(TWO, BigInteger::nextProbablePrime);
}

public static void main(String[] args) {


primes().map(p -> TWO.pow(p.intValueExact()).subtract(ONE))
.filter(mersenne -> mersenne.isProbablePrime(50))
.limit(20)
.forEach(System.out::println);
}

Runs in 10.1s on my 12-core, 24-thread Ryzen 9 3900X

17-214 41
How fast do you think this program runs?

static Stream<BigInteger> primes() {


return Stream.iterate(TWO, BigInteger::nextProbablePrime);
}

public static void main(String[] args) {


primes().map(p -> TWO.pow(p.intValueExact()).subtract(ONE))
.parallel()
.filter(mersenne -> mersenne.isProbablePrime(50))
.limit(20)
.forEach(System.out::println);
}

17-214 42
How fast do you think this program runs?

static Stream<BigInteger> primes() {


return Stream.iterate(TWO, BigInteger::nextProbablePrime);
}

public static void main(String[] args) {


primes().map(p -> TWO.pow(p.intValueExact()).subtract(ONE))
.parallel()
.filter(mersenne -> mersenne.isProbablePrime(50))
.limit(20)
.forEach(System.out::println);
}

Very, very slowly. I gave up after half an hour.

17-214 43
Why did the program run so slowly?

• The streams library has no idea how to parallelize it


– And the heuristics fail miserably
• In the best case, parallel is unlikely to help if:
– Stream source is Stream.iterate, or
– Intermediate limit operation is used
• This isn’t the best case
– Default strategy for limit computes excess elements
– Each Mersenne prime takes twice as long to compute as last one
• Moral: do not parallelize indiscriminately!

17-214 44
What does parallelize well?

• Arrays, ArrayList, HashMap, HashSet,


ConcurrentHashMap, int and long ranges…
• What do these sources have in common?
– Predictably splittable
– Good locality of reference
• Terminal operation also matters
– Must be quick, or easily parallelizable
– Best are reductions, e.g., min, max, count, sum
– Collectors (AKA mutable reductions) not so good
• Intermediate operations matter too
– Mapping and filtering good, limit bad

17-214 45
Example – number of primes ≤ n, π(n)

static long pi(long n) {


return LongStream.rangeClosed(2, n)
.mapToObj(BigInteger::valueOf)
.filter(i -> i.isProbablePrime(50))
.count();
}

Takes 25s to compute π(107) on my machine

17-214 46
Example – number of primes ≤ n, π(n)

static long pi(long n) {


return LongStream.rangeClosed(2, n)
.parallel()
.mapToObj(BigInteger::valueOf)
.filter(i -> i.isProbablePrime(50))
.count();
}

In parallel, it takes 1.9s, which is 13 times as fast!

17-214 47
The takeaway – .parallel() is merely an optimization

• Optimize Judiciously [EJ Item 67]


• Premature optimization is the root of all evil
• Don’t parallelize unless you can prove it maintains correctness
• Don’t parallelize unless you have a good reason to believe it will help
• Measure performance before and after

17-214 48
Summary

• When to use a lambda


– Always, in preference to CICE
• When to use a method reference
– Almost always, in preference to a lambda
• When to use a stream
– When it feels and looks right
• When to use a parallel stream
– When you’ve convinced yourself it has equivalent semantics
and demonstrated that it’s a performance win

17-214 49

You might also like

pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy