Lambda Expressions & Functional Interfaces in Java

Lambda expressions implement functional interfaces. Adds some functional programming patterns to an otherwise object-oriented language.

Functional Interface

  1. only one abstract method
  2. static and default methods do not count
  3. methods from object class like toString, equals, hashCode do not count
  4. can be used with @FunctionalInterface annotation

Implementing a Lambda Expression

  1. copy-paste the function signature
  2. mega arrow ( -> )
  3. provide implementation
  4. implicit return for one-line implementation
  5. for multi-line, use curly braces and explicit return
interface WaterBottle<T> {
    T t get(T t);
}

public class interfaces {
    public static void main(String args[]) {
        WaterBottle<String> bottle = (String a) -> "Hello!";
        bottle.get();
    }
}

java.util.function Toolbox

  1. set of functional interfaces
  2. more than 40
  3. divided into 4 categories - supplier (produces an object), consumer (consumes an object), predicate (performs testing and returns boolean, used in filtering operations of the Stream API), functions (takes any and returns another, used in map operation of the Stream API)
  4. public interface Function<T, R> {
     R apply (T t);
    }
    
  5. also includes Runnable interface

Performance Enhancement

vs Anonymous Classes

  • not an instance of anonymous class
  • generated bytecode is different, hence performance is different
  • lambdas compiled using specific bytecode instructions called invokedynamic (Java 7+)
  • lambdas are 60x times faster, aggressively optimised

Autoboxing (Java 5+)

  • trick used by compiler to automatically convert into primitive type into object
  • comparator example:
Comparator<Integer> cmp = (i1, i2) -> Integer.compare(i1, i2);

int compared = cmp.compare(10, 20)
  • sending primitive values to the function -> compiler will box these values
  • Integer.compare takes only primitive values -> compiler will unbox these values
  • this has cost as moving from int space to object space
  • comparators are used in sorting -> huge cost

Specialised Interfaces

  • set of interfaces tailored to work with primitive instead of specialised types
  • for int, long & double primitive types
IntSupplier supplier = () -> 10;
int i = supplier.getAsInt();

DoubleToIntFunction function = (doubleValue) -> (int)Math.floor(doubleValue);
int i = function.applyAsInt(Math.PI);
  • Naming convention: to

Functions:

TODO: generate samples for all 4 types

List<User> users = List.of(new User("Shreya"), new User("Raghav"), new User("XYZ"));
List<String> names = new ArrayList<>();
Function<User, String> mapping = (User u) -> u.getName();
for (User i : users) {
    names.add(mapping.apply(i));
}
users.forEach(u -> System.out.println(u));
names.forEach(u -> System.out.println(u));

Chaining:

Using default & static methods present on the functional interfaces to chain those lambdas.

Consumer:

Consumer<String> c1 = s -> System.out.println("c1 - " + s);
Consumer<String> c2 = s -> System.out.println("c2 - " + s);

Consumer<String> c3 = s -> {
    c1.accept(s);
    c2.accept(s);
};
c3.accept("Hello!");

also equivalent to

Consumer<String> c3 = c1.andThen(c2);

Predicate:

Predicate<String> isNull = s -> s == null;
Predicate<String> isEmpty = s -> s.length() == 0;

boolean isLameHelloOk = !isNull.test("hello") && !isEmpty.test("hello");

also equivalent to

Predicate<String> isOk = isNull.negate().and(isEmpty.negate());
boolean isDopeHelloOk = isOk.test("hello");

Example:

Passing a custom comparator for the sorting function.

To compare the strings using their lengths, we create a comparator that does an integer comparison of the two strings passed to it and pass that comparator to our sort method.

List<String> users = new ArrayList<String>(List.of("Shreya", "Raghav", "XYZ"));
Comparator<String> cmp = (s1, s2) -> Integer.compare(s1.length(), s2.length());
users.sort(cmp);

Can be re-written as below to make use of a "key extractor" written with a Function:

Function<String, Integer> keyExtractor = s1 -> s1.length();
Comparator<String> cmp = Comparator.comparing(keyExtractor);

The comparing method is a static method on the Comparator interface that creates Comparators, the lambda expressions without having to be written.
The above way does not take into account the boxing/unboxing nonsense from before. Rectifying by using the appropriate Function and the corresponding static method on the Comparator interface:

ToIntFunction<String> keyExtractor = s1 -> s1.length();
Comparator<String> cmp = Comparator.comparingInt(keyExtractor);

For a User class having two properties name and age, we can sort by name and if names are same, then by age. To do that, we have to chain the comparators.

Comparator<User> cmpName = Comparator.comparing(user -> user.getName());
Comparator<User> cmpAge = Comparator.comparing(user -> user.getAge());
Comparator<User> cmp = cmpName.thenComparing(cmpAge);
users.sort(cmp);

To reverse, the comparator has to be modified:

Comparator<User> cmp = cmpName.thenComparing(cmpAge).reversed();