Functional programming and lambda expressions - Java advanced (OCP)

Functional Programming (FP) is a programming paradigm, as OOP is. Java can make use of both. FP relies on Functions. Functions, as methods, can represent a behavior, but unlike methods, can also be used as an object an be passed as parameter or returned from a method.

This article is part of a series on advanced Java concepts, also corresponding to topics to study for upgrading from level I certification to level II certification (OCP level). The new Oracle Java certification framework (as from Java 11) only offers one certification covering both levels, so you’ll want to go through the level I (“OCA”) topics as well.

Functional interface

A functional interface is an interface that has exactly one abstract method. It can have other methods as long as they’re static or default methods.

They can also be called SAM type (for Single Abstract Method).

These interfaces can be implemented as usual (implemented by a class, extended by another interface or used in an anonymous class), but also using a lambda expression or a method reference.

You can make your custom functional interfaces (in which case prefixing it with built-in @FunctionalInterface annotation allows to raise compile time errors if contract is not satisfied) or use the built-in interfaces.

Lambda expressions

Lambda expressions are an inline, anonymous, implementation of a functional interface (see also the article about nested classes and anonymous classes).

General syntax of a lambda is:

(parameters) -> { body and return };

You can recognize the syntax as it’s really similar to what’s used in other languages. Just pay attention if you come from JavaScript or learn both languages, that Java uses single arrow -> while JavaScript uses double arrow =>, but they’re the same. Those functions are often called arrow functions.

You have to know the possible ways to write lambdas for the exam:

// left-hand side:
() -> // no parameter
p -> // implicitly typed parameter
(p) -> // same as previous
(var p) -> // type-inferred parameter
(p, q) -> // N-parameters
(int p, int q) -> // N-parameters, explicitly typed
(int p) -> // explicitly typed parameter
(var p, var q) -> // N-parameters, type-inferred
(final p, q) -> // use of modifier final

// right-hand side:
-> any single-line expression; // with or without return 
-> { any N-lines expression; returns x; };
-> { return expression; }; // for some reason you want to type "return"

Because they use functional interfaces, which only have a since abstract method, in common cases lambda expressions infer their properties from the context. It can be written in a minimal way because it knows the types on input and output (if there is).

Collections.sort(list, (p1, p2) -> p1.getPrice().compareTo(p2.getPrice()));

/* Context is inferred because the method sort takes a Comparator as second argument, thus it's known that the only abstract method of Comparator, compare(), is called. Type of the arguments is inferred from the type stored in the list passed as first parameter */

// Corresponding anonymous class without using lambda expression:
new Comparator<Product>() {
  @Override
  public int compare(Product p1, Product p2) {
   return p1.getPrice().compareTo(p2.getPrice());
  }
};

Lambda expressions have a type and can be stored in a variable. The reference to the lambda can then be used as a parameter instead of the whole expression, for code clarity, but also for reuse purpose.

Comparator<String> sortText = (s1, s2) -> s1.compareTo(s2);
Collections.sort(list, sortText);

Lambdas can also act as regular objects. Example of invoking a referenced lambda:

Function<Integer, Integer> myLambda = x -> x + 1;
int res = myLambda.apply(7); // apply is the abstract method of Function, as described below

Lambda expressions can also make use of local variables, as long as they’re final or effectively final. This is called “closure”.

There are many built-in functional interfaces with which you can use lambda expressions. Try to remember at least the basic ones and go through the list at least once. Specific functions’ way of working can be deducted from the name but the more prepared you are, always the better (especially if you’re taking OCP because the available time during the exam is pretty short).

Convoluted method references (lambdas shorthand)

This very short way to write lambdas exists and may appear in the exam, although it sometimes makes the code less clear and is not always the best practice (question of taste also). This is the syntax:

  • ContainingClass::method (where method is static)
  • object::method (where method is static or instance)
  • Class::new (constructor)
  • ContainingType::method (instance method of an arbitrary object of a particular type)
Function<String, String> lambda = x -> x.toUpperCase();

// use in a loop for example, repeat on each instance of a list of strings:
... = String::toUpperCase;

// with a particular instance:
... = aWord::toUpperCase;

// static method:
Integer::toBinaryString;

// printing:
System.out::print;

// constructor:
Product::new;
// method reference
BiFunction<Integer, Integer, Integer> max = Integer::max;
// same as writing:
BiFunction<Integer, Integer, Integer> max = (x, y) -> Integer.max(x, y);

// called:
System.out.println(max.apply(77, 75));
Consumer<String> consumer = System.out::println;
consumer.accept("Hello World!"); // outputs Hello World!

Built-in functional interfaces

NameAbstract methodExample
Function<T, R>R apply(T t)Function<Integer, Double> lambda = x -> 2.0 * x;
UnaryOperator<T>
(extends Function)
T apply(T t)UnaryOperator<Integer> lambda = x -> 2 * x;
Predicate<T>boolean test(T t)Predicate<Integer> lambda = x -> x == 10;
Consumer<T>void accept(T t)Consumer<Integer> lambda = x -> System.out.println(x);
Supplier<T>T get()Supplier<String> lambda = () -> “I love Java”;
Runnablevoid run()Runnable lambda = () -> System.out.println(“Hello World!”);

Vocabulary note: a “higher-order function” takes at least one function as a parameter and/or returns a function.

Some of these functional interfaces define additional methods, which are not abstract:

Function

  • default <V> Function<T, V> andThen(Function<? super R, ? extends V> after) : returns a composed function that first applies this function to its input and then applies the after function to the result
  • default <V> Function<V, R> compose(Function<? super V, ? extends T> before) : at the exact contrary, applies the before function then this function
  • static <T> Function<T, T> identity() : returns a function that always returns its input argument

UnaryOperator

  • static <T> UnaryOperator<T> identity()

Consumer

  • default Consumer <T> andThen(Consumer<? super T> after)

Predicate

  • default Predicate <T> and(Predicate<? super T> other) : returns a predicate using short-circuit AND of this predicate and other
  • default Predicate<T> negate() : returns a predicate that represents the logical negation of this
  • default Predicate<T> or(Predicate<? super T> other) : returns a composed predicate using short-circuit OR of this predicate and other
  • static <T> Predicate<T> isEqual(Object targeRef) : returns a predicate that tests if two arguments are equal according to Objects.equal(Object, Object)
Predicate<String> s1  = Predicate.isEqual("java"); 
System.out.println(s1.test("Java"));

// another use:
list.removeIf(Predicate.isEqual("java"));
list.removeIf(s1);

Check other Java certification topics (level I or level II) and subscribe to receive future posts.

7 thoughts on “Functional programming and lambda expressions – Java advanced (OCP)

Leave a Reply

Your email address will not be published. Required fields are marked *

Don’t miss out!

Receive every new article in your mailbox automatically.


Skip to content