Functional interfaces

In Java everything that is not a primitive is an object. OK, but what about functions? Introducing a support for functional programming in Java 8 led to the need of give an answer to this question, and the designers decided to use the ad-hoc solution of functional interfaces.

A functional interface is a Java interface that has only one abstract method. It is a good idea to annotate it as FunctionalInterface so that it's clear what it is mean for, and let the compiler to check if it is alright. However it is not mandatory.

We can define and use any interface we like for use it in this way, but we should save to ourselves, and to our code clients, some pain using when possible the standard one, define in the java.util.function package.

Let's see a few examples.

Predicate

The predicate functional interface is defined as:
@FunctionalInterface
public interface Predicate<T> {
    boolean test(T t);

    // ...
}
I can use it if I have to write a method filter() that gets in input a list of data and filter them accordingly some requirements decided by the user.
public static <T> List<T> filter(List<T> data, Predicate<T> p) { // 1
    List<T> results = new ArrayList<>();
    for (T t: data) { // 2
        if (p.test(t)) {
            results.add(t);
        }
    }
    return results;
}
1. It takes as input a list of T and a predicate on T.
2. For each element in the list, if it pass the test defined by the caller, I add it to the list I return.

Here is a piece of code that uses the filter() method:
List<String> result = FunctionalInterfaces.filter(Arrays.asList("", "x", ""), (s) -> !s.isEmpty());
I create a list of strings and I pass it to filter() alongside a lambda function, that has to match with the expected Predicate, so the lambada input has to be a String - I could state it explicitly, but I can also leave it implicit - and it should return a boolean.
The result is a list containing just one string, namely "x".

Consumer

It is defined as:
@FunctionalInterface
public interface Consumer<T> {
    void accept(T t);

    // ...
}

Here I use it in a forEach() method that let the user specify what it should do with each element of a List:
public static <T> void forEach(List<T> data, Consumer<T> c) {
    for(T t: data) {
        c.accept(t);
    }
}
I use this forEach() to keep calculate the length of all the strings in a list:
List<String> data = Arrays.asList("Functional", "Interfaces", "in", "Java 8");
consumed = 0;
FunctionalInterfaces.forEach(data, (s) -> consumed += s.length());
assertThat(consumed, is(28));
Again, note that here the type of the lambda parameter s is inferred from T, type of the list passed to forEach() and used to determine the actual Consumer functional interface to which the lambda should refer to.
Interesting to note how the consumed variable is captured by the lambda without the need of any explicit action by programmer. This mechanism works fine only for non-local variables. Local variables can be captured only if final (at least effectively so).

Function
@FunctionalInterface
public interface Function<T, R> {
    R apply(T t);

    // ...
}

This map() method maps objects of a given type to another one, delegating to the caller the job of specifying how to do the conversion.
public static <T, R> List<R> map(List<T> data, Function<T, R> f) {
    List<R> result = new ArrayList<>();
    for(T t: data) {
        result.add(f.apply(t));
    }
    return result;
}

I use it here to convert a list of strings in a list of integers:
List<String> data = Arrays.asList("Functional", "Interfaces", "in", "Java 8");
List<Integer> result = FunctionalInterfaces.map(data, (s) -> s.length());
The compiler does a lot of job for us here. Since map() is templatized by T, the template type of the input list, and R, from the returned list, it figures out that s, passed to the lambda, is a String, so it should return an object of the class returned by the String method lenght(), that is an int, boxed to Integer.

IntBinaryOperator
@FunctionalInterface
public interface IntBinaryOperator {
    int applyAsInt(int left, int right);
}
In the example for Function, boxing the integers was an expected behavior. We need them as objects since we want them in a collection.
Other times, boxing and unboxing could be just a useless cost that we could avoid using a special functional interface like this one, that works with lambdas working on primitives.

This combine() method works on primitive integers and don't want to pay any extra costs for objects
public static int combine(int[] data, IntBinaryOperator op) {
    if(data == null || data.length != 2) {
        return 0;            
    }
    return op.applyAsInt(data[0], data[1]);
}

I could use it to decide dinamically what kind of operation applying to a couple of integers, accordingly to the lambda passed.
int result = FunctionalInterfaces.combine(new int[] {6, 7}, (a, b) -> a * b);

Supplier

@FunctionalInterface
public interface Supplier<T> {
    T get();
}

We don't have anything to pass to the lambda, and we blindly let it to create an object of the specified type.
public static <T> T aFactory(Supplier<T> s) {
    return s.get();
}

Here is a trivial example:
String result = FunctionalInterfaces.aFactory(() -> "Hello");

I pushed to GitHub a class FunctionalInterfaces with the static method having a reference to functional interfaces as parameter and a JUnit tester that shows how I called them.

Reference: Java 8 in action, section 3.4 Using functional interfaces.

No comments:

Post a Comment