The document discusses the introduction and advantages of lambda expressions and functional programming in Java 8. Some key points include:
- Lambda expressions allow passing behaviors as arguments to methods, simplifying code by removing bulky anonymous class syntax. This enables more powerful and expressive APIs.
- Streams provide a way to process collections of data in a declarative way, leveraging laziness to improve efficiency. Operations can be pipelined for fluent processing.
- Functional programming with immutable data and avoidance of side effects makes code more modular and easier to reason about, enabling optimizations like parallelism. While not natively supported, Java 8 features like lambda expressions facilitate a more functional approach.
SyndBuddy AI 2k Review 2024: Revolutionizing Content Syndication with AI
Java 8 Workshop
1. by Mario Fusco
mario.fusco@gmail.com
twitter: @mariofusco
8 in Action
Lambdas, Streams, and functional-style programming
2. Project Lambda – A Minimal History
➢ 2006 – Gosling: "We will never have lambdas in Java"
➢ 2007 – 3 different proposals for lambdas in Java
➢ 2008 – Reinhold: "We will never have lambdas in Java"
➢ 2009 – Start of project Lambda (JSR 335)
public boolean willJavaHaveLambdas() {
return currentYear % 2 == 1;
}
3. From Single Method Interfaces …
public interface Comparator<T> {
int compare(T o1, T o2);
Functional
Interface
}
Collections.sort(strings, new Comparator<String>() {
public int compare(String s1, String s2) {
return s1.compareToIgnoreCase(s2);
}
});
➢ Bulky syntax
➢ Confusion surrounding the meaning of names and this
➢ Inability to capture non-final local variables
➢ Inability to abstract over control flow
4. … To Lambda Expressions
Collections.sort(strings,(s1, s2) -> s1.compareToIgnoreCase(s2));
Lambda expression are always converted to
instance of a functional interface
Comparator<String> c = (s1, s2) -> s1.compareToIgnoreCase(s2);
Compiler figures out the types
No need of changing the JVM to create a new
type for lambda expressions
5. Common JDK8 @FunctionalInterfaces
➢ Predicate<T> → test a property of the object passed as argument
➢ Consumer<T> → execute an action on the object passed as argument
➢ Function<T, U> → transform a T to a U
➢ BiFunction<T, U, V> → transform a (T, U) to a V
➢ Supplier<T> → provide an instance of a T (such as a factory)
➢ UnaryOperator<T> → a unary operator from T -> T
➢ BinaryOperator<T> → a binary operator from (T, T) -> T
Give a look at java.util.function.*
6. Anatomy of a lambda expression
A lambda expression is like an (anonymous) method:
it provides a list of formal parameters and a body
s -> s.length()
(int x, int y) -> x + y
() -> 42
(x, y, z) -> {
if (x) {
return y;
} else {
return z;
}
}
The formal parameters of
a lambda expression may
have either inferred or
declared types
Return is implicit and
can be omitted
A lambda body is either a
single expression or a block
7. However …
… syntax is probably the less important
thing about lambda expression …
… the really fundamental thing about
lambda expression is …
… the huge paradigm shift they imply
8. Why Lambdas?
➢ Behaviors can be passed to a method together with data
➢ API designers can build more powerful, expressive APIs
➢ More room for generalization
➢ Pass behaviors to a method together with normal data
➢ Libraries remain in control of computation
➢ e.g. internal vs. external iteration
➢ More opportunities for optimization
➢ Laziness
➢ Parallelism
➢ Out-of-order execution
➢ More regular and then more readable code
➢ e.g. nested loops vs. pipelined (fluent) operations
➢ Better composability and reusability
11. Internal vs External Iteration
for (Employee e : employees) {
e.setSalary(e.getSalary() * 1.03);
}
̶ Inherently serial
̶ Client has to manage iteration
̶ Nested loops are poorly readable
employees.forEach(e -> e.setSalary(e.getSalary() * 1.03));
Not only a syntactic change!
+ Library is in control → opportunity for internal optimizations as parallelization,
lazy evaluation, out-of-order execution
+ More what, less how → better readability
+ Fluent (pipelined) operations → better readability
+ Client can pass behaviors into the API as data →
possibility to abstract and generalize over behavior →
more powerful, expressive APIs
12. Sorting with Lambdas
Comparator<Person> byAge = new Comparator<Person>() {
public int compare(Person p1, Person p2) {
return p1.getAge() – p2.getAge();
}
};
Collections.sort(people, byAge);
Functional interface
Lambda expression
Comparator<Person> byAge = (p1, p2) -> p1.getAge() – p2.getAge();
Collections.sort(people, (p1, p2) -> p1.getAge() – p2.getAge());
13. Can We Do Better?
Comparator<Person> byAge = Comparators.comparing(p -> p.getAge());
Comparator<Person> byAge = Comparators.comparing(Person::getAge);
Method reference
Readability
Collections.sort(people, comparing(Person::getAge));
Collections.sort(people, comparing(Person::getAge).reverse());
Collections.sort(people, comparing(Person::getAge)
.compose(comparing(Person::getName)));
Reusability
Composability
15. OOP vs FP
OOP makes code understandable
by encapsulating moving parts
FP makes code understandable
by minimizing moving parts
- Michael Feathers
16. The OOP/FP dualism - OOP
public class Bird { }
public class Cat {
private Bird catch;
private boolean full;
public void capture(Bird bird) {
catch = bird;
}
public void eat() {
full = true;
catch = null;
}
}
Cat cat = new Cat();
Bird bird = new Bird();
cat.capture(bird);
cat.eat();
The story
17. The OOP/FP dualism - FP
public class Bird { }
public class Cat {
public CatWithCatch capture(Bird bird) { return new CatWithCatch(bird); }
}
public class CatWithCatch {
private final Bird catch;
public CatWithCatch(Bird bird) { catch = bird; }
public FullCat eat() { return new FullCat(); }
}
public class FullCat { }
BiFunction<Cat, Bird, FullCat> story =
((BiFunction<Cat, Bird, CatWithCatch>)Cat::capture)
.compose(CatWithCatch::eat);
FullCat fullCat = story.apply( new Cat(), new Bird() );
Immutability
Emphasis on verbs
instead of names
No need to test internal state: correctness enforced by the compiler
18. Streams - Efficiency with laziness
List<Employee> employess = ...
employees.stream()
.filter(e -> e.getIncome() > 50000)
.map(e -> e.getName())
.forEach(System.out::println);
Represents a sequence of element from a source
Not a data structure: doesn't store elements but compute them on demand
Sources can be Collection, array, generating function, I/O ....
Encourages a pipelined ( "fluent" ) usage style
Operations are divided between intermediate and terminal
Lazy in nature: only terminal operations actually trigger a computation
19. Evolving APIs with default methods
Where does that stream() method come from?
public interface Collection<E> extends Iterable<E> {
...
default Stream<E> stream() {
return StreamSupport.stream(spliterator(), false);
}
}
✔ Multiple inheritance of type since Java 1.0
✔ Java 8 introduces multiple inheritance of behavior
✔ No multiple inheritance of state (like in Scala's traits)
20. Default methods resolution rules
1.Classes always win: a method declaration in the class or a
superclass takes priority over any default method declaration.
2.Then sub-interfaces win: the method with the same signature in
the most specific default-providing interface is selected.
3.If the choice is still ambiguous, the class inheriting from multiple
interfaces has to explicitly select which default method
implementation to use by overriding it and calling the desired
method explicitly.
public interface A {
default void hello() { System.out.println("Hello from A"); }
} public interface B {
default void hello() { System.out.println("Hello from B"); }
} public class C implements B, A {
void hello() { B.super.hello(); }
}
22. Intermediate & Terminal Operations
Return the final result of
the operation pipeline
Intermediate
distinct
map, flatMap
limit, skip
peek
sorted
Terminal
collect
count
forEach
min, max
reduce
toArray
findAny, findFirst
allMatch,
anyMatch,
noneMatch
Return another Stream and
then can be chained to form
a pipeline of operations
23. map : an intermediate operation
map( -> )
Stream
24. reduce : a terminal operation
Stream
Integer
reduce(0, (a, b) -> a + b)
25. Putting them all together
List<String> strings = asList("Lambda", "expressions", "are",
"easy", "and", "useful");
int totalLength = strings.stream()
.map(String::length)
.reduce(0, (a, b) -> a + b);
26. Stream Operations Pipelining
Intermediate Operations are lazy and return another
Stream allowing to fluently pipeline other operations
Terminal Operations are eager and computes the result of
the whole pipeline
30. Grouping
Map<Dish.Type, List<Dish>> dishesByType = new HashMap<>();
for (Dish dish : menu) {
Dish.Type type = dish.getType();
List<Dish> dishes = dishesByType.get(type);
if (dishes == null) {
dishes = new ArrayList<>();
dishesByType.put(type, dishes);
}
dishes.add(dish);
}
31. Grouping with Collectors
apply key
classify item into list
next
item
grouping Map
fish meat other
salmon pizza
rice
french fries
pork
beef
chicken
prawns
Classification
Function fish
Stream
Map<Dish.Type, List<Dish>> dishesByType =
menu.stream()
.collect(groupingBy(Dish::getType));
33. groupingBy
Stream
Dish::getType
classification
function
Complex Grouping
subStream subStream subStream
collectingAndThen
reducing
result
Optional[pork]
Optional::get
result
grouping Map
collectingAndThen
fish meat other
salmon
pork pizza
reducing
collectingAndThen
reducing
result result
The original Stream is
divided in subStreams
according to the
classification function
Each subStreams is
independently
processed by the
second Collector
transformation
function
The reducing Collector
returns the most caloric Dish
wrapped in an Optional
The collectingAndThen Collector
returns the value extracted from
the former Optional
The results of the 2nd
level Collectors
become the values of
the grouping map
34. Streams – Parallelism for free
parallelStream()
employees.stream()
.filter(e -> e.getRole() == Role.MANAGER)
.map(Employee::getIncome)
.reduce(0, (a, b) -> a + b);
38. Probably yes …
… but we need functional
forks and knives to eat it
39. Concurrency & Parallelism
Parallel programming
Running multiple tasks at
the same time
Concurrent programming
Managing concurrent requests
Both are hard!
40. The cause of the problem …
Mutable state +
Parallel processing =
Non-determinism
Functional
Programming
42. The native Java concurrency model
Based on:
Locks Synchronization
They are sometimes plain evil …
Threads
Semaphores
… and sometimes a necessary pain …
… but always the wrong default
43. Different concurrency models
Isolated mutable state (actors)
Purely immutable (pure functions)
Shared mutable state
(threads + locks)
44. Summing attendants ages (Threads)
class Blackboard {
int sum = 0;
int read() { return sum; }
void write(int value) { sum = value; }
}
class Attendant implements Runnable {
int age;
Blackboard blackboard;
public void run() {
synchronized(blackboard) {
int oldSum = blackboard.read();
int newSum = oldSum + age;
blackboard.write(newSum);
}
}
}
45. Summing attendants ages (Actors)
class Blackboard extends UntypedActors {
int sum = 0;
public void onReceive(Object message) {
if (message instanceof Integer) {
sum += (Integer)message;
}
}
}
class Attendant {
int age;
Blackboard blackboard;
public void sendAge() {
blackboard.sendOneWay(age);
}
}
46. Summing attendants ages (Functional)
class Blackboard {
final int sum;
Blackboard(int sum) { this.sum = sum; }
}
class Attendant {
int age;
Attendant next;
public Blackboard addMyAge(Blackboard blackboard) {
final Blackboard b = new Blackboard(blackboard.sum + age);
return next == null ? b : next.myAge(b);
}
}
47. The state quadrants
Mutable
Immutable
Shared
Unshared
Actors
Threads
Functional
Programming
Determinism
Non-determinism
48. What is a functional program?
A program created using only pure functions
No side effects allowed like:
}
}avoidable
Reassigning a variable
Modifying a data structure in place
Setting a field on an object
Throwing an exception or halting with an error
Printing to the console
Reading user input
Reading from or writing to a file
Drawing on the screen
deferrable
Functional programming is a restriction on how we write
programs, but not on what they can do
50. A pure functional core
functional
core
a thin external layer to handle side-effects
Any function with side-effects can be split into a pure function at the
core and a pair of functions with side-effect. This transformation can
be repeated to push side-effects to the outer layers of the program.
51. What is a (pure) function?
A function with input type A and output type B
is a computation which relates every value a of
type A to exactly one value b of type B such
that b is determined solely by the value of a
But, if it really is a
function, it will do
nothing else
52. Referential transparency
An expression e is referentially transparent if for all programs p,
all occurrences of e in p can be replaced by the result of
evaluating e, without affecting the observable behavior of p
A function f is pure if the expression f(x) is referentially
transparent for all referentially transparent x
54. RT wins
Under a developer point of view:
Easier to reason about since effects of evaluation are purely
local
Use of the substitution model: it's possible to replace a term
with an equivalent one
Under a performance point of view:
The JVM is free to optimize the code by safely reordering the
instructions
No need to synchronize access to shared data
Possible to cache the result of time consuming functions
(memoization), e.g. with
Map.computeIfAbsent(K key,
Function<? super K,? extends V> mappingFunction)
55. Mutability
Parameter binding is about assigning names to things
Mutating variables is about assigning things to names
Does that second
one sound weird?
… well it's because
it IS weird
56. Immutability
Immutable objects can be shared among
many threads exactly because none of
them can modify it
In the same way immutable (persistent)
data structures can be shared without any
need to synchronize the different threads
accessing them
57. Old Root
New Root
5
8
7 9
3
4
E E E E E E
E
5
3
2
E E
Persistent Collections
Shared data
60. Null references? No, Thanks
✗ Errors source → NPE is by far the most common exception in Java
✗ Bloatware source → Worsen readability by making necessary to fill our code
with null checks
✗ Meaningless → Don't have any semantic meaning and in particular are the
wrong way to model the absence of a value in a statically typed language
✗ Breaks Java philosophy → Java always hides pointers to developers, except
in one case: the null pointer
✗ A hole in the type system → Null has the bottom type, meaning that it can
be assigned to any reference type: this is a problem because, when
propagated to another part of the system, you have no idea what that null
was initially supposed to be
Tony Hoare, who invented the null reference in 1965 while working on
an object oriented language called ALGOL W, called its invention his
“billion dollar mistake”
61. Replacing nulls with Optionals
If nulls are so problematic why don't we just avoid them?
Optional
value
value
Optional
EMPTY
null
Optional is a type that models a possibly missing value
62. public class Person {
private Car car;
public Car getCar() { return car; }
}
public class Car {
private Insurance insurance;
public Insurance getInsurance() { return insurance; }
}
public class Insurance {
private String name;
public String getName() { return name; }
}
Finding Car's Insurance Name
63. String getCarInsuranceName(Person person) {
if (person != null) {
Car car = person.getCar();
if (car != null) {
Insurance insurance = car.getInsurance();
if (insurance != null) {
return insurance.getName()
}
}
}
return "Unknown";
}
Attempt 1: deep doubts
64. Attempt 2: too many choices
String getCarInsuranceName(Person person) {
if (person == null) {
return "Unknown";
}
Car car = person.getCar();
if (car == null) {
return "Unknown";
}
Insurance insurance = car.getInsurance();
if (insurance == null) {
return "Unknown";
}
return insurance.getName()
}
65. Optional to the rescue
public class Optional<T> {
private static final Optional<?> EMPTY = new Optional<>(null);
private final T value;
private Optional(T value) {
this.value = value;
}
public<U> Optional<U> map(Function<? super T, ? extends U> f) {
return value == null ? EMPTY : new Optional(f.apply(value));
}
public<U> Optional<U> flatMap(Function<? super T, Optional<U>> f) {
return value == null ? EMPTY : f.apply(value);
}
}
66. public class Person {
private Optional<Car> car;
public Optional<Car> getCar() { return car; }
}
public class Car {
private Optional<Insurance> insurance;
public Optional<Insurance> getInsurance() { return insurance; }
}
public class Insurance {
private String name;
public String getName() { return name; }
}
Rethinking our model
Using the type system
to model nullable value
76. Thinking in Functions
Learning a new language is relatively easy
compared with learning a new paradigm.
Functional Programming is more a new way of
thinking than a new tool set
79. Futures, weren't they enough?
The Future interface was introduced in Java 5 to model an
asynchronous computation and then provide an handle to a result
that will be made available at some point in the future.
But it doesn't allow to:
✗ Combining two asynchronous
computations in one
✗ Waiting for the completion of all
tasks performed by a set of Futures
✗ Waiting for the completion of only
the quickest task in a set of Futures
(possibly because they’re trying to
calculate the same value in different
ways) and retrieving its result
✗ Programmatically completing a
Future
✗ Reacting to a Future completion
80. CompletableFutures to the rescue
✔ Programmatically completing a Future with a result …
boolean complete(T value)
… or an error
boolean completeExceptionally(Throwable ex)
✔ Reacting to Future completion
CompletableFuture<Void> thenAccept(Consumer<? super T> action)
✔ Combining 2 async operation in sequence ...
static CompletableFuture<U> thenCompose(
Function<? super T,? extends CompletionStage<U>> fn)
… or in parallel
static CompletableFuture<V> thenCombine(
CompletionStage<? extends U> other,
BiFunction<? super T,? super U,? extends V> fn)
✔ Waiting for the completion of all Futures ...
static CompletableFuture<Void> allOf(CompletableFuture<?>... cfs)
… or only the fastest one
static CompletableFuture<Object> anyOf(CompletableFuture<?>... cfs)
82. Key Takeaways
Think in functions
Strive for immutability
Confine side-effects
Avoid blocking code
Cede control with higher-order functions
Leverage referential transparency
Use FP to design more composable and reusable API
Model potentially missing values with Optionals
… but there are no dogmas
Be pragmatic and use the right tool for the job at hand
Poly-paradigm programming is more powerful
and effective than polyglot programming