Contents

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:

1
2
3
4
5
6
7
8
9
Function<BufferedReader, String> f =
  (BufferedReader b) -> {
    try {
      return b.readLine();
    }
    catch(IOException e) {
      throw new RuntimeException(e);
    }
  };

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)

This is legal (void compatibility rule):

1
2
3
4
5
// Note: list.add(s) returns boolean (informing if collection has changed as a result of this operation)
// Predicate has a boolean return 
Predicate<String> p = (String s) -> list.add(s);
// Consumer has a void return
Consumer<String> b = (String s) -> list.add(s);

and this isn’t (the context of the lambda is Object type, which is not the functional interface):

1
Object o = () -> { System.out.println("Tricky example"); };

Casting lambda

This is necessary it type checking has insufficient information and lambda expression matches two or more functional interfaces:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
public void execute(Runnable runnable) {
    runnable.run();
}
public void execute(Action<T> action) {
    action.act();
}
@FunctionalInterface
interface Action {
    void act();
}

In this case this code is ambiguous:

1
execute(()->{})

and should be cast to proper interface:

1
execute((Action) ()->{})

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

1
2
3
inventory.sort(comparing(Apple::getWeight)
         .reversed()                                  
         .thenComparing(Apple::getCountry));

Predicates

Negation

1
Predicate<Apple> notRedApple = redApple.negate();

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

1
2
3
Predicate<Apple> redAndHeavyAppleOrGreen =
    redApple.and(apple -> apple.getWeight() > 150)
            .or(apple -> GREEN.equals(a.getColor()));     

Composition

g(f(x)) or (g o f)(x) - the below code result will be 4:

1
2
3
4
5
6
7
Function<Integer, Integer> f = x -> x + 1;
Function<Integer, Integer> g = x -> x * 2;
// andThen is for sequential thinkers
Function<Integer, Integer> h = f.andThen(g);     
// the same result - compose is for mathematical thinkers
// Function<Integer, Integer> h = g.compose(f);     
int result = h.apply(1);

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

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 get Stream<X>, or we can .mapToObj(..)

Building

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.

1
2
int totalCalories = menu.stream().collect(reducing(
  0, Dish::getCalories, (i, j) -> i + j));

This is its signature:

1
2
3
public static <T,U> Collector<T,?,U> reducing(U identity,
                                              Function<? super T,? extends U> mapper,
                                              BinaryOperator<U> op)
  • 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 U into single value of the same type

collect vs reduce

  • collect is designed to mutate the container (used as accumulator)
  • reduce is defined as immutable operation (with binary operator)

By Grouping

The most powerful collector: grouping collector. Three variants:

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.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
Map<Dish.Type, Map<CaloricLevel, List<Dish>>> dishesByTypeCaloricLevel =
menu.stream().collect(
      groupingBy(Dish::getType,                                              1
         groupingBy(dish -> {                                                2
                if (dish.getCalories() <= 400) return CaloricLevel.DIET;
                else if (dish.getCalories() <= 700) return CaloricLevel.NORMAL;
                else return CaloricLevel.FAT;
          } )
      )
);

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:

1
2
Map<Boolean, List<Dish>> partitionedMenu =
             menu.stream().collect(partitioningBy(Dish::isVegetarian));   1

By imlementing Collector interface

1
2
3
4
5
6
7
  public interface Collector<T, A, R> {
    Supplier<A> supplier();
    BiConsumer<A, T> accumulator();
    Function<A, R> finisher();
    BinaryOperator<A> combiner();
    Set<Characteristics> characteristics();
}

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