Functional Programming - What you need to know
Joao Carlos Sousa do Vale
Posted on May 13, 2022
Functional Programming
Introduction
- focusing on the Streams API.
- a functional interface has exactly one abstract method.
- java.util.function package.
Functional interface | Return type | Method name | # of parameters |
---|---|---|---|
Supplier | T | get() | 0 |
Consumer | void | accept(T) | 1 |
BiConsumer | void | accept(T,U) | 2 (T, U) |
Predicate | boolean | test(T) | 1 (T) |
BiPredicate | boolean | test(T,U) | 2 (T, U) |
Function | R | apply(T) | 1 (T) |
BiFunction | R | apply(T,U) | 2 (T, U) |
UnaryOperator | T | apply(T) | 1 (T) |
BinaryOperator | T | apply(T,T) | 2 (T, T) |
IMPLEMENTING SUPPLIER
- A Supplier is used when you want to generate or supply values without taking any input.
- is often used when constructing new objects.
@FunctionalInterface
public interface Supplier<T> {
T get();
}
Supplier<LocalDate> s1 = () -> LocalDate.now();
LocalDate d1 = s1.get();
System.out.println(d1);
IMPLEMENTING CONSUMER AND BICONSUMER
- You use a Consumer when you want to do something with a parameter but not return anything.
- BiConsumer does the same thing except that it takes two parameters.
@FunctionalInterface
public interface Consumer<T> {
void accept(T t);
}
@FunctionalInterface
public interface BiConsumer<T, U> {
void accept(T t, U u);
}
Consumer<String> c1 = x -> System.out.println(x);
c1.accept("Annie");
var map = new HashMap<String, Integer>();
BiConsumer<String, Integer> b1 = (k, v) -> map.put(k, v);
b1.accept("chicken", 7);
b1.accept("chick", 1);
System.out.println(map);
IMPLEMENTING PREDICATE AND BIPREDICATE
- Predicate is often used when filtering or matching.
- A BiPredicate is just like a Predicate except that it takes two parameters instead of one.
@FunctionalInterface
public interface Predicate<T> {
boolean test(T t);
}
@FunctionalInterface
public interface BiPredicate<T, U> {
boolean test(T t, U u);
}
Predicate<String> p1 = x -> x.isEmpty();
System.out.println(p1.test("")); // true
BiPredicate<String, String> b1 = (string, prefix) -> string.startsWith(prefix);
System.out.println(b1.test("chicken", "chick")); // true
IMPLEMENTING FUNCTION AND BIFUNCTION
- A Function is responsible for turning one parameter into a value of a potentially different type and returning it.
- a BiFunction is responsible for turning two parameters into a value and returning it.
@FunctionalInterface
public interface Function<T, R> {
R apply(T t);
}
@FunctionalInterface
public interface BiFunction<T, U, R> {
R apply(T t, U u);
}
Function<String, Integer> f1 = x -> x.length();
System.out.println(f1.apply("cluck")); // 5
BiFunction<String, String, String> b1 = (string, toAdd) -> string.concat(toAdd);
System.out.println(b1.apply("baby ", "chick")); // baby chick
IMPLEMENTING UNARYOPERATOR AND BINARYOPERATOR
- UnaryOperator and BinaryOperator require all type parameters to be the same type.
- A UnaryOperator transforms its value into one of the same type.
- A BinaryOperator merges two values into one of the same type.
@FunctionalInterface
public interface UnaryOperator<T> extends Function<T, T> {
T apply(T t);
}
@FunctionalInterface
public interface BinaryOperator<T> extends BiFunction<T, T, T> {
T apply(T t1, T t2);
}
UnaryOperator<String> u1 = x -> x.toUpperCase();
System.out.println(u1.apply("chirp")); // CHIRP
BinaryOperator<String> b1 = (string, toAdd) -> string.concat(toAdd);
System.out.println(b1.apply("baby ", "chick")); // baby chick
CONVENIENCE METHODS ON FUNCTIONAL INTERFACES
- Several of the common functional interfaces provide a number of helpful default methods.
Interface instance | Method return type | Method name | Method parameters |
---|---|---|---|
Consumer | Consumer | andThen() | Consumer |
Function | Function | andThen() | Function |
Function | Function | compose() | Function |
Predicate | Predicate | and() | Predicate |
Predicate | Predicate | negate() | — |
Predicate | Predicate | or() | Predicate |
Predicate<String> egg = s -> s.contains("egg");
Predicate<String> brown = s -> s.contains("brown");
Predicate<String> brownEggs = egg.and(brown);
Predicate<String> otherEggs = egg.and(brown.negate());
Consumer<String> c1 = x -> System.out.print("1: " + x);
Consumer<String> c2 = x -> System.out.print(",2: " + x);
Consumer<String> combined = c1.andThen(c2);
combined.accept("Annie"); // 1: Annie,2: Annie
Function<Integer, Integer> before = x -> x + 1;
Function<Integer, Integer> after = x -> x * 2;
Function<Integer, Integer> combined = after.compose(before);
System.out.println(combined.apply(3)); // 8
Optional
- An Optional is created using a factory.
- Think of an Optional as a box that might have something in it or might instead be empty
Optional.empty()
Optional.of(<value>)
CREATING AN OPTIONAL
10: public static Optional<Double> average(int… scores) {
11: if (scores.length == 0) return Optional.empty();
12: int sum = 0;
13: for (int score: scores) sum += score;
14: return Optional.of((double) sum / scores.length);
15: }
...
System.out.println(average(90, 100)); // Optional[95.0]
System.out.println(average()); // Optional.empty
...
20: Optional<Double> opt = average(90, 100);
21: if (opt.isPresent())
22: System.out.println(opt.get()); // 95.0
- We'd get an exception since there is no value inside the Optional.
- java.util.NoSuchElementException: No value present
26: Optional<Double> opt = average();
27: System.out.println(opt.get()); // NoSuchElementException
- it is common to want to use empty() when the value is null.
- You can do this with an if statement or ternary operator ( ? :) .
Optional o = (value == null) ? Optional.empty() : Optional.of(value);
Or
Optional o = Optional.ofNullable(value);
Method | When Optional is empty | When Optional contains a value |
---|---|---|
get() | Throws an exception | Returns value |
ifPresent(Consumer c) | Does nothing | Calls Consumer with value |
isPresent() | Returns false | Returns true |
orElse(T other) | Returns other parameter | Returns value |
orElseGet(Supplier s) | Returns result of calling Supplier | Returns value |
orElseThrow() | Throws NoSuchElementException | Returns value |
orElseThrow(Supplier s) | Throws exception created by calling Supplier | Returns value |
- ifPresent() -> You can think of it as an if statement with no else.
Optional<Double> opt = average(90, 100);
opt.ifPresent(System.out::println);
DEALING WITH AN EMPTY OPTIONAL
30: Optional<Double> opt = average();
31: System.out.println(opt.orElse(Double.NaN));
32: System.out.println(opt.orElseGet(() -> Math.random()));
NaN
0.49775932295380165
30: Optional<Double> opt = average();
31: System.out.println(opt.orElseThrow());
- Without specifying a Supplier for the exception, Java will throw a NoSuchElementException.
Exception in thread "main" java.util.NoSuchElementException:
No value present
at java.base/java.util.Optional.orElseThrow(Optional.java:382)
30: Optional<Double> opt = average();
31: System.out.println(opt.orElseThrow(
32: () -> new IllegalStateException()));
Exception in thread "main" java.lang.IllegalStateException
at optionals.Methods.lambda$orElse$1(Methods.java:30)
at java.base/java.util.Optional.orElseThrow(Optional.java:408)
Using Streams
- A stream in Java is a sequence of data.
- A stream pipeline consists of the operations that run on a stream to produce a result.
UNDERSTANDING THE PIPELINE FLOW
-
stream pipeline as an assembly line in a factory.
- stream operations
-
There are three parts to a stream pipeline
- source: Where the stream comes from
- intermediate operations: Transforms the stream into another one.
- terminal operation: Actually produces a result.
The stream is no longer valid after a terminal operation completes.
CREATING STREAM SOURCES
- In Java, the streams are represented by the Stream interface, defined in the java.util.stream package.
Creating Finite Streams
11: Stream<String> empty = Stream.empty(); // count = 0
12: Stream<Integer> singleElement = Stream.of(1); // count = 1
13: Stream<Integer> fromArray = Stream.of(1, 2, 3); // count = 3
14: var list = List.of("a", "b", "c");
15: Stream<String> fromList = list.stream();
Creating Infinite Streams
7: Stream<Double> randoms = Stream.generate(Math::random);
18: Stream<Integer> oddNumbers = Stream.iterate(1, n -> n + 2);
- overloaded example of iterate()
19: Stream<Integer> oddNumberUnder100 = Stream.iterate(
20: 1, // seed
21: n -> n < 100, // Predicate to specify when done
22: n -> n + 2); // UnaryOperator to get next value
Method | Finite or infinite? | Notes |
---|---|---|
Stream.empty() | Finite | Creates Stream with zero elements |
Stream.of(varargs) | Finite | Creates Stream with elements listed |
coll.stream() | Finite | Creates Stream from a Collection |
coll.parallelStream() | Finite | Creates Stream from a Collection where the stream can run in parallel |
Stream.generate(supplier) | Infinite | Creates Stream by calling the Supplier for each element upon request |
Stream.iterate(seed, unaryOperator) | Infinite | Creates Stream by using the seed for the first element and then calling the UnaryOperator for each subsequent element upon request |
Stream.iterate(seed, predicate, unaryOperator) | Finite or infinite | Creates Stream by using the seed for the first element and then calling the UnaryOperator for each subsequent element upon request. Stops if the Predicate returns false |
USING COMMON TERMINAL OPERATIONS
- You can perform a terminal operation without any intermediate operations but not the other way around.
- Reductions are a special type of terminal operation where all the contents of the stream are combined into a single primitive or Object.
Method | What happens for infinite streams | Return value | Reduction |
---|---|---|---|
count() | Does not terminate | long | Yes |
min() max() |
Does not terminate | Optional | Yes |
findAny() findFirst() |
Terminates | Optional | No |
allMatch() anyMatch() noneMatch() |
Sometimes terminates | boolean | No |
forEach() | Does not terminate | void | No |
reduce() | Does not terminate | Varies | Yes |
collect() | Does not terminate | Varies | Yes |
count()
- The count() method determines the number of elements in a finite stream.
long count()
Stream<String> s = Stream.of("monkey", "gorilla", "bonobo");
System.out.println(s.count()); // 3
min() and max()
- The min() and max() methods allow you to pass a custom comparator and find the smallest or largest value in a finite stream according to that sort order.
Optional<T> min(Comparator<? super T> comparator)
Optional<T> max(Comparator<? super T> comparator)
Stream<String> s = Stream.of("monkey", "ape", "bonobo");
Optional<String> min = s.min((s1, s2) -> s1.length()-s2.length());
min.ifPresent(System.out::println); // ape
- Returns an optional because we have cases where is not possible to find a min or max.
Optional<?> minEmpty = Stream.empty().min((s1, s2) -> 0);
System.out.println(minEmpty.isPresent()); // false
findAny() and findFirst()
- The findAny() and findFirst() methods return an element of the stream unless the stream is empty.
- If the stream is empty, they return an empty Optional.
- can terminate with an infinite stream.
Optional<T> findAny()
Optional<T> findFirst()
Stream<String> s = Stream.of("monkey", "gorilla", "bonobo");
Stream<String> infinite = Stream.generate(() -> "chimp");
s.findAny().ifPresent(System.out::println); // monkey (usually)
infinite.findAny().ifPresent(System.out::println); // chimp
allMatch(), anyMatch(), and noneMatch()
- The allMatch(), anyMatch(), and noneMatch() methods search a stream and return information about how the stream pertains to the predicate.
- These may or may not terminate for infinite streams.
- they are not reductions because they do not necessarily look at all of the elements.
boolean anyMatch(Predicate <? super T> predicate)
boolean allMatch(Predicate <? super T> predicate)
boolean noneMatch(Predicate <? super T> predicate)
var list = List.of("monkey", "2", "chimp");
Stream<String> infinite = Stream.generate(() -> "chimp");
Predicate<String> pred = x -> Character.isLetter(x.charAt(0));
System.out.println(list.stream().anyMatch(pred)); // true
System.out.println(list.stream().allMatch(pred)); // false
System.out.println(list.stream().noneMatch(pred)); // false
System.out.println(infinite.anyMatch(pred)); // true
forEach()
- it is common to iterate over the elements of a stream.
- on an infinite stream does not terminate.
- it is not a reduction.
void forEach(Consumer<? super T> action)
tream<String> s = Stream.of("Monkey", "Gorilla", "Bonobo");
s.forEach(System.out::print); // MonkeyGorillaBonobo
reduce()
- The reduce() method combines a stream into a single object.
T reduce(T identity, BinaryOperator<T> accumulator)
Optional<T> reduce(BinaryOperator<T> accumulator)
<U> U reduce(U identity, BiFunction<U,? super T,U> accumulator, BinaryOperator<U> combiner)
- The identity is the initial value of the reduction
- The accumulator combines the current result with the current value in the stream.
var array = new String[] { "w", "o", "l", "f" };
var result = "";
for (var s: array) result = result + s;
System.out.println(result); // wolf
Stream<String> stream = Stream.of("w", "o", "l", "f");
String word = stream.reduce("", (s, c) -> s + c);
System.out.println(word); // wolf
OR
Stream<String> stream = Stream.of("w", "o", "l", "f");
String word = stream.reduce("", String::concat);
System.out.println(word); // wolf
Stream<Integer> stream = Stream.of(3, 5, 6);
System.out.println(stream.reduce(1, (a, b) -> a*b)); // 90
- There are three choices for what is in the Optional.
- If the stream is empty, an empty Optional is returned.
- If the stream has one element, it is returned.
- If the stream has multiple elements, the accumulator is applied to combine them.
BinaryOperator<Integer> op = (a, b) -> a * b;
Stream<Integer> empty = Stream.empty();
Stream<Integer> oneElement = Stream.of(3);
Stream<Integer> threeElements = Stream.of(3, 5, 6);
empty.reduce(op).ifPresent(System.out::println); // no output
oneElement.reduce(op).ifPresent(System.out::println); // 3
threeElements.reduce(op).ifPresent(System.out::println); // 90
- The third method signature is used when we are dealing with different types.
Stream<String> stream = Stream.of("w", "o", "l", "f!");
int length = stream.reduce(0, (i, s) -> i+s.length(), (a, b) -> a+b);
System.out.println(length); // 5
- i is Integer and s is a String.
- combiner used in parallel streams.
collect()
- is a special type of reduction called a mutable reduction.
<R> R collect(Supplier<R> supplier,
BiConsumer<R, ? super T> accumulator,
BiConsumer<R, R> combiner)
<R,A> R collect(Collector<? super T, A,R> collector)
Stream<String> stream = Stream.of("w", "o", "l", "f");
StringBuilder word = stream.collect(
StringBuilder::new,
StringBuilder::append,
StringBuilder::append)
System.out.println(word); // wolf
- Combiner used in parallel streams;
Stream<String> stream = Stream.of("w", "o", "l", "f");
TreeSet<String> set = stream.collect(
TreeSet::new,
TreeSet::add,
TreeSet::addAll);
System.out.println(set); // [f, l, o, w]
Stream<String> stream = Stream.of("w", "o", "l", "f");
TreeSet<String> set =
stream.collect(Collectors.toCollection(TreeSet::new));
System.out.println(set); // [f, l, o, w]
Stream<String> stream = Stream.of("w", "o", "l", "f");
Set<String> set = stream.collect(Collectors.toSet());
System.out.println(set); // [f, w, l, o]
USING COMMON INTERMEDIATE OPERATIONS
- an intermediate operation produces a stream as its result.
filter()
- The filter() method returns a Stream with elements that match a given expression.
Stream<T> filter(Predicate<? super T> predicate)
Stream<String> s = Stream.of("monkey", "gorilla", "bonobo");
s.filter(x -> x.startsWith("m"))
.forEach(System.out::print); // monkey
distinct()
- The distinct() method returns a stream with duplicate values removed.
Stream<T> distinct()
Stream<String> s = Stream.of("duck", "duck", "duck", "goose");
s.distinct()
.forEach(System.out::print); // duckgoose
limit() and skip()
- The limit() and skip() methods can make a Stream smaller, or they could make a finite stream out of an infinite stream.
Stream<T> limit(long maxSize)
Stream<T> skip(long n)
Stream<Integer> s = Stream.iterate(1, n -> n + 1);
s.skip(5)
.limit(2)
.forEach(System.out::print); // 67
map()
- The map() method creates a one‐to‐one mapping from the elements in the stream to the elements of the next step in the stream.
<R> Stream<R> map(Function<? super T, ? extends R> mapper)
Stream<String> s = Stream.of("monkey", "gorilla", "bonobo");
s.map(String::length)
.forEach(System.out::print); // 676
flatMap()
- The flatMap() method takes each element in the stream and makes any elements it contains top‐level elements in a single stream.
<R> Stream<R> flatMap(
Function<? super T, ? extends Stream<? extends R>> mapper)
List<String> zero = List.of();
var one = List.of("Bonobo");
var two = List.of("Mama Gorilla", "Baby Gorilla");
Stream<List<String>> animals = Stream.of(zero, one, two);
animals.flatMap(m -> m.stream())
.forEach(System.out::println);
Bonobo
Mama Gorilla
Baby Gorilla
sorted()
- The sorted() method returns a stream with the elements sorted.
Stream<T> sorted()
Stream<T> sorted(Comparator<? super T> comparator)
Stream<String> s = Stream.of("brown-", "bear-");
s.sorted()
.forEach(System.out::print); // bear-brown
Stream<String> s = Stream.of("brown bear-", "grizzly-");
s.sorted(Comparator.reverseOrder())
.forEach(System.out::print); // grizzly-brown bear-
peek()
- it allows us to perform a stream operation without actually changing the stream.
Stream<T> peek(Consumer<? super T> action)
var stream = Stream.of("black bear", "brown bear", "grizzly");
long count = stream.filter(s -> s.startsWith("g"))
.peek(System.out::println).count(); // grizzly
System.out.println(count); // 1
PUTTING TOGETHER THE PIPELINE
var list = List.of("Toby", "Anna", "Leroy", "Alex");
list.stream()
.filter(n -> n.length() == 4)
.sorted()
.limit(2)
.forEach(System.out::println);
Stream.generate(() -> "Elsa")
.filter(n -> n.length() == 4)
.limit(2)
.sorted()
.forEach(System.out::println);
Working with Primitive Streams
CREATING PRIMITIVE STREAMS
- IntStream: Used for the primitive types int, short, byte, and char
- LongStream: Used for the primitive type long
- DoubleStream: Used for the primitive types double and float
Method | Primitive stream | Description |
---|---|---|
OptionalDouble average() | IntStream LongStream DoubleStream |
The arithmetic mean of the elements |
Stream<T> boxed() | IntStream LongStream DoubleStream |
A Stream where T is the wrapper class associated with the primitive value |
OptionalInt max() OptionalLong max() OptionalDouble max() |
IntStream LongStream DoubleStream |
The maximum element of the stream |
OptionalInt min() OptionalLong min() OptionalDouble min() |
IntStream LongStream DoubleStream |
The minimum element of the stream |
IntStream range(int a, int b) LongStream range(long a, long b) |
IntStream LongStream |
Returns a primitive stream from a (inclusive) to b (exclusive) |
IntStream rangeClosed(int a, int b) LongStream rangeClosed(long a, long b) |
IntStream LongStream |
Returns a primitive stream from a (inclusive) to b (inclusive) |
int sum() long sum() double sum() |
IntStream LongStream DoubleStream |
Returns the sum of the elements in the stream |
IntSummaryStatistics summaryStatistics() LongSummaryStatistics summaryStatistics() DoubleSummaryStatistics summaryStatistics() |
IntStream LongStream DoubleStream |
Returns an object containing numerous stream statistics such as the average, min, max, etc. |
Table - Common primitive stream methods
Examples:
Creating streams
DoubleStream empty = DoubleStream.empty();
DoubleStream oneValue = DoubleStream.of(3.14);
oneValue.forEach(System.out::println);
DoubleStream varargs = DoubleStream.of(1.0, 1.1, 1.2);
varargs.forEach(System.out::println);
3.14
1.0
1.1
1.2
- Creating infinite streams
var random = DoubleStream.generate(Math::random);
var fractions = DoubleStream.iterate(.5, d -> d / 2);
random.limit(3).forEach(System.out::println);
fractions.limit(3).forEach(System.out::println);
0.07890654781186413
0.28564363465842346
0.6311403511266134
0.5
0.25
0.125
- Using range
IntStream range = IntStream.range(1, 6);
range.forEach(System.out::println);
IntStream rangeClosed = IntStream.rangeClosed(1, 5);
rangeClosed.forEach(System.out::println);
MAPPING STREAMS
- Another way to create a primitive stream is by mapping from another stream type.
Source stream class | To create Stream | To create DoubleStream | To create IntStream | To create LongStream |
---|---|---|---|---|
Stream | map() | mapToDouble() | mapToInt() | mapToLong() |
DoubleStream | mapToObj() | map() | mapToInt() | mapToLong() |
IntStream | mapToObj() | mapToDouble() | map() | mapToLong() |
LongStream | mapToObj() | mapToDouble() | mapToInt() | map() |
Mapping methods between types of streams
- example
Stream<String> objStream = Stream.of("penguin", "fish");
IntStream intStream = objStream.mapToInt(s -> s.length());
Source stream class | To create Stream | To create DoubleStream | To create IntStream | To create LongStream |
---|---|---|---|---|
Stream | Function | ToDoubleFunction | ToIntFunction | ToLongFunction |
DoubleStream | Double Function | DoubleUnary Operator | DoubleToInt Function | DoubleToLong Function |
IntStream | IntFunction | IntToDouble Function | IntUnary Operator | IntToLong Function |
LongStream | Long Function | LongToDouble Function | LongToInt Function | LongUnary Operator |
Function parameters when mapping between types of streams
- flatmap() on primitive streams has a different method name
var integerList = new ArrayList<Integer>();
IntStream ints = integerList.stream()
.flatMapToInt(x -> IntStream.of(x));
DoubleStream doubles = integerList.stream()
.flatMapToDouble(x -> DoubleStream.of(x));
LongStream longs = integerList.stream()
.flatMapToLong(x -> LongStream.of(x));
USING OPTIONAL WITH PRIMITIVE STREAMS
var stream = IntStream.rangeClosed(1,10);
OptionalDouble optional = stream.average();
optional.ifPresent(System.out::println); // 5.5
System.out.println(optional.getAsDouble()); // 5.5
System.out.println(optional.orElseGet(() -> Double.NaN)); // 5.5
- OptionalDouble is for a primitive and Optional is for the Double wrapper class.
- getAsDouble() rather than get().
- orElseGet() takes a DoubleSupplier instead of a Supplier.
OptionalDouble | OptionalInt | OptionalLong | |
---|---|---|---|
Getting as a primitive | getAsDouble() | getAsInt() | getAsLong() |
orElseGet() parameter type | DoubleSupplier | IntSupplier | LongSupplier |
Return type of max() and min() | OptionalDouble | OptionalInt | OptionalLong |
Return type of sum() | double | int | long |
Return type of average() | OptionalDouble | OptionalDouble | OptionalDouble |
Function parameters when mapping between types of streams
- The sum() method does not return an optional.
- The min() method returns an OptionalDouble.
5: LongStream longs = LongStream.of(5, 10);
6: long sum = longs.sum();
7: System.out.println(sum); // 15
8: DoubleStream doubles = DoubleStream.generate(() -> Math.PI);
9: OptionalDouble min = doubles.min(); // runs infinitely
SUMMARIZING STATISTICS
private static int range(IntStream ints) {
IntSummaryStatistics stats = ints.summaryStatistics();
if (stats.getCount() == 0) throw new RuntimeException();
return stats.getMax()-stats.getMin();
}
- Summary statistics include the following:
- Smallest number (minimum): getMin()
- Largest number (maximum): getMax()
- Average: getAverage()
- Sum: getSum()
- Number of values: getCount()
LEARNING THE FUNCTIONAL INTERFACES FOR PRIMITIVES
Functional Interfaces for boolean
- BooleanSupplier
boolean getAsBoolean()
12: BooleanSupplier b1 = () -> true;
13: BooleanSupplier b2 = () -> Math.random()> .5;
14: System.out.println(b1.getAsBoolean()); // true
15: System.out.println(b2.getAsBoolean()); // false
Functional Interfaces for double, int, and long
Common functional interfaces for primitives
Functional interfaces | # parameters | Return type | Single abstract method |
---|---|---|---|
DoubleSupplier IntSupplier LongSupplier |
0 | double int long |
getAsDouble getAsInt getAsLong |
DoubleConsumer IntConsumer LongConsumer |
1 (double) 1 (int) 1 (long) |
void | accept |
DoublePredicate IntPredicate LongPredicate |
1 (double) 1 (int) 1 (long) |
boolean | test |
DoubleFunction<R> IntFunction<R> LongFunction<R> |
1 (double) 1 (int) 1 (long) |
R | apply |
DoubleUnaryOperator IntUnaryOperator LongUnaryOperator |
1 (double) 1(int) 1(long) |
double int long |
applyAsDouble applyAsInt applyAsLong |
DoubleBinaryOperator IntBinaryOperator LongBinaryOperator |
2 (double, double) 2 (int, int) 2 (long, long) |
double int long |
applyAsDouble applyAsInt applyAsLong |
Primitive‐specific functional interfaces
Functional interfaces | # parameters | Return type | Single abstract method |
---|---|---|---|
ToDoubleFunction ToIntFunction ToLongFunction |
1 (T) | double int long |
applyAsDouble applyAsInt applyAsLong |
ToDoubleBiFunction ToIntBiFunction ToLongBiFunction |
2 (T, U) | double int long |
applyAsDouble applyAsInt applyAsLong |
DoubleToIntFunction DoubleToLongFunction IntToDoubleFunction IntToLongFunction LongToDoubleFunction LongToIntFunction |
1 (double) 1 (double) 1 (int) 1 (int) 1 (long) 1 (long) |
int long double long double int |
|
applyAsInt applyAsLong applyAsDouble applyAsLong applyAsDouble applyAsInt |
|||
ObjDoubleConsumer ObjIntConsumer ObjLongConsumer |
2 (T, double) 2 (T, int) 2 (T, long) |
void | accept |
COLLECTING RESULTS
- There are many predefined collectors
- These collectors are available via static methods on the Collectors interface.
Examples of grouping/partitioning collectors
Collector | Description | Return value when passed to collect |
---|---|---|
averagingDouble(ToDoubleFunction f) averagingInt(ToIntFunction f) averagingLong(ToLongFunction f) |
Calculates the average for our three core primitive types | Double |
counting() | Counts the number of elements | Long |
groupingBy(Function f) groupingBy(Function f, Collector dc) groupingBy(Function f, Supplier s, Collector dc) |
Creates a map grouping by the specified function with the optional map type supplier and optional downstream collector | Map> |
joining(CharSequence cs) | Creates a single String using cs as a delimiter between elements if one is specified | String |
maxBy(Comparator c) minBy(Comparator c) |
Finds the largest/smallest elements | Optional |
mapping(Function f, Collector dc) | Adds another level of collectors | Collector |
partitioningBy(Predicate p) partitioningBy(Predicate p, Collector dc) |
Creates a map grouping by the specified predicate with the optional further downstream collector | Map> |
summarizingDouble(ToDoubleFunction f) summarizingInt(ToIntFunction f) summarizingLong(ToLongFunction f) |
Calculates average, min, max, and so on | DoubleSummaryStatistics IntSummaryStatistics LongSummaryStatistics |
summingDouble(ToDoubleFunction f) summingInt(ToIntFunction f) summingLong(ToLongFunction f) |
Calculates the sum for our three core primitive types | Double Integer Long |
toList() toSet() |
Creates an arbitrary type of list or set | List Set |
toCollection(Supplier s) | Creates a Collection of the specified type | Collection |
toMap(Function k, Function v) toMap(Function k, Function v, BinaryOperator m) toMap(Function k, Function v, BinaryOperator m, Supplier s) |
Creates a map using functions to map the keys, values, an optional merge function, and an optional map type supplier | Map |
Collecting Using Basic Collectors
var ohMy = Stream.of("lions", "tigers", "bears");
String result = ohMy.collect(Collectors.joining(", "));
System.out.println(result); // lions, tigers, bears
var ohMy = Stream.of("lions", "tigers", "bears");
Double result = ohMy.collect(Collectors.averagingInt(String::length));
System.out.println(result); // 5.333333333333333
var ohMy = Stream.of("lions", "tigers", "bears");
TreeSet<String> result = ohMy
.filter(s -> s.startsWith("t"))
.collect(Collectors.toCollection(TreeSet::new));
System.out.println(result); // [tigers]
Collecting into Maps
- you need to specify two functions.
- The first function tells the collector how to create the key.
- The second function tells the collector how to create the value.
var ohMy = Stream.of("lions", "tigers", "bears");
Map<String, Integer> map = ohMy.collect(
Collectors.toMap(s -> s, String::length));
System.out.println(map); // {lions=5, bears=5, tigers=6}
- In case of store the same key two times, the collector has no idea what to do, it “solves” the problem by throwing an exception and making it our problem.
var ohMy = Stream.of("lions", "tigers", "bears");
Map<Integer, String> map = ohMy.collect(Collectors.toMap(
String::length,
k -> k)); // BAD
Exception in thread "main"
java.lang.IllegalStateException: Duplicate key 5
- we can
var ohMy = Stream.of("lions", "tigers", "bears");
Map<Integer, String> map = ohMy.collect(Collectors.toMap(
String::length,
k -> k,
(s1, s2) -> s1 + "," + s2));
System.out.println(map); // {5=lions,bears, 6=tigers}
System.out.println(map.getClass()); // class java.util.HashMap
- if you want to save in a TreeMap
var ohMy = Stream.of("lions", "tigers", "bears");
TreeMap<Integer, String> map = ohMy.collect(Collectors.toMap(
String::length,
k -> k,
(s1, s2) -> s1 + "," + s2,
TreeMap::new));
System.out.println(map); // // {5=lions,bears, 6=tigers}
System.out.println(map.getClass()); // class java.util.TreeMap
Collecting Using Grouping, Partitioning, and Mapping
- suppose that we want to get groups of names by their length.
var ohMy = Stream.of("lions", "tigers", "bears");
Map<Integer, List<String>> map = ohMy.collect(
Collectors.groupingBy(String::length));
System.out.println(map); // {5=[lions, bears], 6=[tigers]}
Note that the function you call in groupingBy() cannot return null. It does not allow null keys.
Suppose that we don't want a List as the value in the map and prefer a Set instead.
var ohMy = Stream.of("lions", "tigers", "bears");
Map<Integer, Set<String>> map = ohMy.collect(
Collectors.groupingBy(
String::length,
Collectors.toSet()));
System.out.println(map); // {5=[lions, bears], 6=[tigers]}
- We can even change the type of Map returned through yet another parameter.
var ohMy = Stream.of("lions", "tigers", "bears");
TreeMap<Integer, Set<String>> map = ohMy.collect(
Collectors.groupingBy(
String::length,
TreeMap::new,
Collectors.toSet()));
System.out.println(map); // {5=[lions, bears], 6=[tigers]}
- other option
var ohMy = Stream.of("lions", "tigers", "bears");
Map<Integer, Long> map = ohMy.collect(
Collectors.groupingBy(
String::length,
Collectors.counting()));
System.out.println(map); // {5=2, 6=1}
- Suppose that we wanted to get the first letter of the first animal alphabetically of each length.
- mapping() collector that lets us go down a level and add another collector.
- takes two parameters: the function for the value and how to group it further.
var ohMy = Stream.of("lions", "tigers", "bears");
Map<Integer, Optional<Character>> map = ohMy.collect(
Collectors.groupingBy(
String::length,
Collectors.mapping(
s -> s.charAt(0),
Collectors.minBy((a, b) -> a -b))));
System.out.println(map); // {5=Optional[b], 6=Optional[t]}
- Partitioning is a special case of grouping.
- two possible groups—true and false.
- unlike groupingBy(), we cannot change the type of Map that gets returned.
var ohMy = Stream.of("lions", "tigers", "bears");
Map<Boolean, List<String>> map = ohMy.collect(
Collectors.partitioningBy(s -> s.length() <= 5));
System.out.println(map); // {false=[tigers], true=[lions, bears]}
- modifying from List to Set
var ohMy = Stream.of("lions", "tigers", "bears");
Map<Boolean, Set<String>> map = ohMy.collect(
Collectors.partitioningBy(
s -> s.length() <= 7,
Collectors.toSet()));
System.out.println(map); // {false=[], true=[lions, tigers, bears]}
REFERENCES
Posted on May 13, 2022
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.