1 Interfaces funcionales¶
Introducción¶
La programación imperativa es uno de los paradigmas de programación de computadoras más utilizados. Bajo este paradigma, la programación se describe en términos del estado del programa y de sentencias que cambian dicho estado. Java es un lenguaje imperativo, lo que implica que un programa Java está compuesto por una secuencia de instrucciones, que son ejecutadas en el mismo orden en el que se escriben, de manera que al ejecutarla se produce cambios en el estado del programa.
Por su parte, la programación funcional es un paradigma de programación alternativo, en el que el resultado de un programa deriva de la aplicación de distintas funciones a la entrada, sin cambiar el estado interno del programa. En la programación funcional los bloques principales de construcción de nuestros programas son las funciones y no los objetos.
Al aplicar programación funcional se produce normalmente un código más corto y más sencillo de entender que aplicando programación imperativa, ya que es más fácil crear abstracciones a través de funciones que a través de interfaces.
Java siempre fue un lenguaje para programación imperativa y de hecho las funciones en Java NO son objetos, por lo que una función no puede pasarse directamente como argumento de otra función para que se ejecute su código. Sin embargo, gracias a las interfaces funcionales y a las clases inline anónimas podíamos superar esta limitación. Pero ¿qué es una interfaz funcional?
Interfaces funcionales¶
Una interfaz funcional es una interfaz que contiene un único método abstracto. Esto no quiere decir que no pueda contener otros métodos. De hecho, puede contener:
- Otros métodos
static
(Java 8+). - Otros métodos
default
(Java 8+). - Otros métodos
private
(Java 9+) - Métodos que sobrescriban métodos de la clase
Object
.
A la hora de definir una interfaz funcional, Java 8 proporciona la anotación @FunctionInterface
, que informa al compilador de que dicha interfaz es funcional y por tanto tiene un único método abstracto. El objetivo de esta anotación es que se produzca un error de compilación si le añadimos un segundo método abstracto a la interfaz. El uso de esta anotación no se ha establecido como obligatoria para mantener la compatibilidad con el código ya existente, pero sí que está recomendada.
Una interfaz funcional pura es aquella en la que las clases que la implementan no almacenan ningún estado, como por ejemplo Comparator
. Veamos el método sort
de la interfaz List<E>
que recibe un objeto de una clase que implementa la interfaz Comparator
: default void sort(Comparator<? super E> c)
. El método utiliza el Comparator
para ordenar la lista llamando al método compare
de dicho objeto cada vez que debe comparar dos objetos de la lista. Por lo tanto, debemos crear una clase que implemente Comparator
para determinar cómo se comparan dos elementos:
import java.util.Comparator;
public class ListOrder implements Comparator<Integer>{
@Override
public int compare(Integer o1, Integer o2){
return Integer.compare(o1, o2);
}
}
Así, cuando queramos ordenar una lista de enteros haríamos:
import java.util.Arrays;
import java.util.List;
public class Main{
public void show(){
List<Integer> list = Arrays.asList(3, 2, 6, 1, 5, 4);
list.sort(new ListOrder());
for(Integer i : list){
System.out.printf(" %d ", i);
}
}
public static void main(String[] args){
new Main().show();
}
}
Implementación de interfaces funcionales mediante clases inline anónimas¶
El problema del código anterior es que si esta ordenación se hace solamente en dicha ocasión, se ha creado la clase ListOrder
para un único uso. En ese caso, es más conveniente utilizar una clase inline anónima:
import java.util.Arrays;
import java.util.List;
public class InlineAnonymousClass{
public void show(){
List<Integer> list = Arrays.asList(3, 2, 6, 1, 5, 4);
list.sort(new Comparator<Integer>(){
@Override
public int compare(Integer o1, Integer o2){
return Integer.compare(o1, o2);
}
});
for(Integer i : list){
System.out.printf(" %d ", i);
}
}
public static void main(String[] args){
new InlineAnonymousClass().show();
}
}
Lo que estamos haciendo es indicarle al método sort()
el código que debe ejecutar para comparar dos objetos. Entonces, ¿no sería más fácil que al método sort()
le pudiéramos pasar directamente el código que debe ejecutar? El problema es que en Java las funciones no son objetos, por lo que no pueden ser referenciadas mediante una variable o pasadas directamente como argumento.
En realidad, lo que nos interesa es poder establecer tipos función, es decir, tipos que representen una función que reciba unos determinados parámetros de algún tipo y que devuelva un valor de retorno de algún tipo. Si existieran los tipos función, podríamos definir variables o parámetros de dichos tipos. De hecho hay lenguajes de programación que tienen tipos función. En Java, debido a la necesidad de mantener la compatibilidad con versiones anteriores, no existe ninguna sintaxis especial para definir tipos función sino que se utilizan las interfaces funcionales para representarlos. Dado que una interfaz funcional solo puede tener un único método abstracto, la firma de dicho método puede ser usado como tipo función.
Interfaces funcionales puras predefinidas¶
Java incorpora, a partir de la versión 8, una serie de interfaces funcionales puras predefinidas en el paquete java.util.function
para permitir la programación funcional en Java:
Function<T,R>
: su método abstracto esR apply(T t)
.UnaryOperator<T>
: es un caso específico de la interfaz funcional Function, es decir, coinciden el tipo del argumento y el tipo de retorno, por lo que está parametrizada con un único tipo. BiFunction<T,U,R>
: su método abstracto esR apply(T t,U u)
.BinaryOperator<T>
:es un caso específico de la interfaz funcional BiFunctionen el que coinciden el tipo de los dos argumentos recibidos por el método apply y el tipo de retorno del mismo. Es por tanto similar a BiFunction . La interfaz funcional BinaryOperator está, por tanto, parametrizada con un único tipo. Predicate<T>
: su método abstracto esboolean test(T t)
.BiPredicate<T>
: su método abstracto esboolean test(T t,U u)
.Consumer<T>
: su método abstracto esvoid accept(T t)
.BiConsumer<T,U>
: su método abstracto esvoid accept(T t,U u)
.Supplier<T>
: su método abstracto esT get()
.
Veamos un ejemplo utilizando la interfaz funcional BinaryOperator<T>
public class Main2 {
public void show(){
ShowBinaryOperator binOper = new ShowBinaryOperator();
System.out.printf("12 + 6 = %d\n", binOper.calculate(12, 6, new BinaryOperator<Integer>(){
@Override
public Integer apply(Integer t, Integer u){
return t + u;
}
}));
System.out.printf("12 - 6 = %d\n", binOper.calculate(12, 6, new BinaryOperator<Integer>(){
@Override
public Integer apply(Integer t, Integer u){
return t - u;
}
}));
System.out.printf("12 / 6 = %d\n", binOper.calculate(12, 6, new BinaryOperator<Integer>(){
@Override
public Integer apply(Integer t, Integer u){
return t / u;
}
}));
System.out.printf("12 * 6 = %d\n", binOper.calculate(12, 6, new BinaryOperator<Integer>(){
@Override
public Integer apply(Integer t, Integer u){
return t * u;
}
}));
}
public static void main(String[] args){
new Main2().show();
}
}
Interfaces funcionales para tipos primitivos¶
Como no podemos usar la parametrización de clases e interfaces con los tipos primitivos (limitación de generics), el paquete java.util.function
define también una serie de interfaces funcionales similares a las explicadas anteriormente pero específicas para los tipos primitivos:
- Para el tipo primitivo boolean:
BooleanSupplier
- Para el tipo primitivo double:
DoubleBinaryOperator
,DoubleConsumer
,DoubleFunction
,DoublePredicate
,DoubleSupplier
,DoubleToIntFunction
,DoubleToLongFunction
,DoubleUnaryOperator
,ToDoubleBiFunction
,ToDoubleFunction
,ObjDoubleConsumer
. - Para el tipo primitivo int:
IntBinaryOperator
,IntConsumer
,IntFunction
,IntPredicate
,IntSupplier
,IntToDoubleFunction
,IntToLongFunction
,IntUnaryOperator
,ToIntBiFunction
,ToIntFunction
,ObjIntConsumer
. - Para el tipo primitivo long:
LongBinaryOperator
,LongConsumer
,LongFunction
,LongPredicate
,LongSupplier
,LongToDoubleFunction
,LongToIntFunction
,LongUnaryOperator
,ToLongBiFunction
,ToLongFunction
,ObjLongConsumer
.
Además, la mayoría de las interfaces vistas hasta ahora incluyen métodos cuyo nombre incluye ToTipo
que retornan objetos de interfaces funcionales para tipos primitivos.