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

Hace poco descubrí que las interfaces funcionales añadidas en Java 8 (Predicate, Function, etc) no solo tienen su correspondiente método abstracto, sino que además incluye unos métodos ‘default‘ muy útiles. Vamos a ver aquí en concreto los de la interfaz Predicate.

Concepto

Uno de las más importantes novedades en Java 8 son las lambdas. Para poder usarlas fácilmente, se han añadido varias interfaces funcionales (FunctionalInterface) para no tener que implementarlas nosotros, como son Function, Consumer, Supplier, Predicate

Estas interfaces, al ser FunctionalInterface solo tienen un único método. La interfaz Predicate, por ejemplo:

@FunctionalInterface
public interface Predicate<T> {
    boolean test(T t);
}

La interfaz Predicate sirve para evaluar una condición, devolviendo un booleano, y recibiendo un objeto como parámetro.
Su único método es el método ‘test‘, que realiza esa evaluación.

Pero si vemos el código de la interfaz Predicate veremos que también tiene varios métodos ‘default‘, que son los siguientes:

default Predicate<T> and(Predicate<? super T> other) {
    ...
}

default Predicate<T> negate() {
    ...
}

default Predicate<T> or(Predicate<? super T> other) {
    ...
}

Aunque quizá en principio no los necesitemos, en algunos casos pueden ser útiles, sobre todo a la hora de tener un código más legible. Vamos a ver un ejemplo de cómo podríamos usar esos métodos.

Ejemplo

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)
);

Si quisiéramos filtrar de nuestra lista los personajes que son jóvenes y que además lleven espada, haríamos algo así:

List<BookCharacter> youngsAndSwords =
        bookCharacters.stream()
                      .filter(bChar -> bChar.getAge() < 90 && Weapon.SWORD.equals(bChar.getMainWeapon()))
                      .collect(Collectors.toList());

o así:

List<BookCharacter> youngsAndSwords =
        bookCharacters.stream()
                      .filter(bChar -> bChar.getAge() < 90)
                      .filter(bChar -> Weapon.SWORD.equals(bChar.getMainWeapon()))
                      .collect(Collectors.toList());

La recomendada y más ‘clean code’ es la segunda manera, para separar los filtros.

Esta es la manera común de implementar las lambdas, pero en algunos casos nos puede interesar tener nuestras lambdas definidas en una variable, atributo o incluso una clase, para reutilizarla en más sitios. Algo como esto:

package com.edwise.pocs.java8predicatemethods;

// imports

public class BookCharacterPredicate {
    public static Predicate<BookCharacter> isYoung() {
        return bChar -> bChar.getAge() < 90;
    }

    public static Predicate<BookCharacter> useSword() {
        return bChar -> BookCharacter.Weapon.SWORD.equals(bChar.getMainWeapon());
    }

    public static Predicate<BookCharacter> isHuman() {
        return BookCharacter::isHuman;
    }

    public static Predicate<BookCharacter> isValid() {
        return bChar -> bChar.getName() != null &&
                bChar.getAge() > 0 &&
                bChar.getMainWeapon() != null;
    }
}

De esa manera, podríamos reescribir nuestro stream así:

List<BookCharacter> youngsAndSwords =
        bookCharacters.stream()
                      .filter(isYoung())
                      .filter(useSword())
                      .collect(Collectors.toList());

o así:

List<BookCharacter> youngsAndSwords =
        bookCharacters.stream()
                      .filter(isYoung().and(useSword()))
                      .collect(Collectors.toList());

En el segundo caso usamos el método ‘and‘ de Predicate, que acepta otro Predicate, y, por supuesto, hace un and lógico sobre los dos.

Si queremos filtrar los que no usan espada:

List<BookCharacter> notUseSword =
        bookCharacters.stream()
                      .filter(useSword().negate())
                      .collect(Collectors.toList());

Usamos el método ‘negate‘ que lo que hace es devolver la negación lógica de nuestro Predicate.

Y si queremos un filtro algo retorcido que nos devuelva los que no son humanos o usan espada:

List<BookCharacter> notHumanOrSwords =
        bookCharacters.stream()
                      .filter(isHuman().negate().or(useSword()))
                      .collect(Collectors.toList());

Aquí, además de ‘negate’, usamos el método ‘or‘, que hace un OR lógico con los dos Predicates.

Otra manera de usar nuestros Predicate podría ser tener un método que recibe, entre otras cosas, ese Predicate, por ejemplo:

public class BookCharacterChecker {

    public void doSomeStuffIfThisAndValid(BookCharacter bChar,
                                          Predicate<BookCharacter> predicate) {
        if (predicate.and(BookCharacterPredicate.isValid()).test(bChar)) {
            // do some stuff
            System.out.println("doing stuff with result true");
        } else {
            // do other stuff
            System.out.println("doing stuff with result false");
        }
    }
}

...

BookCharacter gandalf =
        new BookCharacter("Gandalf", Integer.MAX_VALUE, Weapon.STAFF, false);
BookCharacterChecker bookCharacterChecker = new BookCharacterChecker();

bookCharacterChecker.doSomeStuffIfThisAndValid(gandalf, bChar -> bChar.getAge() > 90);

En este caso, recibimos la lambda como parámetro, un Predicate, realizamos un and con otro Predicate y hacemos una cosa u otra según se cumpla.

Bonus: método estático isEqual

La interfaz Predicate tiene también un método static ‘isEqual‘, que devuelve un Predicate. Nos sirve para crear Predicates para comprobar igualdad. Por ejemplo:

BookCharacter aragorn = new BookCharacter("Aragorn", 88, Weapon.SWORD, true);
Predicate<BookCharacter> equalToAragorn = Predicate.isEqual(aragorn);

List<BookCharacter> allExceptAragorn =
        bookCharacters.stream()
                      .filter(equalToAragorn.negate())
                      .collect(Collectors.toList());

Aquí creamos con el método ‘isEqual‘ un Predicate que evalúe si un objeto es igual al personaje Aragorn. Luego filtramos nuestro stream negándolo, luego el List resultante debería devolver todos excepto Aragorn.

Hasta aquí por hoy, todo el código del post está en mi github, proyecto java8-predicate-methods-example.