java 8 - functional features
TRANSCRIPT
Agenda
1. functional paradigm2. lambdas3. functional interfaces4. default methods5. streams
a. parallel iterationb. lazy evaluation
6. map & flat map7. maps
Functional paradigm
● Treats computation as the evaluation of mathematical functions and avoids changing-state and mutable data.
● Roots in lambda calculus○ Alonzo Church developed concept in 1930○ John McCarthy developed Lisp in 1950s
Functional concepts
● Referential transparency○ Expression can be replaced by the value it returns
● Pure function○ The result depends only on input value, not state○ Evaluation does not cause side-effects
● Idempotence○ Operation with side effects that can be applied multiple time without changing the result
beyond initial application.○ ex. HTTP GET, PUT, DELETED (without POST)
● Higher order function○ Function that takes another function as argument
Functional concepts (2)
● Map○ Apply function to collection items
● Reduce○ Combine input collection into single value result
● Currying○ Translation of function with multiple arguments into higher-order functions
Declarative vs imperative
● Imperative - instructions that changes state○ Assembler○ C
● Declarative - describe what you want○ HTML○ AngularJs (directives)
Declarative vs imperative
Imperative Declarative
getRemoteData("example.com", { data, error in if error == nil { parseData(data, { parsed, error in if error == nil { handleParsedData(parsed, { error in if error != nil { displayError(error) } }) } else { displayError(error) } }) } else { displayError(error) }}
getRemoteData("example.com") .then(parseData) .then(handleParsedData) .onError(displayError)
Declarative vs imperative
● Imperative - “Please take the glass, pour the water and pass it to me.”
● Declarative - “I want to drink water.”
Declarative vs imperative
Declarative programming helps in:
● Direct translation of the business model into business logic● Better readability of the business logic● Better scalability for the program in terms of functionality (reusability)● Easies testing - due to better isolation and loose coupling● Less bugs (quality and safety - due to side effects and avoiding state)
Imperative is mainstream
Imperative programming paradigm is:
● the most popular,● the easiest,● delivers not best outcome in terms of maintenance,
Imperative programming is mainstream because all of us have been taught it while learning - that’s why it’s mainstream.
Difficulties in declarative programming
● It requires separating small responsibilities● It requires good quality, clear unit tests (instead of debugging)● It requires trust in quality of the implementation● It required transition in thinking (learning it)
getRemoteData("example.com") .then(parseData) .then(handleParsedData) .onError(displayError)
Functional - disadvantages
● Performance of more complex algorithms is lower● Sometimes side-effects are required
○ ex. storing in session○ -> functional approach helps to split stateful and stateless parts
● For some algorithms it decreases readibility
Lambdas
Definition: Lambda is anonymous function.
● Function is first-class citizen● Short syntax
String x = “Hello”;Function y = System.out::println;
y.apply(x);
Lambdas
() -> {}
(input) -> {}
() -> {output}
(input) -> {output}
input -> {}
() -> output () -> {return output;}
input -> output
Lambdas
() -> {}
(input) -> {}
() -> {output}
(input) -> {output}
input -> {}
() -> output () -> {return output;}
input -> output
(input1, input2) -> output
Lambdas - functional interfaces
Runnable r = () -> {}
Consumer c = (input) -> {}
Supplier s = () -> {output}
Function f = (input) -> {output}
Lambdas - functional interfaces
BiConsumer bc = (input1, input2) -> {}
UnaryOperator negate = integer -> -integer
BinaryOperator add = (int1, int2) -> int1 + int2
Predicate p = input -> boolean
BiPredicate bp = (input1, input2) -> boolean
Lambda under the hood
Anonymous function:
● Created in compiletime● Resides in the same folder as enclosing class
Lambda:
1. On first run code is generated in runtime using invokedynamic 2. invokedynamic is replaced with code equivalent to anonymous class3. Performance of generated code is the same as anonymous class
● Java 8 introduced new methods in interfaces, like:
● Implementation:
Default methods
Iterable.forEach(Consumer<? super T> action)
default void forEach(Consumer<? super T> action) { Objects.requireNonNull(action); for (T t : this) { action.accept(t); }}
The goal:
● Allow Oracle to extend collection interfaces● Allow everybody extend interfaces without breaking compatibility.
Is it useful?
● Not very often.● Keep compatibility in case stateless method are added.
Default methods
● Problem known in languages with multiple inheritance (like C++)
Deadly diamond of death
interface A { default void action() { System.out.println("A"); }}
interface B { default void action() { System.out.println("B"); }}
interface AB extends A, B ?
● Problem:
● Solution:
Deadly diamond of death
interface A { default void action() { System.out.println("A"); }}
interface B { default void action() { System.out.println("B"); }}
interface AB extends A, B { @Override void action();}
interface AB extends A, B { @Override default void action() { A.super.action(); }}
or
Streams
● Streams are for operations● Collections are for storing.
Most often, it is required to process operations rather than store data.
Streams - useful methods
● map● filter● distinct● skip, limit● peek
● min, max● count● findAny, findFirst
users.stream().map(...).filter(...).distinct().skip(5).limit(10).peek(item -> System.out.print(item))
.count();
● Heavy operations pipelined:
Streams are lazy
final Stream<Integer> inputs = // 0, 1, 2, 3, 4, 5, 6, 7, 8, 9 IntStream.range(0, 10).boxed();
inputs .map(new HeavyOperation("A")) .map(new HeavyOperation("B")) .map(new HeavyOperation("C")) .filter(input -> input == 3) .collect(toList());
● Heavy operation - prints to console
Streams are lazy
class HeavyOperation {
public Integer apply(Integer input) { System.out.println("Heavy operation " + operationName + " for element " + input); return input; }}
● Heavy operations on all items
Streams are lazy
final Stream<Integer> inputs = IntStream.range(0, 10).boxed();
inputs .map(new HeavyOperation("A")) .map(new HeavyOperation("B")) .map(new HeavyOperation("C")) .collect(toList());
Heavy operation A for element 0Heavy operation B for element 0Heavy operation C for element 0Heavy operation A for element 1Heavy operation B for element 1Heavy operation C for element 1Heavy operation A for element 2Heavy operation B for element 2Heavy operation C for element 2Heavy operation A for element 3Heavy operation B for element 3Heavy operation C for element 3Heavy operation A for element 4Heavy operation B for element 4Heavy operation C for element 4Heavy operation A for element 5Heavy operation B for element 5Heavy operation C for element 5Heavy operation A for element 6Heavy operation B for element 6Heavy operation C for element 6Heavy operation A for element 7Heavy operation B for element 7Heavy operation C for element 7Heavy operation A for element 8Heavy operation B for element 8Heavy operation C for element 8Heavy operation A for element 9Heavy operation B for element 9Heavy operation C for element 9
CONSOLE OUTPUT
● Only required operations
Streams are lazy
final Stream<Integer> inputs = IntStream.range(0, 10).boxed();
inputs .map(new HeavyOperation("A")) .map(new HeavyOperation("B")) .map(new HeavyOperation("C")) .filter(input -> input == 3) .findFirst();
Heavy operation A for element 0Heavy operation B for element 0Heavy operation C for element 0Heavy operation A for element 1Heavy operation B for element 1Heavy operation C for element 1Heavy operation A for element 2Heavy operation B for element 2Heavy operation C for element 2Heavy operation A for element 3Heavy operation B for element 3Heavy operation C for element 3
CONSOLE OUTPUT
● No collection operation
Streams are lazy
final Stream<Integer> inputs = IntStream.range(0, 10).boxed();
inputs .map(new HeavyOperation("A")) .map(new HeavyOperation("B")) .map(new HeavyOperation("C")) .filter(input -> input == 3);
CONSOLE OUTPUT
● Collection function
● Reduction function (fold function)
list .stream() .collect(toList());
Streams - operations output
int sum = integers .stream() .sum();
int sum = integers .stream() .reduce(0, (total, item) -> total + item);
Streams - parallel execution
List inputs = ...
inputs .parallelStream() .map(new HeavyOperation("A")) .map(new HeavyOperation("B")) .map(new HeavyOperation("C")) .findFirst();
Streams - collectors
● Aggregating
● Comparing
● Grouping
List<User> allUsers = users.stream().collect(toList());
User userWithMaxLogins = users.stream() .collect( maxBy( comparing(User::loginsCount) ) );
Map<Role, List<User>> usersPerRole = users.stream() .collect( groupingBy( User::getRole) );
Streams - collectors
● Partitioning
Map<Boolean, List<User>> activeUsers = stream .collect( partitioningBy( user -> user.getLoginsCount() > 0));
Flat mapping
● Stream: Combine stream of streams into single stream.
● Optional: Combine optional of optional into single optional.○ A pipeline of operations out of which any may fail
Flat mapping
private Optional<Banana> fetchBananaImperative() { Optional<Island> island = findIsland();
boolean noIslandFound = !island.isPresent(); if (noIslandFound) { return empty(); }
Optional<Jungle> jungle = findJungle(island.get());
boolean noJungleFound = !jungle.isPresent(); if (noJungleFound) { return empty(); }
Optional<Tree> tree = findTree(jungle.get());
boolean noTreeFound = !tree.isPresent(); if (noTreeFound) { return empty(); }
Optional<Banana> banana = findBanana(tree.get());
boolean noBananaFound = !banana.isPresent(); if (noBananaFound) { return empty(); }
return banana; }
Flat mapping
private Optional<Banana> fetchBananaImperative() { Optional<Island> island = findIsland();
boolean noIslandFound = !island.isPresent(); if (noIslandFound) { return empty(); }
Optional<Jungle> jungle = findJungle(island.get());
boolean noJungleFound = !jungle.isPresent(); if (noJungleFound) { return empty(); }
Optional<Tree> tree = findTree(jungle.get());
boolean noTreeFound = !tree.isPresent(); if (noTreeFound) { return empty(); }
Optional<Banana> banana = findBanana(tree.get());
boolean noBananaFound = !banana.isPresent(); if (noBananaFound) { return empty(); }
return banana; }
private Optional<Banana> fetchBananaFluent() { Optional<Island> islandOptional = findIsland();
Optional<Banana> banana = islandOptional .flatMap(island -> findJungle(island)) .flatMap(jungle -> findTree(jungle)) .flatMap(tree -> findBanana(tree));
return banana;}
Map - new methods
map.compute(key, (key, value) -> newValue);
map.putIfAbsent(key, value);
map.replace(key, value);
map.replace(key, oldValue, newValue);
map.getOrDefault(key, defaultValue);
map.merge(key, newValue, (oldValue, newValue) -> mergedValue);
Lambda vs method reference
Optional<Banana> banana = islandOptional .flatMap(island -> findJungle(island)) .flatMap(jungle -> findTree(jungle)) .flatMap(tree -> findBanana(tree));
Lambda vs method reference
Optional<Banana> banana = islandOptional .flatMap(island -> findJungle(island)) .flatMap(jungle -> findTree(jungle)) .flatMap(tree -> findBanana(tree));
Optional<Banana> banana = islandOptional .flatMap(island -> findJungle(island)) .flatMap(Jungle::getTree) .flatMap(tree -> tree.getBanana());
● Syntax may affect readibility
Lambda vs method reference
Options:
● allow mixing lambda and method reference● only lambdas
Optional<Banana> banana = islandOptional .flatMap(island -> findJungle(island)) .flatMap(Jungle::getTree) .flatMap(tree -> tree.getBanana());