Modern Java in Action 1 - Java 8 refresher
Book notes about Java 8

This post starts Java 8 refresher series - I will keep here my short notes during reading “Modern Java in Action”.
Intro
I decided to take a look at “Modern Java in Action” (2018) book by Raoul-Gabriel Urma, Alan Mycroft and Mario Fusco. Here you can find some “notes to myself”, so this post in neither a tutorial nor a lecture. Many of these points are very well known to you already. So don’t hesitate and just skip them. Or just skip this whole post altogether.
Lambda expressions
Lambda definition
A definition from the book:
Lambda expression can be understood as a concise representation of an anonymous function that can be passed around.
Behavior parametrization
Lambdas can be used as a means for behavior parametrization. Example: execute-around pattern in which a method works on or processes some data (or does some business-related stuff) but needs to execute a lot of bolerplate code before and after the data is processed or stuff is done.
The data can be obtained in a very different way each time the method is called, but each time one needs to do some prepareation before and some cleanup afterwards. So obtaining the data is encapsulated as external behavior and passed into the “boilerplate” method. In this method, the boilerplate wraps around the call to a lambda function which was passed as “boilerplate” method parameter.
Functional interfaces
One-method interfaces which are used on consuming side of the lambda - they provide target types for lambda expressions or method references.
Java does not have proper function types like Scala or Go or Haskell. Standard library groups common basic functional interfaces in java.util.function package.
| Target type | Function descriptor |
|---|---|
| Predicate<T> | T -> boolean |
| Consumer<T> | T -> void |
| Function<T, R> | T -> R |
| Supplier<T> | () -> T |
| UnaryOperator<T> | T -> T |
| BinaryOperator<T> | (T, T) -> T |
| BiPredicate<T, U> | (T, U) -> boolean |
| BiConsumer<T, U> | (T, U) -> void |
| BiFunction<T, U, R> | (T, U) -> R |
Exceptions
When some external API expects a functional interface that does not throw checked exceptions, the lambda we passs can catch the checked one and rethrow the unchecked:
|
|
Type checking
The type of a lambda is deduced from the context in which the lambda is used.
Steps:
- check what functional interface is expected by the method that has lambda passed in
- check the signature of the single method of such interface
- check if the lambda parameters and return values match the types of lambda expression
- statement in the position of lambda result is compatible with void (this is called
void-compatibility rule)
- statement in the position of lambda result is compatible with void (this is called
This is legal (void compatibility rule):
|
|
and this isn’t (the context of the lambda is Object type, which is not the functional interface):
|
|
Casting lambda
This is necessary it type checking has insufficient information and lambda expression matches two or more functional interfaces:
|
|
In this case this code is ambiguous:
|
|
and should be cast to proper interface:
|
|
Constructing method references
| function signature | method reference | note |
|---|---|---|
| (args) -> Classname.staticMethod(args) | Classname::staticMethod | |
| (arg0, rest) -> arg0.instanceMethod(rest) | Classname::instanceMethod | arg0 is of type Classname |
| (args) -> expr.instanceMethod(args) | expr::instanceMethod | |
| object creation | Classname::new | creates object; eg. as Supplier |
Composition
Comparator
|
|
Predicates
Negation
|
|
Logical operations. Ineresting:
a.or(b).and(c) must be read as (a || b) && c. Similarly, a.and(b).or(c) must be read as as (a && b) || c
|
|
Composition
g(f(x)) or (g o f)(x) - the below code result will be 4:
|
|
Streams
API that allows to write code that is
- declarative (concise, more readable)
- composable (flexible)
- parallelizable (possibly better performance)
Stream: a sequence of elements from a source that supports data-processin operations
- not a data structure but a means to perform data computations (like filter, map, reduce, find, match, sort)
- source may be: collection, array or IO resource, but also: generator functions (e.g. ints() or doubles()
- may be constructed lazily
- traversable only once
- one may get new stream from source (if possinble; not possible for IO channels)
- internal iteration
Operations
Two types:
- intermediate (filter, map, limit, sorted, distinct)
- terminal (forEach, count, collect)
Filtering
- do
filter(<condition>).distinct()using filter and distinct to get unique elements for a condition - use
takeWhileanddropWhile(since Java 9) - limit(n) complementary to skip(n)
- finding with allMatch, anyMatch, noneMatch, findFirst, and findAny
- short-cirquit in filtering operations
Numeric streams
- numeric streams (IntStream, DoubleStream, LongStream) and variants of Optional: OptionalInt, OptionalLong and OptionalDouble for use when primitive values are processed and we don’t want boxing overhead
- XStream can be
.boxed()to getStream<X>, or we can.mapToObj(..)
Building
- Stream.of(…), Stream.empty(), Stream.ofNullable(val) - since Java 9
- Arrays.stream(arr)
- methods in java.nio.file.File
- Stream.iterate(T seed, UnaryOperator
f) - Stream.generate(Supplier
s)
Collecting
Interesting class: Collectors allows to do the folowing:
- Reducing and summarizing stream elements to a single value
- Grouping elements
- Partitioning elements
Tasks:
.collect(Collectors.toList())- just collect to list.collect(Collectors.joining(", "))- create string with items joined by given string.collect(Collectors.summingInt(Employee::getSalary)))- compute sum and return int.collect(Collectors.groupingBy(Employee::getDepartment))- creates a map with key function.collect(Collectors.groupingBy(Employee::getDepartment), Collectors.summingInt(Employee::getSalary)));- compute sum of salaries per dept.collect(Collectors.partitioningBy(s -> s.getGrade() >= PASS_THRESHOLD))- partition to elements that are fulfilling and those not fulfilling a predicate (map from Boolean to a list of items
By Reducing
This is the most powerful, core collecting factory method. With this, you can emulate all the cases above.
|
|
This is its signature:
|
|
- first is initial value (returned when collection is empty), called identity value, of type
U - second transforms stream value (of type
? super T) into another value (of type? extends U) - third is a binary oberator that aggregates two items of type
Uinto single value of the same type
collect vs reduce
collectis designed to mutate the container (used as accumulator)reduceis defined as immutable operation (with binary operator)
By Grouping
The most powerful collector: grouping collector. Three variants:
- with classifier only: groupingBy
- with classifier and downstream collector: groupingBy
- …and with map factory method: groupingBy
Example of two-level grouping achieved using groupingBy with classifier and downstream collector: the downstream collector further groups dishes of specific type into CaloricLevel groups.
|
|
By Partitioning
This is a collector that does partitioning by a predicate. It creates a map with True and False keys and values split into predicate matching and predicate not-matching stream elements:
|
|
By imlementing Collector interface
|
|
where
- T - is the type of collection’s elements
- A - is the type of an accumulator
- R - is the type of the result of finisher function application
The functions have following meaning:
- supplier() - creates a supplier (factory method that creates) a new container for accumulated values
- accumulator() - performs an operation on a collection element and current value of accumulator (modifying accumulator in-place)
- finisher() - called on an accumulator after all elements of a collection are traversed to get final value (or Function.identity() if accumulator is of required shape already)
- combiner() - used to merge two accumulators in case parts of the collection are accumulated in parallel; for parallel reduction a fork/join framework is used
The last one (characteristics()) is used to mark specific characteristics of the collector required in order to allow/forbid certain optimizations, like:
- UNORDERED - elements can be processed in any order
- CONCURRENT - accumulator() can be called from multiple threads and collector can be used in parallel recution
- IDENTITY_FINISH - finisher() is just an identity and its call can be omitted
This text is part of the series modern-java-in-action...
- 2022-05-07 - Modern Java in Action 8 - concurrency and reactive programming
- 2022-02-07 - Modern Java in Action 7 - notes about the module system
- 2022-30-06 - Modern Java in Action 6 - Time and Date
- 2022-20-06 - Modern Java in Action 5 - Optional
- 2022-17-06 - Modern Java in Action 4 - refactoring and testing
- 2022-15-06 - Modern Java in Action 3 - collection API
- 2022-14-06 - Modern Java in Action 2 - fork-join and spliterators
- 2022-03-06 - Modern Java in Action 1 - Java 8 refresher