java 8: more readable and flexible code
DESCRIPTION
In this presentation we introduce Java 8’s main new features and show how you can benefit from them to write code that is more readable and more flexible to requirement changes. We will show how: 1) Lambda expressions and behaviour parameterisation let you write concise code that can cope for requirement changes 2) The new Streams API lets you express complex data process queries in a succinct way while automatically leveraging your multi-core architecture 3) Using the new Optional class can let you reduce unexpected NullPointer exceptions 4) Default methods bring a form of multi-inheritance to JavaTRANSCRIPT
Wri$ng more concise and flexible code with Java 8
Raoul-‐Gabriel Urma @raoulUK
1
uname -‐a • PhD at University of Cambridge (2011 -‐ current) – Source code analysis, automated refactoring, type systems, programming languages
• MEng Imperial College London (2007 -‐ 2011) • Google (Python team), Oracle (Java team), Goldman Sachs,
eBay • Regular author for Oracle Java Magazine • Conference speaker: Fosdem, Devoxx…
2
What I’m going to cover
• Why Java 8? (5min) • Behaviour parameterisa$on (10min) • Lambda expressions (20min) • Streams (15min) • Op$onal (5min) • Default methods (10min)
3
Java 8 in Ac$on: Lambdas, Streams and func$onal-‐style programming
4
• Co-‐authored with Mario Fusco & Alan Mycro` • Most complete book on Java 8
h"p://manning.com/urma
Why Java 8
Before: Collec$ons.sort(inventory, new Comparator<Apple>() { public int compare(Apple a1, Apple a2){ return a1.getWeight().compareTo(a2.getWeight()); } });
A5er: inventory.sort(comparing(Apple::getWeight));
5
Why Java 8?
6
List<Dish> lowCaloricDishes = new ArrayList<>(); for(Dish d: dishes){ if(d.getCalories() < 400){ lowCaloricDishes.add(d); } } List<String> lowCaloricDishesName = new ArrayList<>(); Collec$ons.sort(lowCaloricDishes , new Comparator<Dish>() { public int compare(Dish d1, Dish d2){ return Integer.compare(d1.getCalories(), d2.getCalories()); } }); for(Dish d: lowCaloricDishes){ lowCaloricDishesName.add(d.getName()); } 6
sor$ng by calories
filtering low calories
Extract names
Why Java 8?
7 7
List<String> lowCaloricDishesName = dishes.stream() .filter(d -‐> d.getCalories() < 400) .sorted(comparing(Dish::getCalories)) .map(Dish::getName) .collect(toList());
Why Java 8? ( as seen by language designers)
• Commodity CPUs are mul$core (e.g. 4 cores) • Analogy: you have 4 assistants, in theory you could get the work done 4 $mes faster!
• In prac$ce: – it’s hard because you now have to figure out how to distribute a piece of work amongst 4 people.
– It’s easier to just pass the whole piece of work to one person.
8
Why Java 8? ( as seen by language designers)
• Vast majority of Java programs use only one of these cores and leave the others idle
• Why? wri$ng efficient parallel code is hard – summing an array with a for loop is easy – how to sum an array on 4 cores?
9
Why Java 8? ( as seen by language designers)
• Java 8 introduces features that make parallel data processing easier
• Driven from three concepts: – stream processing – behaviour parameterisa$on ("passing code") – no shared mutable data
10
Stream processing
cat file1 file2 | tr "[A-‐Z]" "[a-‐z]" | sort | tail -‐3
11
!
Stream processing List<String> lowCaloricDishesName = dishes.parallelStream() .filter(d -‐> d.getCalories() < 400) .sorted(comparing(Dish::getCalories)) .map(Dish::getName) .collect(toList());
12
stream processing
Behaviour parameterisa$on
13
Behaviour parameterisa$on
List<String> lowCaloricDishesName = dishes.parallel() .filter(d -‐> d.getCalories() < 400) .sorted(comparing(Dish::getCalories)) .map(Dish::getName) .collect(toList());
14
lambda expression
method reference
No shared mutable data List<String> lowCaloricDishesName = dishes.parallelStream() .filter(d -‐> d.getCalories() < 400) .sorted(comparing(Dish::getCalories)) .map(Dish::getName) .collect(toList());
15
Can be duplicated and ran on disjoint part of input
Java 8
• Introduces a concise way to pass behaviour – lambda expressions, method references
• Introduces an API to process data in parallel – Streams API – several opera$ons such as filter, map, reduce can be parameterised with lambdas
• Also: default methods (more later) – more flexible inheritance
16
Behaviour parameterisa$on
• Goal: abstract over behaviour – Do <something> for every element in a list – Do <something> else when the list is finished – Do <yet something else> if an error occurs
• Why should you care? – Adapt to changing requirements – Java 8 Streams API heavily relies on it
17
1st awempt: fitering green apples public sta$c List<Apple> filterGreenApples(List<Apple> inventory) { List<Apple> result = new ArrayList<>(); for(Apple apple: inventory){ if("green".equals(apple.getColor() ) { result.add(apple); } } return result; }
18
2nd awempt: abstrac$ng color public sta$c List<Apple> filterApplesByColour(List<Apple> inventory, String color) { List<Apple> result = new ArrayList<>(); for (Apple apple: inventory){ if (apple.getColor().equals(color))) { result.add(apple); } } return result; }
19
List<Apple> greenApples = filterApplesByColor(inventory, "green"); List<Apple> redApples = filterApplesByColor(inventory, "red");
2nd awempt: abstrac$ng weight public sta$c List<Apple> filterApplesByWeight(List<Apple> inventory, int weight) { List<Apple> result = new ArrayList<>(); for (Apple apple: inventory){ if (apple.getWeight() > weight) { result.add(apple); } } return result; }
20
List<Apple> heavyApples = filterApplesByWeight(inventory, 150); List<Apple> megaHeavyApples = filterApplesByWeight(inventory, 250);
3rd awempt: filtering with everything
public sta$c List<Apple> filter (List<Apple> inventory, String color, int weight, boolean flag) { List<Apple> result = new ArrayList<>(); for (Apple apple: inventory){ if ( (flag && apple.getColor().equals(color)) || (!flag && apple.getWeight() > weight) ){ result.add(apple); } } return result; }
21
List<Apple> greenApples = filter(inventory, "green", 0, true); List<Apple> heavyApples = filter(inventory, "", 150, false);
4th (a) awempt: modeling selec$on criteria
public interface ApplePredicate{ public boolean test (Apple apple); } public class AppleWeightPredicate implements ApplePredicate{ public boolean test(Apple apple){ return apple.getWeight() > 150; } } public class AppleColorPredicate implements ApplePredicate{ public boolean test(Apple apple){ return "green".equals(apple.getColor()); } }
22
4th (a) awempt: modeling selec$on criteria
23
4th (b) awempt: filtering by an abstract criteria
public sta$c List<Apple> filter(List<Apple> inventory, ApplePredicate p){ List<Apple> result = new ArrayList<>(); for(Apple apple: inventory){ if(p.test(apple)){ result.add(apple); � } } return result; }
24
Let’s pause for a liwle bit
• Our code is much more flexible! – can create any kinds of selec$on criteria on an Apple – re-‐use of code for filter
• However, declaring many selec$on criterias using classes is verbose… L – we have the right abstrac$on but not good concision – we need a bewer way to create and pass behaviours
25
5th awempt: anonymous classes List<Apple> result = filter(inventory, new ApplePredicate() { public boolean test(Apple apple){ return "red".equals.(apple.getColor()); } }); List<Apple> result = filter(inventory, new ApplePredicate() { public boolean test(Apple apple){ return apple.getWeight() > 150; } }); 26
6th awempt: Java 8 lambdas List<Apple> result = filter(inventory, (Apple apple) -‐> "red".equals.(apple.getColor())); List<Apple> result = filter(inventory, (Apple apple) -‐> apple.getWeight() > 150);
27
Great because closer to problem statement!
7th awempt: abstrac$ng over the list type
public sta$c <T> List<T> filter (List<T> list, Predicate<T> p) { List<T> result = new ArrayList<>(); for(T e: list){ if(p.test(e)) { result.add(e); } } return result; }
28
8th awempt: Java 8 lambdas again List<String> result = filter(strings, (String s) -‐> s.endsWith("JDK")); List<Integer> result = filter(numbers, (Integer i) -‐> i % 2 == 0); List<Apple> result = filter(inventory, (Apple a) -‐> apple.getWeight() > 150);
29
Moral of the story
• Behaviour parameterisa$on lets you write more flexible code – Adapt for changes – Avoids code duplica$on
• To encourage this style of programming we need a concise way to create and pass behaviours – Java 8 brings lambda expressions to help!
30
Flexible
Concise
Anonymous classes
Verbose
Rigid
Lambdas Class + Instance
Value parameterisa$on
Behaviour parameterisa$on
Value vs Behaviour parameterisa$on
Lambda expressions: what is it?
• A lambda expression is: – a kind of anonymous func$on – that can be passed around: – It doesn’t have a name, but it has a list of parameters, a body, a return type, and also possibly a list of excep$ons that can be thrown.
32
In a nutshell
33
Lambda expressions: what is it?
• A lambda expression is: – a kind of anonymous func$on
• Anonymous: doesn’t have an explicit name like a method: less to write and think about!
• Func$on: not associated to a class like a method is
34
Lambda expressions: what is it?
• A lambda expression is: – a kind of anonymous func$on – that can be passed around:
• Passed around: A lambda expression can be passed as argument to a method or stored in a variable
35
Lambda expressions: what is it?
• A lambda expression is: – a kind of anonymous func$on – that can be passed around: – it doesn’t have a name, but it has a list of parameters, a body, a return type, and also possibly a list of excep$ons that can be thrown.
• Concise: you don’t need to write a lot of boilerplate like you do for anonymous classes.
36
Goal
• Goal: let you pass a piece of behaviour/code in a concise way
• You can cope with changing requirements by using a behaviour, represented by a lambda, as a parameter to a method
• You no longer need to choose between abstracaon and concision!
37
Before/A`er
Before: inventory.sort(new Comparator<Apple>() { public int compare(Apple a1, Apple a2){ return a1.getWeight().compareTo(a2.getWeight()); } });
A5er: inventory.sort((Apple a1, Apple a2) -‐> a1.getWeight().compareTo(a2.getWeight()));
38
Examples
39
Use case Example of lambda A boolean expression (List<String> list) -‐> list.isEmpty() Crea$ng objects () -‐> new Apple(10) Consuming from an object (Apple a) -‐> System.out.println(a.getWeight()) Select/extract from an object (String s) -‐> s.length() Combine two values (int a, int b) -‐> a * b Compare two objects (Apple a1, Apple a2) -‐>
a1.getWeight().compareTo(a2.getWeight())
Where and how to use lambdas?
40
• Java lets you pass expressions only where a type is expected
• So what’s the type of a lambda expression?
Func$onal interface (1)
41
• The type of a lambda expression is essen$ally a funcaonal interface
• A func$onal interface is an interface that declares exactly one (abstract) method.
Func$onal interface (2)
42
// java.u$l.Comparator public interface Comparator<T> { public int compare(T o1, T o2); } public interface Runnable { public void run(); } public interface ApplePredicate { public boolean test (Apple a); }
Func$onal interface & Lambdas
43
• The signature of the abstract method of the func$onal interface essen$ally describes the signature of the lambda expression.
• Lambda expressions let you provide the implementa$on of the abstract method of a func$onal interface directly inline and treat the whole expression as an instance of a func$onal interface.
Func$onal interface & Lambdas
44
Runnable r1 = () -‐> System.out.println("Hello World 1"); Runnable r2 = new Runnable(){ public void run(){ System.out.println("Hello World 2"); } } public void process(Runnable r){ r.run(); } process(r1); process(r2); process(() -‐> System.out.println("Hello World 3"));
Execute Around Pawern
45
Open ressource
Do some processing
Close ressource
Execute Around Pawern
46
public sta$c String processFile() throws IOExcep$on { try (BufferedReader br = new BufferedReader(new FileReader("data.txt"))) { return br.readLine(); } }
• This current code is limited. You can read only the first line of the file.
What we want
47
String result = processFile((BufferedReader br) -‐> br.readLine()); String result = processFile((BufferedReader br) -‐> br.readLine() + br.readLine());
Step 1: Introducing func$onal interface
48
public interface BufferedReaderProcessor { public String process(BufferedReader b) throws IOExcep$on; } public sta$c String processFile(BufferedReaderProcessor p) throws IOExcep$on { ... }
Step 2: Execu$ng a behaviour
49
public sta$c String processFile(BufferedReaderProcessor p) throws IOExcep$on { try (BufferedReader br = new BufferedReader(new FileReader("data.txt"))) { return p.process(br); } } String result = processFile((BufferedReader br) -‐> br.readLine()); String result = processFile((BufferedReader br) -‐> br.readLine() + br.readLine());
Lots of func$onal interfaces
50
Funcaonal interface Lambda signature Predicate<T> T -‐> boolean Consumer<T> T -‐> void Func$on<T, R> T -‐> R Supplier<T> () -‐> T UnaryOperator<T> T -‐> T Binaryoperator<T> (T, T) -‐> T BiFunc$on<T, U, R> (T, U) -‐> R
• Have a look in java.u$l.func$on.*
Method references
51
• Method references let you reuse exis$ng method defini$ons and pass them just like lambdas. « First-‐class » func$ons
• Just a syntac'c suggar to lambdas
Before: (Apple a) -‐> a.getWeight() A5er: Apple::getWeight Before: (str, i) -‐> str.substring(i) A5er: String::substring
Recipes
52
Bewer code with Java 8: a prac$cal example
53
public class AppleComparator implements Comparator<Apple> { public int compare(Apple a1, Apple a2){ return a1.getWeight().compareTo(a2.getWeight()); } } inventory.sort(new AppleComparator());
Anonymous class
54
inventory.sort(new Comparator<Apple>() { public int compare(Apple a1, Apple a2){ return a1.getWeight().compareTo(a2.getWeight()); } });
Lambdas
55
inventory.sort( (Apple a1, Apple a2) -‐> a1.getWeight().compareTo(a2.getWeight()) );
Lambdas
56
inventory.sort( (Apple a1, Apple a2) -‐> a1.getWeight().compareTo(a2.getWeight()) ); With type inference: inventory.sort( (a1, a2) -‐> a1.getWeight().compareTo(a2.getWeight()) );
A bit of help from library
57
Comparator<Apple> byWeight = Comparator.comparing((Apple a) -‐> a.getWeight()); inventory.sort(byWeight);
Method references
58
Comparator<Apple> byWeight = Comparator.comparing(Apple::getWeight); inventory.sort(byWeight);
Tidy up
59
import sta$c java.u$l.Comparator.comparing; inventory.sort(comparing(Apple::getWeight)); Reads like probable statement!
Streams
60
• Nearly every Java applica$ons makes and processes collec$ons
• However processing collec$ons is far from perfect: – SQL like opera$ons? – How to efficiently process large collec$ons?
SQL-‐like opera$ons
61
• Many processing pawerns are SQL-‐like – finding a transac$on with highest value – grouping transac$ons related to grocery shopping
• Re-‐implemented every $me • SQL is declaraave – express what you expect not how to implement a query
– SELECT id, MAX(value) from transac$ons
Example: Java 7
62
List<Transac$on> groceryTransac$ons = new Arraylist<>(); for(Transac$on t: transac$ons){ if(t.getType() == Transac$on.GROCERY){ groceryTransac$ons.add(t); } } Collec$ons.sort(groceryTransac$ons, new Comparator(){ public int compare(Transac$on t1, Transac$on t2){ return t2.getValue().compareTo(t1.getValue()); } }); List<Integer> transac$onIds = new ArrayList<>(); for(Transac$on t: groceryTransac$ons){ transac$onsIds.add(t.getId()); }
sor$ng by decreasing value
filtering grocery transac$ons
extract Ids
Example: Java 8
63
List<Integer> transac$onsIds = transacaons.stream() .filter(t -‐> t.getType() == Transac$on.GROCERY) .sorted(comparing(Transac$on::getValue).reversed()) .map(Transac$on::getId) .collect(toList());
Example: Java 8 -‐ parallel
64
List<Integer> transac$onsIds = transacaons.parallelStream() .filter(t -‐> t.getType() == Transac$on.GROCERY) .sorted(comparing(Transac$on::getValue).reversed()) .map(Transac$on::getId) .collect(toList());
Ok cool – so what’s a Stream?
65
• Informal: a fancy iterator with database-‐like opera$ons
• Formal: A sequence of elements from a source that supports aggregate opera$ons.
Ok cool – so what’s a Stream?
66
• Sequence of elements: a stream provides an interface to a sequenced set of values of a specific element type. However, streams don’t actually store elements, they are computed on demand.
Ok cool – so what’s a Stream?
67
• Source: Streams consume from a data-‐providing source such as Collec$ons, Arrays, or IO resources.
Ok cool – so what’s a Stream?
68
• Aggregate operaaons: Streams support SQL-‐like opera$ons and common opera$ons from func$onal programing languages such as filter, map, reduce, find, match, sorted etc.
Two addi$onal proper$es
69
• Pipelining: Many stream opera$ons return a stream themselves. This allows opera$ons to be chained and form a larger pipeline as well as certain op$misa$ons (more later).
• Internal iteraaon: In contrast to collec$ons, that are iterated explicitly (“external itera$on”), stream opera$ons do the itera$on behind the scene for you.
70
Pipelining
• Intermediate operaaons: return a Stream and can be “connected”
• Terminal operaaons: computes the result of the pipeline
Intermediate opera$ons Terminal opera$on
71
Internal Itera$on
72
Laziness & short-‐circui$ng
73
List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5, 6, 7, 8); List<Integer> twoEvenSquares = numbers.stream() .filter(n -‐> n % 2 == 0) .map(n -‐> n * n) .limit(2) .collect(toList());
Laziness & loop fusion & short-‐circui$ng
74
List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5, 6, 7, 8); List<Integer> twoEvenSquares = numbers.stream() .filter(n -‐> n % 2 == 0) .map(n -‐> n * n) .limit(2) .collect(toList());
• filtering 1 • filtering 2 • mapping 2 • filtering 3 • filtering 4 • mapping 4
Many opera$ons
75
• Filtering: filter, dis$nct, limit, skip • Finding/Matching: anyMatch, allMatch, noneMatch, findAny, findFirst
• Mapping: map, flatMap • Reducing: reduce
Streams: much more (1) Map<Dish.Type, List<Dish>> dishesbyType = new HashMap<>(); for (Dish dish: menu) {
Dish.Type type= dish.getType(); List<Dish> dishesForType = dishesbyType .get(type); if (dishesForType == null) { dishesForType = new ArrayList<>(); dishesbyType .put(type, dishesForType); } dishesForType .add(dish);
}
76
Streams: much more(2)
77
Map<Dish.Type, List<Dish>> dishesByType = menu.stream().collect(groupingBy(Dish::getType)); {FISH=[prawns, salmon], OTHER=[french fries, rice, season fruit, pizza], MEAT=[pork, beef, chicken]}
Streams: much more(3) Map<Boolean, Map<Dish.Type, List<Dish>>> vegetarianDishesByType = menu.stream().collect(
paraaoningBy(Dish::isVegetarian, groupingBy(Dish::getType))); • Produces a two-‐level Map: {false={FISH=[prawns, salmon], MEAT=[pork, beef, chicken]}, true={OTHER=[french fries, rice, season fruit, pizza]}}
78
Step 1: sequen$al for loop
public sta$c long itera$veSum(long n) { long result = 0; for (long i = 0; i < n; i++) { result += i; } return result; }
79
3ms
Step 2: sequen$al stream public sta$c long sequen$alSum(long n) { return Stream.iterate(1L, i -‐> i + 1) .limit(n) .reduce(Long::sum).get(); }
80
98ms
Step 3: parallel stream public sta$c long sequen$alSum(long n) { return Stream.iterate(1L, i -‐> i + 1).parallel() .limit(n) .reduce(Long::sum).get(); }
81
193ms!!
Step 4: improved sequen$al stream
public sta$c long rangedSum(long n) { return LongStream.rangeClosed(1, n) .reduce(Long::sum).getAsLong(); }
82
17ms
Step 5: improved parallel stream public sta$c long rangedSum(long n) { return LongStream.rangeClosed(1, n).parallel() .reduce(Long::sum).getAsLong(); }
83
1ms
Streams: much more (1)
84
• Infinite streams • Collectors: – collect – groupingBy – par$$onBy
Op$onal<T>
85
• A new class added in Java8: java.u$l.Op$onal • Indicates the presence or absence of a value • has a bunch of method to simplify and force “null” checking
Op$onal<Transac$on> optTransac$on = numbers.stream().findAny(t -‐> t.getValue() < 200);
Create Op$onal
86
opt = Op$onal.of(notNull); opt = Op$onal.ofNullable(mayBeNull); opt = Op$onal.empty();
Op$onal: do something if there’s a value
87
Instead of: if(transac$on != null){ System.out.println(t); } Write: optTransac$on.ifPresent(System.out::println);
Op$onal: reject certain values
88
Instead of: if(transac$on != null && transac$on.getId() > 10){ System.out.println(t); } Write: optTransac$on.filter(t -‐> t.getId() > 10) .ifPresent(System.out::println);
Op$onal: transform value if present
89
Instead of: if(transac$on != null){ String group = transac$on.getGroup(); if(“shopping”.equals(group)){ System.out.println(t); } } Write: optTransac$on.map(Transac$on::getGroup) .filter(g -‐> “shopping”.equals(g)) .ifPresent(System.out::println);
Op$onal: default value
90
Instead of: int len = (list != null)? list.size() : -‐1; Write: int len = list.map(List::size).orElse(-‐1);
Op$onal: nested checking
91
Instead of: if(transac$on == null){ return “Unknown”; } Des$nator des$nator = transac$on.getDes$nator(); if(des$nator == null){ return “Unknown”; } country = des$nator.getCountry(); if(country == null){ return “Unknown”; } return country.getName() != null ? country.getName() : “Unknown”;
Op$onal: nested checking
92
transac$on.flatMap(Transac$on::getDes$nator) .flatMap(Transac$on::getCountry) .map(Country::getName) .orElse(“Unknown”);
Default methods
93
• Interfaces in Java 8 can contain implementa$on code
From List.java: default void sort(Comparator<? super E> c){ Collec$ons.sort(this, c); }
Goal
94
• Evolve libraries • Adding a method to an interface is source incompa$ble – all implementers need to support the method – recompiling a class implemen$ng the interface will fail
• Providing a “default” implementa$on solves the problem
95
Applica$ons: Op$onal methods
96
interface Iterator<T> { boolean hasNext(); T next(); default void remove() { throw new UnsupportedOpera$onExcep$on(); } }
Applica$ons: mul$ple inheritance
97
• A class has been allowed to implement mul$ple interfaces since day 1 of Java!
public class ArrayList<E> extends AbstractList<E> implements List<E>, RandomAccess, Cloneable, java.io.Serializable, Iterable<E>, Serializable { … }
Applica$ons: mul$ple inheritance
98
Applica$ons: mul$ple inheritance
99
public interface Sized { public int size(); public default boolean isEmpty(){ return size() == 0; } }
Applica$ons: mul$ple inheritance
100
public interface Foreachable<T> extends Iterable<T> { public default void forEach(Consumer<? super T> block){ for(T e: this){ block.accept(e); } } }
Applica$ons: mul$ple inheritance
101
public interface Removeable<T> extends Iterable<T> { public default boolean removeIf(Predicate<? super T> filter){ boolean removed = false; Iterator<T> each = iterator(); while(each.hasNext()) { if(filter.test(each.next())) { each.remove(); removed = true; } } return removed; } }
Applica$ons: mul$ple inheritance
102
Resolu$on rules
103
1. Classes always win. A method declara$on in the class or a superclass takes priority over any default method declara$on. 2. Otherwise, the method with the same signature in the most specific default-‐providing interface is selected. (If B extends A, B is more specific than A).
Use case 1
104
Use case 1
105
public interface A{ public default void hello() { System.out.println("Hello from A"); } } public interface B extends A{ public default void hello() { System.out.println("Hello from B"); } } public class C implements B, A { public sta$c void main(String... args) { new C().hello(); // ?? } }
Use case 2 (a)
106
public class D implements A{ } public class C extends D implements B, A { public sta$c void main(String... args) { new C().hello(); // ?? } }
Use case 2 (b)
107
public class D implements A{ public void hello(){ System.out.println(“Hello from D”); } } public class C extends D implements B, A { public sta$c void main(String... args) { new C().hello(); // ?? } }
Use case 3
108
Explicit disambigua$on
109
public class C implements B, A { public void hello(){ B.super.hello(); } }
Use case 4
110
More…
111
• Func$onal asynchronous programming with CompleatableFuture
• Performance, Spliterators • Recipes using Op$onal • Tools, tes$ng & debugging • Func$onal programming in Java • Good ideas from Scala • Date/Time API, Java bytecode & lambdas
• Get the book Java 8 in Acaon to find out more!!! • h"p://manning.com/urma/ 38% discount code: vturma01