45
Java 8 Functional features 2015-11-09 Rafał Rybacki

Java 8 - functional features

Embed Size (px)

Citation preview

Java 8Functional features

2015-11-09 Rafał Rybacki

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 mainstream.

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

() -> {}

Lambdas

() -> {}

(input) -> {} input -> {}

Lambdas

() -> {}

(input) -> {}

() -> {output}

input -> {}

() -> output () -> {return output;}

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());

Thanks.