De null a full

Programación Java

View on GitHub

Interfaces funcionales

El primer concepto a entender para trabajar en programación funcional en Java es el de las interfaces funcionales. Una interfaz funcional es cualquier interfaz con un único método abstracto.

Se recomienda, pero no es obligatorio, que las interfaces funcionales estén anotadas con @FunctionalInterface.

Una interfaz funcional puede tener cualquier número de métodos default (métodos que la propia interfaz implementa).

Por qué usar programación funcional

Las principales ventajas de la programación funcional son:

Por ejemplo, digamos que quiero coger una lista de Strings y obtener una lista de ellos en formato “texto-ejemplo”. La opción tradicional sería:

List<String> strings = Arrays.asList("valor 1", null, "valor 2", "");
List<String> tags = new ArrayList<>();
for (String string: strings) {
    if (string != null && !string.isEmpty()) {
        String tag = string.toLowerCase().replaceAll("\\s+", "-");
        tags.add(tag);
    }
}
System.out.println(String.join(", ", tags));

Y la opción funcional:

List<String> strings = Arrays.asList("valor 1", null, "valor 2", "");
List<String> tags = strings.stream()
        .filter(StringUtils::isNotEmpty)
        .map(this::toTag);
        .collect(Collectors.toList());

Interfaces funcionales comunes

Existe una gama de especializaciones para los tipos primitivos double, int y long como ToIntFunction o LongUnaryOperator. Si sabemos que podemos trabajar con tipos primitivos es bueno usarlos por su menor huella de memoria y mejor rendimiento.

Modos de uso: de menos a más funcional

Para ilustrar las maneras de implementar interfaces funcionales usaremos como ejemplo un Predicate en el siguiente código:

List<String> strings = Stream.of("a", null, "b", null, "c")
        .filter(predicate)
        .collect(Collectors.toList());

Predicate<T> es una interfaz funcional cuyo SAM es boolean test(T), es decir, un booleano en función del objeto a analizar.

Haremos uso de la programación funcional para cribar los nulos de la lista.

Implementación explícita

La opción clásica es generar una clase que implemente nuestra operación:

public class NotNullPredicate<T> implements Predicate<T> {
    @Override
    public boolean test(T t) {
        return t != null;
    }
}

Y usar una nueva instancia en el código:

List<String> strings = Stream.of("a", null, "b", null, "c")
        .filter(new NotNullPredicate<>())
        .collect(Collectors.toList());

Esto puede funcionar bien con ejemplos sencillos, pero si necesitamos otros objetos para evaluar nuestro predicado los tendremos que pasar al constructor, pudiendo resultar en monstruosidades como new ValidItemPredicate(servicioValidacion, objetosYaExistentes, cache, indice), que son difíciles de entender, y por lo tanto de mantener.

Implementación anónima

La otra opción que teníamos disponible antes de Java 8 es la implementación anónima, disponible para cualquier tipo de interfaz:

List<String> strings = Stream.of("a", null, "b", null, "c")
        .filter(new Predicate<String>() {
            @Override
            public boolean test(String t) {
                return t != null;
            }
        })
        .collect(Collectors.toList());

Con respecto a la implementación explícita ganamos el no tener que pasar argumentos a un constructor y no mantener otra clase, pero declarar implementaciones anónimas puede hacer que nuestro código sea menos legible.

Lambda

La implementación anónima puede aligerarse en el caso de las interfaces funcionales, aprovechando que ya sabe qué interfaz y método rellena:

List<String> strings = Stream.of("a", null, "b", null, "c")
        .filter((String t) -> {
            return t != null;
        })
        .collect(Collectors.toList());

Podemos ir varios pasos más allá para minimizar la expresión:

El resultado de aplicar estas abreviaturas es el siguiente:

List<String> strings = Stream.of("a", null, "b", null, "c")
        .filter(t -> t != null)
        .collect(Collectors.toList());

Referencia de método

Supongamos que queremos encapsular nuestra lógica para reutilizarla en otros puntos, y creamos para ello un método isNotNull:

List<String> strings = Stream.of("a", null, "b", null, "c")
        .filter(t -> isNotNull(t))
        .collect(Collectors.toList());

Cuando nuestra función consiste en pasar los argumentos tal cual a un método podemos convertir la lambda en una referencia de método:

List<String> strings = Stream.of("a", null, "b", null, "c")
        .filter(this::isNotNull)
        .collect(Collectors.toList());

Esto también aplica a métodos estáticos, como Objects.nonNull:

List<String> strings = Stream.of("a", null, "b", null, "c")
        .filter(Objects::nonNull)
        .collect(Collectors.toList());

Y también se puede utilizar cuando el SAM tiene un sólo argumento para que llame a un método de su propia clase

List<Integer> lengths = Stream.of("a", null, "b", null, "c")
        .filter(Objects::nonNull)
        .map(String::length) // equivalente a implementar str -> str.length()
        .collect(Collectors.toList());

Ejercicios

Implementa las siguientes interfaces funcionales:

Enlaces de interés