Autor: José C. Riquelme Santos

Catedrático de Universidad. Departamento de Lenguajes y Sistemas Informáticos. Universidad de Sevilla

Functional Interfaces

1. Introduction
Functional interfaces are a powerful mechanism provided by Java 8 to give us the possibility to pass functions as parameters to other methods. Actually, this option already existed on previous versions of Java, for instance with the interface Comparator.

What is a functional interface? A functional interface defines “objects” that do not store values like traditional objects, but only “functions”. Remark that both “object” and “function” are between quotes, because functional interfaces are not actual objects or functions, but only a mechanism to have a method to receive functional elements as arguments. Let us have a look back to the Comparator interface: what is being defined with a Comparator? A natural ordering criteria is being defined, in such a way that the compare method tells us which of the two given objects can be considered as smaller. If a Comparator object is passed to a method, we are providing this method a function capable of telling the order of objects. The method is “generic” with respect to this ordering and must be prepared to receive any criteria and execute its functionality according to the input ordering.

This way, for instance, the sort method from the class Collections can receive an object of type Comparator. Previously, a class of type Comparator must have been implemented to define the order of the objects of the class to be compared.

Example. The Comparator class that orders flights according to their price from bigger to smaller is:

public class ComparatorFlightPrice implements Comparator {
     public int compare(Flight v1, Flight v2){
            return v1.getPrice().compareTo(v2.getPrice());
     }
}

And the invocation could be:

       Collections.sort(flightList, new ComparatorFlightPrice());

As it can be seen, the sort method receives a functional type argument, as the Comparator object received tells the sort how the values from flightList must be ordered. The code of the sort method of the class Collections must obviously be prepared to order under different criteria. For that purpose, the sort method has among its code lines a generic invocation to the compare method of the object it receives as parameter. This way, if it receives a Comparator to order by the price, it will order the input list by the flight price, whereas if the passed Comparator orders by the passenger number, this will be the ordering criteria.
What Java 8 does is to extend the number of functional interfaces and their usability defining a set of methods whose arguments are those interfaces.

2. Justification
Let us study another possible application of functional interfaces different from Comparator. There is a good number of algorithms that use a boolean condition on their scheme. One of the simplest examples is the counter algorithm pattern, which returns the amount of elements of a collection that fulfil a certain condition: How many full flights take departure today? How many flights go to Madrid this week? etc. We know that this algorithm’s scheme is the following:

       Scheme counter
             Initiate counter
             For each element in collection
                    If condition
                           Increment counter
                    EndIf
             EndFor
             Return counter
       }

This scheme takes as input the elements collection and the condition they must verify, and the counter as output. For instance, let us have a look at the methods from Airport that count the number of flights towards a specific destination and the number of flights after a specific date.

public Integer counterFlightsDate(LocalDate f){
       Integer counter=0;
       for(Flight v:flights){
             if (v.getDate().compareTo(f)>0){
                    counter++;
             }
       }
       return counter;
}
 
public Integer counterFlightsDestination(String d){
       Integer counter =0;
       for(Flight v:flights){
             if (v.getDestination().equals(d)){
                    counter ++;
             }
       }
       return counter;
}

It is clear that the code is exactly the same for both methods, except for the condition and the parameter type: the LocalDate f for the first case and the destination String d of the second one.  This code would be invoked as follows:

Integer cont1 = SVQairport.counterFlightsDate("Madrid");
System.out.println("The number of flights to Madrid is "+cont1);
Integer cont2 = SVQairport.counterFlightsDestination(LocalDate.of(2014,07,16));
System.out.println("The number of flights later than 16th of July is "+cont2);

Let us assume we could pass the condition we specify on the “if” as a parameter. Then, the method code could be generalized to something like:

public Integer genericalFlightCount(Condition filter[1]){
       Integer counter=0;
       for (Flight v:flights){
             if (condition of filter over v[2]){
                    counter ++;
             }
       }
       return counter;
}

This way, we would have a generic counter method for Airport which, once coded, could be invoked with different boolean expressions and have different functionalities. In order to be able to pass a boolean expression as a parameter, we would need a type (an interface) Condition that would implement a method receiving and object of the involved type (Flight in this case) and would return a value of type boolean with the requested condition. This possibility is the functional interface Predicate, which will be the first example to be studied in the following section.
The invocation of this generic method would be:
Integer cont1 = SVQairport.genericalFlightCount(v-> v.getDestination.equals("Madrid")[3]);
System.out.println("The number of flights from Madrid is "+cont1);
 
LocalDate f = LocalDate.of(2014,07,16)
Integer cont2 =
      SVQairport.genericalFlightCount
             (v->v.getDate().compareTo(f)>0[4]);
System.out.println("The number of flights later than 16th of July is "+cont2);

[1] Later, this hypothetical type Condition will actually be the type Predicate

2 This expression will actually be filter.test(v), since this is the method implemented in Predicate

3 This is a lambda expression (see next section) that indicates that the “condition” or “filter” that is passed as a parameter indicates that each Flight v returns a condition over whether or not its destination is Madrid

4 This is another lambda expression that, for each Flight v, returns true if the departure date is later that the 14th of July

3. Lambda Expressions
There is another point that changes in Java 8, and it is the way functional interfaces are provided as parameter to the methods. As we saw on the previous example, in order to use a Comparator in Java7, an external class containing the compare method is defined, and an object of that class is passed to the method that requires it, either upon invocation or with a previously created object. Java 8 has to other, more flexible mechanisms to define functional interfaces: lambda expressions and method references.
A lambda expression is a simplification of a method in which the input parameters and the output expression are separated by an arrow operator ‘->’. Input parameters are written between brackets and separated by commas. If the interface has a single input argument, brackets are not necessary. So, the shape of the first part of a lambda expression will be like () -> if there are no input parameters, x-> if there is only one or (x, y) -> for two arguments. Usually, it is not necessary to define the type of the arguments, because Java 8 can deduce them from the context. After the arrow operator -> we must write the expression that will be the return value for the interface that we are declaring.


Example 1. A functional interface receiving a Flight and returning its price would be:

       x-> x.getPrice();

Example 2. A functional interface receiving a String that represents a number and returning an Integer with the corresponding value would be written as:

       x -> new Integer(x);

Example 3.  Let us have another example with the interface Comparator. The invocation to the sort method from Collections can be performed constructing the required Comparator with a lambda expression directly on the call code:

Collections.sort(flightList,(x,y)->x.getPrice().compareTo(y.getPrice()));

The lambda expression is formed by its arguments, two in this case: x and y between brackets and separated by a comma. There are two arguments because the compare method from the interface Comparator also needs two arguments. As we can observe x and y are references to the type Flight even though we do not need to formally specify that, because the Java 8 compilator is able to “understand” it from the context, because since we are ordering a List, the Comparator must be of type Flight and thus the arguments for the compare method must also be of that type. Then, after the arrow symbol ‘->’ we write the expression the compare method should return; in our case, an expression of type int with the comparison between the price of the flights.

Another way of referencing the Comparator is invoking the method that returns the property we want to compare. For instance, Java 8 allows this other invocation:

Collections.sort(flights,Comparator.comparing(Flight::getPrice));

At this second invocation, the Comparator interface calls the static method comparing, whose argument is a functional interface of type Function that can be defined just by referencing the method that returns the property we want to compare.

Lambda expressions are most commonly used directly upon the invocation of the method that requires a functional interface, but if a certain lambda expression is going to be used several times, it can be declared with an identifier.

Example. To define if a Flight is full we will write:

Predicate flightFull =
             x-> x.getNumPassengers().equals(x.getNumSeats());

This way, the identifier flightFull can substitute a Predicate interface on all of the invocations that have an argument of type Predicate.

If the functional interface is going to need input arguments of a type different to the one specified for the interface itself, the better way to define it is as if it was a method.

Example. If we need to define a Predicate for Flight that receives an argument of type Date and tells if a Flight takes off after the given date, we define:

Predicate<Flight> laterDate(LocalDate f){
       return x -> x.getDate().compareTo(f)>0;
}


4. Predicate
As it has been previously pointed out, the Predicate interface implements a logical condition for methods that need some filter or condition. Predicate implements a method called test that returns a boolean from an object of type T. Therefore, the Predicate type is used to classify objects of type T according to whether or not they satisfy a certain property. For example, given a Flight tell if it is complete, given a Book tell if its title contains some specific word, given a Song tell if it lasts more than x seconds, or given a String tell if it starts by a certain character.

 Example 1. The predicate that tells if a flight is full is:

Predicate flightFull = x->
                    x.getNumPassengers().equals(x.getNumSeats());

Example 2. If we need to define a condition based on a parameter, the functional interface can be given an input argument. For instance, if we need to know if a certain Flight is from a determinate date, we would define:

Predicate equalDate(LocalDate f){
             return x -> x.getDate().equals(f);
}

Also, specialised interfaces such as DoublePredicate, IntPredicate and LongPredicate are available, to obtain a logical value from objects of basic data types.

Default methods.
The Predicate interface also has three methods implementing the logical operations: negate(), and(Predicate) and or(Predicate). For example, if we need an argument of type Predicate that tells if a Flight corresponds to a certain date and it is full, it would be written as:

       equalDate(f).and(flightFull)


5. BiPredicate 
The BiPredicate interface produces a logical value from two parameters of different type. For example, given a String representing a destination and a Flight it would return whether or not that Flight goes to that destination, given a song and a duration it would tell if the song’s duration is smaller than the specified one, etc.

Example. The interface that returns if a Flight takes off at a specified date would be:

 BiPredicate<Flight, LocalDate> getCoincidence =
             (x,y)-> y.equals(x.getDate());


6. Function
Function is an interface with an apply method that receives an argument of type T and returns an object of type R. It is mainly used to transform objects from a type another derived or combined one; for example, the author of a Book, the duration of a Song, the number of passengers of a Flight, etc. Java 8 provides a set of specialised interfaces that differ on their input or output parameter type. For instance, ToDoubleFunction, ToIntFunction, ToLongFunction are specialised on receiving an object of type T and returning the type specified on the interface name. These interfaces implement a method called applyAsX where X will be Double, Int or Long, depending on the case. Conversely, the functions LongFunction, IntFunction and DoubleFunction receive a value of the type specified on the name and return another one of type R, using the apply method. Finally there are six more interfaces called XToYFunction where X, Y take the values Double, Int or Long, being X the input argument type and Y the return value type. The method they implement is ApplyAsY, where Y is the return value type. 

Example 1: The function that given a Flight determines its duration could be defined as:

Function functionDuration = x->x.getDuration();


In this case, if the type Flight has the method getDuration defined, the operator :: can be used on the invocation of the method that uses the type Function as input parameter:
                Flight::getDuration

Of course, if the expression of the Function will not be used more than once, the lambda expression x->x.getDuration() can be the input parameter on the method that require this function.

Example 2. Given a Flight the Function that returns its occupation ratio is:

FunctionfunctOccRatio=
       x-> 1.*x.getNumPasengers()/x.getNumSeats();


This case is a clear example of specialised function:
ToDoubleFunction functOccRatio(){
       return x->1.*x.getNumPassengers()/x.getNumSeats();
}

Default methods
The Function interface has two methods that allow us to operate functions with composition: compose(Function) and andThen(Function). The difference between them is the order in which the involved Functions are applied. The Function resulting from applying the method f.compose(g) first applies g and then f, whereas f.andThen(g) is the result of first applying f and then g.

Example. Let us suppose that we have a Function which, given an object of type Duration, it returns its conversion to minutes:

Function inMinutes=
             x->x.getMinutes()+x.getHours()*60;

 
And another function that returns the duration of a flight:

Function getDuration =
                    Flight::getDuration;

      
Then, the function that returns the duration in minutes of a flight would be:

Function getDurationInMinutes=
                    inMinutes.compose(getDuration);

Or also:
Function<Flight,Integer> getDurationInMinutes =
                    getDuration.andThen(inMinutes);

 
7. BiFunction 
BiFunction is a function that receives two arguments of types T and U and returns a result of type R, using a method called apply. There exist also three interfaces specialised on returning a certain type: ToDoubleBiFunction, ToIntBiFunction and ToLongBiFunction, that implement a method applyAsX where X can be Double, Int or Long.
Example. To obtain a function that, given a Date and a Flight, returns how many days remain between the given date and the moment the flight takes off, it would be:

ToIntBiFunction<Flight, LocalDate> getDays(Flight v, LocalDate f){
       return (x,y)->y.subtract(x.getDate());
}


8. Consumer
The interface Consumer is a variant of Function in which no value is returned, which means it modifies the given object with a method called accept that receives an object of type T and returns void. They are used to define an action over an object. For instance, incrementing the price of a Flight for a certain percentage, subtracting a Date a number of days or printing on the console a value. Java 8 also provides the specialised interfaces DoubleConsumer, LongConsumer or IntConsumer that also implement the method accept.

Example 1. If we want to increase the price of a Flight for 10%, we would define a Consumer:
Consumer<Flight> incrementPrice10p =
             x->x.setPrice(x.getPrice()*1.1);
 
Example 2. If we wanted that increment to be performed over a percentage passed as parameter, we could write the following method for the type Flight:

Consumer<Flight> incrementPrice(Double p){
       return x->x.setPrice(x.getPrice()*(1+p/100.));
}

Example 3. It is very common to find the following Consumer to replace the expression System.out.println:

Consumer<Flight> printFlight =
       x->System.out.println(x);

Example 4. If we wanted to have a method of Flight that would implement a certain action over an object of type Flight depending on a condition, we could write:

public void applyAction(Predicate<Flight> cond, Consumer<Flight> act){
       if (cond.test(this)){
             act.accept(this);
       }
}

Once we would have an object v of type Flight, the invocation of the previous method to increase the price of v if the number of passengers is below 50 would be:

v.applyAction(x->x.getNumPassengers()<50 o:p="">
                x->x.incrementPrice(10.));

where incrementPrice is the Consumer defined in Example 2.


9. BiConsumer 
BiConsumer is an interface to define an action over two input arguments of different type. It is used to represent actions that modify an object receiving an object of another type. Its specialised interfaces are: ObjDoubleConsumer, ObjIntConsumer and ObjLongConsumer that receive an object of type T and another one of the type specified on the name. All of them implement a functional method called accept.

Example. To change the duration of a Flight, we could write the following code:

BiConsumer<Flight, Duration> changeDuration = (x,y)->x.setDuration(y);


10. Supplier 
Supplier is an interface that provides an object of type T without any argument using a method called get. Also, there are specialised interfaces like BooleanSupplier, DoubleSupplier, IntSupplier and LongSupplier to provide objects of the indicated type. In these cases the method they implements is called getAsX, where X is Boolean, Double, Int or Long respectively.

Usually, the interfaces of type Supplier will just invoke constructors. This way, a lambda expression to invoke the constructor of Flight supposing that FlightImpl has a default constructor would be:

Supplier<Flight> giveMeFlight = ()-> new FlightImpl();

If we want the supplier to have an argument we will have to write:

Supplier<Flight> giveMeFlight (String s) {
       return ()->new FlightImpl(s);
}

Another usual way to build suppliers is using the method expression:

Supplier> giveMeSet = HashSet::new;


11. UnaryOperator 
The interface UnaryOperator represents an operation that receives a single parameter of type T and returns another object of the same type, using the method apply. It is a particular case of the Function interface with the same type for the input and output values and Java implements it as a subinterface of Function. Java 8 also has the specialised interfaces DoubleUnaryOperator, IntUnaryOperator and LongUnaryOperator that implement the method applyAsX being X the character chain Double, Int or Long respectively. Since this interface is a subinterface of Function, it also implements the default methods compose and andThen with the same behaviour.

Example 1. If we need an operator to modify a Duration adding it a certain amount of minutes given with a parameter, we would write:

public UnaryOperator addMinutes(Integer m){
        return x -> x.sum(new DurationImpl(0,m));
}


12. BinaryOperator 
The interface BinaryOperator represents an operation that receives two operands of type T and returns a result of the same type using the method apply. As we can see, it is a particular case of the interface BiFunction where the three types T, U and R are the same and Java 8 implements it as a subinterface of BiFunction. There are also specialisations like DoubleBinaryOperator, IntBinaryOperator and LongBinaryOperator to operate numerical values. In these interfaces, the method they implement is applyAsX, where X can take the names Double, Int or Long respectively. 

Example 1. We have a type Duration defined, which stores the duration of a Flight in hours and minutes. If the type Duration already has the sum method defined:

public Duration sum(Duration d) {
       Integer min = getMinutes() + d.getMinutes();
       Integer hour = getHours() + d.getHours();
       return new DurationImpl(hour+min/60,min%60);
}

Then we could redefine it as a BinaryOperator:

BinaryOperator addDur = (x,y) -> x.sum(y);

Equivalent to this other expression:

BinaryOperator addDur = Duration::sum;

If the method sum was not defined for Duration we could directly define:

BinaryOperator addDur = (x,y)-> {
       Integer min = x.getMinutes() + y.getMinutes();
       Integer hour = x.getHours() + y.getHours();
       return new DurationImpl(hour+min/60,min%60);
};

Example 2. The interface DoubleBinaryOperator allows us to define real functions as a composition of other two. For example, if we wanted to define a function h as the quotient of other two unknown functions f and g, we would write the code:

public DoubleBinaryOperator functionH(DoubleBinaryOperator f,
                                        DoubleBinaryOperator g){
       return (x,y)->f.applyAsDouble(x,y)/g.applyAsDouble(x,y);
}

This way, a possible invocation for the quotient between the addition and the product of two numbers would be:

public Double callFunctionH(Double x, Double y){
       return functionH((a,b)->a+b,(a,b)->a*b).applyAsDouble(x,y);
}

 

No hay comentarios:

Publicar un comentario