1. Lambda expressions and functional programming

Lambda expressions are the foundation of functional programming and serve as mappings. Functional programming is centered around functions, with a focus on pure expressions that don’t have side effects. In Java, it’s not possible to pass functions (methods) directly to other functions or return them, so the workaround is to encapsulate these methods in a class and pass them as objects. Lambda expressions provide a convenient way to quickly implement a class with a specific interface. They serve as implementations of functional interfaces—interfaces with only one abstract method—and are an alternative and efficient shortcut to classes that implement interfaces. As a result, expressing program code and passing it to other methods becomes more straightforward.

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. Write lambda expressions for functional interfaces ⭐

There are many 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.2. Developing lambda expressions ⭐

We have observed 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.2. 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.2.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 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 );
CityTourList

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

1.3. Functional libraries

There are a huge number of libraries for Java, and this is one reason why Java is so popular. Some additions translate functional principles into the Java world.

1.3.1. VAVR ⭐⭐⭐


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