Java 8 tips: métodos default en la interfaz funcional Function

Como vimos en mi anterior post de Java 8, la interfaz Predicate nos ofrece varios métodos ‘default’ que pueden ser muy útiles. El resto de FuntionalInterface de Java 8 también tiene otros. En el caso de la interfaz Function, tiene alguno interesante, vamos a verlo en este post.

Concepto

Continuando con el post sobre métodos ‘default’ en la interfaz Predicate, vamos a ver aquí que otros métodos aporta la interfaz Function.

La interfaz Function representa una función, que acepta un parámetro y devuelve otro. Es una interfaz muy genérica, que se puede usar para muchos casos, e incluso es extendida por otras interfaces funcionales, como UnaryOperator (es igual que Function, pero los párametros de entrada y de salida son del mismo tipo).

Además, tiene varías implementaciones similares, como son BiFunction (como Function, pero con 2 parámetros de entrada).

Su implementación básica es la siguiente:

@FunctionalInterface
public interface Function<T, R> {

    R apply(T t);
}

Su método de interfaz funcional, ‘apply‘, recibe el parámetro y devuelve el resultado.

Pero, al igual que Predicate, tiene unos métodos ‘default’ extra, así como un método estático:

default <V> Function<V, R> compose(Function<? super V, ? extends T> before) {
   ...
}

default <V> Function<T, V> andThen(Function<? super R, ? extends V> after) {
    ...
}

static <T> Function<T, T> identity() {
    ...
}

Ejemplo:

Al igual que en el anterior post, todos los ejemplos los haremos sobre un List de BookCharacter:

public class BookCharacter {
    private final String name;
    private final Integer age;
    private final Weapon mainWeapon;
    private final boolean human;

    public BookCharacter(String name, Integer age, Weapon mainWeapon, boolean human) {
        this.name = name;
        this.age = age;
        this.mainWeapon = mainWeapon;
        this.human = human;
    }

    public enum Weapon {SWORD, AXE, BOW, STAFF, RING}

    // getters, toString, equals and hashCode
}
...
List<BookCharacter> bookCharacters = Arrays.asList(
        new BookCharacter("Gandalf", Integer.MAX_VALUE, Weapon.STAFF, false),
        new BookCharacter("Aragorn", 88, Weapon.SWORD, true),
        new BookCharacter("Gimli", 140, Weapon.AXE, false),
        new BookCharacter("Legolas", 2931, Weapon.BOW, false),
        new BookCharacter("Boromir", 41, Weapon.SWORD, true),
        new BookCharacter("Frodo", 51, Weapon.RING, false),
        new BookCharacter("Sam", 33, Weapon.SWORD, false)
);

Tanto el método ‘compose‘ como el ‘andThen‘ hacen exactamente lo mismo, que es realizar una composición de funciones. Lo único que los diferencia es el orden en el que lo hacen. Dependiendo de cual nos guste o encaje más (principalmente en legibilidad) elegiremos una u otra.

Imaginemos que tenemos definidas estas dos funciones:

    public static Function<List<BookCharacter>, BookCharacter> findFirstSwordsman() {
        return list ->
                list.stream()
                    .filter(bookChar -> Weapon.SWORD.equals(bookChar.getMainWeapon()))
                    .findFirst()
                    .orElse(new BookCharacter("NOBODY", 0, Weapon.SWORD, true));
    }

    public static Function<BookCharacter, String> characterToCode() {
        return bookCharacter ->
                bookCharacter.getName() + "::" + bookCharacter.getMainWeapon() + "::" +
                        (bookCharacter.isHuman() ? "human" : "nonHuman");
    }

La primera nos devuelve el primer BookCharacter espadachín de un List de BookCharacter (devolviendo un ‘NODOBY’ si no encontrará ninguno).
La segunda nos devuelve un String formado a partir de un BookCharacter.

Vamos a crear otra función que sea una composición de esas dos funciones, usando los métodos ‘andThen’ y ‘compose’:

        Function<List<BookCharacter>, String> andThenFunction =
                findFirstSwordsman().andThen(characterToCode());

        Function<List<BookCharacter>, String> composeFunction =
                characterToCode().compose(findFirstSwordsman());

Esas dos funciones, tal cual las hemos definido hacen lo mismo. La diferencia, como comenté antes, es el orden.
En el primer caso, con el método ‘andThen‘ primero se ejecuta la función sobre la que llamamos a andThen, y sobre el resultado de esta, se llama a la función pasada como parámetro.
En el segundo caso, con el método ‘compose‘, primero es ejecutada la función pasada como parámetro, y sobre el resultado de esta, se llama a la función sobre la que llamamos a compose.

Por último, Function nos provee de un método estático llamado ‘identity‘. Este método devuelve la llamada función ‘identidad’, que no es más que un Function que devuelve exactamente lo mismo que ha recibido como parámetro.
Por ejemplo, si a partir de nuestro List de BookCharacter queremos obtener un Map, con los nombres como claves y los BookCharacter como valores, haríamos lo siguiente:

        Map<String, BookCharacter> characterMap =
                bookCharacters.stream()
                              .collect(Collectors.toMap(BookCharacter::getName,
                                      bookChar -> bookChar));

Como vemos, usamos el método ‘toMap‘ de Collectors que recibe dos funciones, una para obtener la clave, y otra para el valor. En nuestro caso, el valor es exactamente el objeto recibido, luego tenemos ese ‘bookChar -> bookChar’.

Podemos en lugar de eso usar la función identidad de Function:

        Map<String, BookCharacter> characterMap =
                bookCharacters.stream()
                              .collect(Collectors.toMap(BookCharacter::getName,
                                      Function.identity()));

¿Sirve para algo usar la función identidad en lugar de lo anterior? No especialmente. Quizá por legibilidad nos guste más usar la función identidad, o quizá no… al gusto de cada uno 🙂

Tenéis en mi github todo el código usado en este post, en el proyecto java8-function-methods-example.

Deja un comentario