1. Lambda expressions and functional programming

Lambda expressions are mappings and the base of functional programming. Functional programming focuses on functions, so in the best case pure expressions without side effects. In Java, you can’t pass functions — so methods — directly to other functions or return them, so the trick is to put these methods in a class and pass them as objects. And how do you quickly implement a class with a certain interface? With lambda expressions! Lambda expressions are implementations of functional interfaces — that is, interfaces with exactly one abstract method — and an alternative and shortcut to classes that implement interfaces. This makes it very easy to express program code and pass it to other methods.

The following tasks deal with the different notations of lambda expressions and some important functional interfaces that occur repeatedly in various places in Java libraries.

Prerequisites

  • be able to read generics

  • understand functional interfaces and relation to lambda expressions

  • know the annotation `@FunctionalInterface

  • be able to distinguish and use variants of lambda expressions

  • be able to use method and constructor references

  • know the basic content and structure of the java.util.function package

  • know how to use internal and external iteration

Data types used in this chapter:

1.1. Lambda expressions

Due to the compact notation of lambda expressions, the modeling of mappings becomes more important. Of course, an implementation of interfaces has been possible since Java 1.0, but the code size has been too large with regular classes, and modeling libraries that accept and supply "functions" has not generated much interest. The first prototypes appeared around 2006, but it took until Java 8 (March 2014) for lambda expressions to enter the language.[1].

1.1.1. Quiz: Recognize valid functional interfaces ⭐

Which of the following declarations are correct functional interfaces that a lambda expression can implement?

@FunctionalInterface
interface Distance {
  abstract public int distance( int a, int b );
}

@FunctionalInterface
interface MoreDistance extends Distance {
  double distance( double a, double b );
}

@FunctionalInterface
interface MoreDistance2 extends Distance {
  default double distance( double a, double b ) {
    return distance( (int) a, (int) b );
  }
}

@FunctionalInterface
interface DistanceImpl {
  default int distance( int a, int b ) { return a + b; }
}


@FunctionalInterface
interface DistanceEquals {
  int distance( int a, int b );
  boolean equals( Object other );
}

1.1.2. Quiz: From interface implementation to lambda expression ⭐

Given an interface declaration Distance and an implementation ManhattanDistance of the functional interface.

@FunctionalInterface
interface Distance {
  int distance( int a, int b );
}

class Schmegeggy {
  static void printDistance( Distance distance, int a, int b ) {
    System.out.println( distance.distance( a, b ) );
  }

  public static void main( String[] args ) {
    class ManhattanDistance implements Distance {
      @Override public int distance( int a, int b ) {
        return a + b;
      }
    }

    printDistance( new ManhattanDistance(), 12, 33 );
  }
}

In the course of a code revision, the interface implementation should be dropped and replaced by a lambda expression. Which lambda expressions are equivalent and valid?

  1. printDistance( ( a, b ) -> a + b, 12, 33 );

  2. printDistance( ( a, b ) -> { return a + b; }, 12, 33 );

  3. printDistance( (int a, int b) -> a + b, 12, 33 );

  4. printDistance( (int a, b) -> a + b, 12, 33 );

  5. printDistance( a, b -> { return a + b; }, 12, 33 );

  6. printDistance( ( a, b ) -> return a + b, 12, 33 );

  7. printDistance( ( a, b ) -> {int a; return b + b;}, 12, 33 );

  8. printDistance( (Integer a, Integer b) -> a + b, 12, 33 );

1.1.3. Write lambda expressions for functional interfaces ⭐

There is a large amount of interfaces in the Java library, especially functional interfaces, which are interfaces with only one abstract method. These can be well implemented with lambda expressions.

Task:

  • Assign a valid lambda expression to each of the following variables. The variable types are functional interfaces.

    /* interface Runnable    { void run(); }
    interface ActionListener { void actionPerformed(ActionEvent e); }
    interface Supplier<T>    { T get(); }
    interface Consumer<T>    { void accept(T t); }
    interface Comparator<T>  { int compare(T o1, T o2); } */
    Runnable              runnable   = ...
    ActionListener        listener   = ...
    Supplier<String>      supplier   = ...
    Consumer<Point>       consumer   = ...
    Comparator<Rectangle> comparator = ...
  • The lambda expressions only have to be compilable, the implementation does not have to make sense.

  • The declarations of the functional interfaces are in the comment for better understanding, but do not serve any other purpose.

  • Experiment with different compact notations.

1.1.4. Quiz: write lambda expressions like this? ⭐

Given is a class with a nested type declaration. There are some compiler errors in the main(…​) method — where exactly?

public class Ackamarackus {
  @FunctionalInterface
  interface Flummadiddle {
    void razzmatazz();
  }

  public static void main( String[] args ) {
    Flummadiddle a = () -> System.out.println();
    Flummadiddle b = () -> { System.out.println(); };
    Flummadiddle c = () -> { System.out.println() };
    Flummadiddle d = () -> { System.out.println(); return; };
    Flummadiddle e = -> { System.out.println(); };
    Flummadiddle f = _ -> { System.out.println(); };
    Flummadiddle g = __ -> { System.out.println(); };
    Flummadiddle h = void -> System.out.println();
    Flummadiddle i = (void) -> System.out.println();
    Flummadiddle j = System.out::println;
  }
}

1.1.5. Developing lambda expressions ⭐

We have seen that there are many functional interfaces in the java.util.function package. However, many of the interfaces contain multiple methods, default methods and static methods.

Task:

  • From the Javadoc, find the abstract method of the following types, and implement it using a lambda expression that does not have to make sense:

    DoubleSupplier ds = ...
    LongToDoubleFunction ltdf = ...
    UnaryOperator<String> up = ...

1.1.6. Quiz: Contents of the package java.util.function ⭐

Browse the Javadoc of the package java.util.function. Can the interfaces be divided into groups?

1.1.7. Quiz: Know functional interfaces for mappings ⭐

Given the following mappings; what are the known functional interfaces for these mappings?

AbbildungFunktionale Schnittstelle

()void

()T

()boolean

()int

()long

()double

(T)void

(T)T

(T)R

(T)boolean

(T)int

(T)long

(T)double

(T, T)T

(T, U)void

(T, U)R

(T, U)boolean

(T, U)int

(T, U)long

(T, U)double

(T, T)int

(T, T)long

(T, T)double

(int)void

(int)R

(int)boolean

(int)int

(int)long

(int)double

(int,int)int

(long)void

(long)R

(long)boolean

(long)int

(long)long

(long)double

(long,long)long

(double)void

(double)R

(double)boolean

(double)int

(double)long

(double)double

(double,double)double

1.2. Method and constructor references

Method references and constructor references are among the most unusual notations of Java. Many developers are unfamiliar with the very compact notation, and this is why practicing is so important, firstly to be able to read foreign code without problems of understanding, secondly to be able to comprehend why an intelligent IDE suggests us to rewrite a lambda expression in some cases, and thirdly to be able to use this reference notation completely by your own.

1.2.1. Rewriting lambda expressions ⭐⭐

How do you write the following lambda expressions as method or constructor references?

typelambda expressionmethod/constructor reference

Static method

(args) -> TypeName.method(args)

Non-static method (in instance)

(args) -> instance.method(args)

Non static method (no instance)

(instance, args) -> instance.method(args)

Constructor

(args) -> new TypeName(args)

array constructor

(int size) -> new TypeName[size]

The task is kept abstract on purpose, in the following chapters we will see practical examples.

1.3. Selected functional interfaces

The types Predicate, UnaryOperator and Consumer are functional interfaces that often occur as parameter types, for example in the Collection API, when it is a requirement to delete or transform elements according to a certain condition.

1.3.1. Delete entries, remove comments, convert to CSV ⭐

The assignment focuses on three methods that exists on every List:

  • default boolean removeIf(Predicate<? super E> filter) (Collection)

  • default void replaceAll(UnaryOperator<E> operator) (List)

  • default void forEach(Consumer<? super T> action) (Iterable)

The three methods perform an internal iteration, so it is not us who have to iterate through the list. We just need to specify the operation that will be performed on each element.

Bonny Brain and the crew are planning a city tour. Each city is given by two pieces of information: Name and population:

record City( String name, int population ) { }

The destinations of the city trip are in an ArrayList.

List<City> cityTour = new ArrayList<>();
City g = new City( "Gotham (cathedral)", 8_000_000 );
City m = new City( "Metropolis (pleasure garden)", 1_600_000 );
City h = new City( "Hogsmeade (shopping street)", 1_124 );
Collections.addAll( cityTour, g, m, h );

Task:

  • Use removeIf(…​) so that in the end only the larger cities with more than 10,000 inhabitants remain.

  • The city names contain comments in round brackets. If comments are included, they are always at the end of the string. Delete the comments and use the replaceAll(…​) method to replace the City objects.

  • Use forEach(…​), so that at the end the data of the cities appear on the screen in CSV format (comma separated output).

Example:

  • From the given list above, the output is:

    Gotham,8000000
    Metropolis,1600000

Java 8 Backport

Records are new in Java 16; in older Java versions implement City as a class with a parameterized constructor that stores String name and int population in final object variables.

1.4. Suggested solutions

1.4.1. Quiz: Recognize valid functional interfaces

Distance

The distance interface is a proper functional interface, and the abstract keyword is unnecessary because if a method is not a default method, it is automatically an abstract method with no implementation. The visibility modifier public is also unnecessary, because the abstract methods of an interface are automatically public. By the way, the visibility modifier should be first, but in our case you should remove both keywords.

MoreDistance

At first glance, the MoreDistance interface looks the same as Distance, however, we would need to take a look at inheritance, which gives MoreDistance two abstract methods, namely its own and the inherited one from the base type. In the end there are two abstract methods in the interface, this is invalid for functional interfaces.

MoreDistance2

The interface is a functional interface because it contains exactly one abstract method, from the extended interface Distance. Whether an interface itself contains an abstract method or inherits one from the superclass is irrelevant. If an interface has exactly one abstract method, it can still have any number of default methods or static methods; it remains a functional interface.

DistanceImpl

The interface contains a default implementation, which is fine as such, however this does not count as a functional interface. A functional interface must declare exactly one abstract method. This method must not already have an implementation as a default method.

DistanceEquals

This is a functional interface even though it has two methods. But this is because the method equals(…​) is a special method from java.lang.Object, which is always present on every object anyway. You put the equals(…​) method in an interface now and then, so that an individual Javadoc can describe the meaning of the method.

1.4.2. Quiz: From interface implementation to lambda expression

1st, 2nd, and 3rd

All three compile and are equivalent.

All other notations will result in a compiler error for different reasons.

4.

Either both types are determined or no type

5.

Multiple lambda parameters must always be parenthesized.

6.

Either you use a block with {} and write return, or the shorter notation does not contain return.

7.

The variable a must not be declared again, and b+b is not equivalent to a+b.

8.

There is no autoboxing concerning the types in the parameter list. int and integer are completely different types.

1.4.3. Write lambda expressions for functional interfaces

com/tutego/exercise/lambda/LambdaTargetType.java
Runnable              runnable   = () -> {};
ActionListener        listener   = event -> {};
Supplier<String>      supplier   = () -> "";
Consumer<Point>       consumer   = point -> {};
Comparator<Rectangle> comparator = (r1, r2) -> 0;

The implementations are as simple as possible and do not implement any logic, because that always depends on the semantics.

1.4.4. Quiz: write lambda expressions like this?

The following assignments lead to a compiler error:

c

The semicolon at the end of System.out.println() is missing, there must be complete statements in the {} block.

e

Lambda parameters are missing on the left, in our case ().

f and g

The method in the functional interface has no parameter list, therefore only () must be left of the arrow.

h and i

For methods, void stands for no return, but void cannot be left for a missing parameter list for lambda expressions.

1.4.5. Developing lambda expressions

com/tutego/exercise/lambda/LambdaTargetType2.java
DoubleSupplier        ds   = () -> Math.random();
LongToDoubleFunction  ltdf = value -> value * Math.random();
UnaryOperator<String> up   = s -> s.trim();

1.4.6. Quiz: Contents of the package java.util.function

First of all, we recognize different suffixes:

  • Consumer: something is consumed and used up, but it is not returned.

  • Predicate: a test is made, a value goes in, and a boolean comes out. Basically a specialization of Function with a special return type.Function)

  • Supplier: Something is produced, so it is returned, but no input is expected.

  • Function: something is transformed by a function, that is, a value comes in, and a value comes out.

  • UnaryOperator: a special function where the input type and output type are always the same. In a Function, the input type and output type can be completely different..

Then different prefixes can be identified. These are Double, Int, Long. These are special functional interfaces, which exist instead of a generic type only for these three special primitive datatypes. For these primitive data types there are also combinations, e.g. IntToDoubleFunction.

1.4.7. Quiz: Know functional interfaces for mappings

Mappingfunctional interface

()void

Runnable

()T

Supplier

()boolean

BooleanSupplier

()int

IntSupplier

()long

LongSupplier

()double

DoubleSupplier

(T)void

Consumer<T>

(T)T

UnaryOperator<T>

(T)R

Function<T,R>

(T)boolean

Predicate<T>

(T)int

ToIntFunction<T>

(T)long

ToLongFunction<T>

(T)double

ToDoubleFunction<T>

(T, T)T

BinaryOperator<T>

(T, U)void

BiConsumer<T,U>

(T, U)R

BiFunction<T,U,R>

(T, U)boolean

BiPredicate<T,U>

(T, U)int

ToIntBiFunction<T,U>

(T, U)long

ToLongBiFunction<T,U>

(T, U)double

ToDoubleBiFunction<T,U>

(T, T)int

ToIntBiFunction<T,T>

(T, T)long

ToLongBiFunction<T,T>

(T, T)double

ToDoubleBiFunction<T,T>

(int)void

IntConsumer

(int)R

IntFunction<R>

(int)boolean

IntPredicate

(int)int

IntUnaryOperator

(int)long

IntToLongFunction

(int)double

IntToDoubleFunction

(int,int)int

IntBinaryOperator

(long)void

LongConsumer

(long)R

LongFunction<R>

(long)boolean

LongPredicate

(long)int

LongToIntFunction

(long)long

LongUnaryOperator

(long)double

LongToDoubleFunction

(long,long)long

LongBinaryOperator

(double)void

DoubleConsumer

(double)R

DoubleFunction<R>

(double)boolean

DoublePredicate

(double)int

DoubleToIntFunction

(double)long

DoubleToLongFunction

(double)double

DoubleUnaryOperator

(double,double)double

DoubleBinaryOperator

1.4.8. Rewriting lambda expressions

Typelambda expressionmethod/constructor reference

Static method

(args) -> TypeName.method(args)

TypeName::method

Non-static method (in instance)

(args) -> instance.method(args)

instance::method

Non static method (no instance)

(instance, args) -> instance.method(args)

TypeName::method

Constructor

(args) -> new TypeName(args)

TypeName::new

Array constructor

(int size) -> new TypeName[size]

TypeName[]::new

1.4.9. Delete entries, remove comments, convert to CSV

com/tutego/exercise/util/CityTourList.java
public static void main( String[] args ) {

  List<City> cityTour = new ArrayList<>();
  City g = new City( "Gotham (cathedral)", 8_000_000 );
  City m = new City( "Metropolis (pleasure garden)", 1_600_000 );
  City h = new City( "Hogsmeade (Shopping Street)", 1_124 );
  Collections.addAll( cityTour, g, m, h );

  cityTour.removeIf( city -> city.population() <= 10_000 );
  cityTour.replaceAll(
      city -> new City( city.name().replaceAll( "\\s*\\(.*\\)$", "" ),
                        city.population() )
  );
  cityTour.forEach( CityTourList::printAsCsv );
}

private static void printAsCsv( City city ) {
  System.out.printf( "%s,%s%n", city.name(), city.population() );
}

After creating a copy of the list in the first step, we call three methods on the list.

In the first method, removeIf(Predicate), we must specify a predicate that determines if the size of the city is as needed. If not, it must be removed. The Predicate tests the City object and returns true or false.

The next method, replaceAll(UnaryOperator), expects a function — a UnaryOperator<T> is a subtype of Function<T, T>. This function is applied on every element and replaces every element in the list. Since City objects cannot be modified (the object variables are final), we create a new City object and transfer the values, using the String method replaceAll(…​) to help us remove anything in round brackets. The List method replaceAll(…​) will replace each City in the list with a new City that we build in the implementation from the UnaryOperator.

The third method, forEach(Consumer), expects a consumer. Now, instead of using a lambda expression directly, as we did with the other two methods, forEach(…​) references its own method printAsCsv(…​) via a method reference. It has the same parameter list as a Consumer: it expects something, namely a City, and returns nothing. Inside the method, we put the two components together, separated by a semicolon, and output them.


1. For those interested in the history, see http://www.javac.info/ for old drafts