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:
Integer
counter=0;
for (Flight v:flights){
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:
System.out.println("The number of flights from Madrid
is "+cont1);
LocalDate f = LocalDate.of(2014,07,16)
Integer cont2
=
SVQairport.genericalFlightCount
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
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:
Function functOccRatio=
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="">50>
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