Programación Funcional en Java 8: Guía Definitiva de Predicados

En esta extensa guía, veremos la programación funcional y la aplicación de predicados en Java, ¡con funciones Lambda!

Introducción

La interfaz Predicate se introdujo en Java 8 como parte del paquete java.util.function. El lanzamiento de la versión 8 marca el punto en el que Java adoptó un amplio soporte para las prácticas de programación funcional que se extienden para incluir varias características nuevas, incluidas expresiones lambda, métodos predeterminados e interfaces funcionales predefinidas como el “Predicado” en sí.

Java es un lenguaje orientado a objetos, imperativo en su esencia (en contraste con la práctica declarativa que es la programación funcional). No obstante, era posible aplicar principios funcionales a los programas de Java antes de la versión 8; sin embargo, requería trabajo adicional para eludir la estructura innata del lenguaje y resultó en un código enrevesado. Java 8 trajo formas de aprovechar la verdadera eficacia y facilidad a la que aspira la programación funcional.

Esta guía cubrirá el uso de predicados como una forma de interfaces funcionales en Java.

{.icon aria-hidden=“true”}

Note: It's highly recommended to get acquainted with Interfaces funcionales y expresiones lambda before proceeding to Predicados en Java.

Predicados en Java

Una interfaz funcional es una interfaz que tiene exactamente un método abstracto. Suele ser un método test() o apply() y usted prueba o aplica alguna operación en un elemento.

Por ejemplo, podríamos intentar escribir un sistema de "filtrado" personal que filtre a las personas "amistosas" en una lista, en función de las nociones personales preconcebidas de alguien.

{.icon aria-hidden=“true”}

Nota: Nuestro estándar de "simpatía" se establecerá solo con fines ilustrativos y no refleja ninguna investigación real o análisis estadístico.

Asumiendo que una ‘Persona’ tiene algunos pasatiempos y preferencias:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
enum PetPreference {
    DOGPERSON, CATPERSON, HASAPETSNAKE
}

public class Person {
    private String name;
    private int age;
    private boolean extrovert;
    private PetPreference petPreference;
    private List<String> hobbies;

    // Constructor, getters, setters and toString()
}

Uno podría tener un sesgo hacia ser amigo de extrovertidos que tienen los mismos pasatiempos que ellos. Si bien esta práctica en la vida real probablemente no sea la mejor opción, podríamos filtrar una lista de personas según sus pasatiempos y otras características.

La función test() de la interfaz funcional aceptará una lista de personas para filtrar, terminando con un grupo de personas que, según la opinión aplicada, son "buenas personas":

1
2
3
public interface Bias {
    boolean test(Person p);
}

Aunque la interfaz Bias se escribió para este ejemplo, el comportamiento general que define se implementa todo el tiempo en la programación. Aplicamos constantemente pruebas lógicas para ajustar el algoritmo al estado del programa.

{.icon aria-hidden=“true”}

El paquete java.util.function, emplea Predicados para cubrir los casos en los que se van a aplicar pruebas lógicas, de forma genérica. En general, los predicados se utilizan para probar algo y devolver un valor verdadero o falso según esa prueba.

La interfaz funcional predefinida tiene la estructura estructura, aunque acepta un parámetro genérico:

1
2
3
public interface Predicate<T> {
    boolean test(T t);
}

Podemos omitir la creación de una interfaz Bias personalizada y usar un Predicado en su lugar. Acepta un objeto para probar y devuelve un booleano. Eso es lo que hacen los predicados. Importemos primero el paquete function:

1
import java.util.function.*;

Podemos probar esto creando una ‘Persona’ y probándola a través de un ‘Predicado’:

1
2
3
4
5
Person p1 = new Person("David", 35, true, PetPreference.DOGPERSON, "neuroscience", "languages", "travelling", "reading");

Predicate<Person> bias = p -> p.isExtrovert();
boolean result = bias.test(p1);
System.out.println(result);

El cuerpo de la prueba en sí está definido en la Expresión Lambda: estamos probando si el campo isExtrovert() de una persona es verdadero o falso. Esto podría ser reemplazado por otras operaciones, tales como:

1
p -> p.getHobbies().contains("Being nice to people"); 

Siempre que el resultado final sea un booleano, el cuerpo puede representar cualquier prueba. Ahora, definamos un método filter() que tome una lista de personas y un predicado para usar para filtrarlas:

1
2
3
4
5
6
7
8
9
public static List<Person> filter(List<Person> people, Predicate<Person> bias) {
    List<Person> filteredPeople = new ArrayList<>();
    for (Person p : people) {
      if (bias.test(p)) {
        filteredPeople.add(p);
      }
    }
    return filteredPeople;
}

Para cada persona en la lista, aplicamos el método test() y, según el resultado, las agregamos o las saltamos en la lista de personas filtradas. Hagamos una lista de personas y probemos el método:

1
2
3
4
5
6
7
8
9
Person p1 = new Person("David", 35, true, PetPreference.DOGPERSON, "neuroscience", "languages", "travelling", "reading");
Person p2 = new Person("Marry", 35, true, PetPreference.CATPERSON, "archery", "neurology");
Person p3 = new Person("Jane", 15, false, PetPreference.DOGPERSON, "neurology", "anatomy", "biology");
Person p4 = new Person("Mariah", 27, true, PetPreference.HASAPETSNAKE, "hiking");
Person p5 = new Person("Kevin", 55, false, PetPreference.CATPERSON, "traveling", "swimming", "weightlifting");

List<Person> people = Arrays.asList(p1, p2, p3, p4, p5);

System.out.println(filter(people, p -> p.isExtrovert()));

Dado que un ‘Predicado’ es una interfaz funcional, podemos usar una Expresión Lambda para definir su cuerpo de forma anónima en la llamada al método.

Este código da como resultado:

1
2
3
4
5
[
Person{name='David', age=35, extrovert=true, petPreference=DOGPERSON, hobbies=[neuroscience, languages, travelling, reading]}, 
Person{name='Marry', age=35, extrovert=true, petPreference=CATPERSON, hobbies=[archery, neurology]}, 
Person{name='Mariah', age=27, extrovert=true, petPreference=HASAPETSNAKE, hobbies=[hiking]}
]

El método test()

Podemos inyectar diferentes comportamientos al método test() de Predicate a través de lambdas y ejecutarlo contra objetos Person:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
Person randomPerson = new Person("Aaron", 41, true, PetPreference.DOGPERSON, "weightlifting", "kinesiology");

Predicate<Person> sociable =  c -> c.isExtrovert() == true;
System.out.println(sociable.test(randomPerson));

Predicate<Person> dogPerson = c -> c.getPetPreference().equals(PetPreference.DOGPERSON);
System.out.println(dogPerson.test(randomPerson));

Predicate<Person> seniorCitizen = c -> c.getAge() > 65;
System.out.println(seniorCitizen.test(randomPerson));

El predicado sociable altera el método innato test() para seleccionar extrovertidos. El predicado dogPerson comprueba si una persona es una persona canina y el predicado seniorCitizen devuelve true para personas mayores de 65 años.

Aaron (randomPerson) es extrovertido, amante de los perros, y aún le quedan algunos buenos años hasta que se convierta en una persona mayor. La consola debería leer:

1
2
3
true
true
false

Hemos comparado las características de Aaron con algunos valores fijos (true, DOGPERSON, 65), pero ¿y si quisiéramos generalizar estas pruebas?

Podríamos crear un método para identificar varios alcances de edad en lugar de solo personas mayores o podríamos tener un método de preferencia de mascotas que esté parametrizado. En estos casos, necesitamos argumentos adicionales con los que trabajar y dado que los Predicados solo están destinados a operar en un objeto de un tipo específico, tenemos que construir un método alrededor de ellos.

Vamos a crear un método que tome una lista de pasatiempos y los compare con los pasatiempos que pertenecen a la ‘Persona’ en cuestión:

1
2
3
4
5
6
7
8
public static Predicate<Person> hobbyMatch(String ... hobbies) {
    List<String> hobbiesList = Arrays.asList(hobbies);
    return (c) -> {
        List<String> sharedInterests = new ArrayList<>(hobbiesList);
        sharedInterests.retainAll(c.getHobbies());
        return sharedInterests.size() > 0;
    };
}

El método hobbyMatch() toma una lista de cadenas de longitud variable y las analiza en una lista. La lambda que devuelve hobbyMatch() duplica esta lista en forma de ArrayList y aplica el método integrado retainAll() en el duplicado eliminando los elementos que no coinciden con ningún elemento del c.getHobbies() (manteniendo los elementos comunes entre dos listas).

{.icon aria-hidden=“true”}

Nota: Hemos copiado hobbiesList a sharedInterests ya que las lambdas son funciones puras y no deben causar ningún efecto secundario (como alterar una variable global).

Después de filtrar la lista sharedInterest, la expresión lambda comprueba si existe más de un elemento en la lista y devuelve true si ese es el caso.

Podemos pasar hobbyMatch() al método filter() junto con un grupo de personas y enumerarlos en la consola:

1
2
3
4
5
6
7
8
9
Person p1 = new Person("Marshall", 35, true, PetPreference.DOGPERSON, "basketball", "eating", "reading");
Person p2 = new Person("Marry", 35, true, PetPreference.CATPERSON, "archery", "swimming");
Person p3 = new Person("Jane", 15, false, PetPreference.DOGPERSON, "neurology", "anatomy", "biology");
Person p4 = new Person("Mariah", 27, true, PetPreference.HASAPETSNAKE, "hiking");
Person p5 = new Person("Kevin", 55, false, PetPreference.CATPERSON, "traveling", "swimming", "weightlifting");

List<Person> people = Arrays.asList(p1, p2, p3, p4, p5);

System.out.println(filter(people, hobbyMatch("neurology", "weightlifting")));

Esto resulta en:

1
2
3
4
[
Person{name='Jane', age=15, extrovert=false, petPreference=DOGPERSON, hobbies=[neurology, anatomy, biology]}, 
Person{name='Kevin', age=55, extrovert=false, petPreference=CATPERSON, hobbies=[traveling, swimming, weightlifting]}
]

Método estático: isEqual()

Junto con la interfaz Predicate vino un conjunto de métodos auxiliares para ayudar en las operaciones lógicas. isEqual() es un método estático que compara dos objetos a través del método equals() del parámetro de tipo del objeto Predicate:

1
2
3
4
5
Predicate<Integer> equalToThree = Predicate.isEqual(3);
System.out.println(equalToThree.test(5));

Predicate<String> equalToAaron = Predicate.isEqual("Aaron");
System.out.println(equalToAaron.test(randomPerson.getName()));

El predicado equalToThree está programado para comparar su argumento con 3 a través del método equal() del objeto Integer. equalToThree.test(5) fallará miserablemente.

equalToAaron usará el método equal() del objeto String para comprobar si el argumento de su método test() es igual a "Aaron".

Si aplicamos la prueba a randomPerson creada previamente, el método devuelve true.

Métodos predeterminados y encadenamiento de predicados {#métodospredeterminados y encadenamiento de predicados}

La interfaz Predicate tiene tres métodos predeterminados que ayudan en la creación de expresiones lógicas complejas. Los métodos predeterminados and(), or() y negate() toman una expresión lambda y devuelven un nuevo objeto Predicate con el comportamiento definido. Cuando se vinculan en una cadena, cada predicado nuevo resultante del método predeterminado opera en el vínculo anterior.

Cada cadena debe tener el método funcional test() como enlace final, cuyo parámetro se introduce en el primer Predicado para comenzar la cadena.

y()

Usamos el método and() predeterminado para aplicar la operación lógica and (&&) en dos predicados.

1
2
3
4
5
6
7
8
Person randomPerson = new Person("Aaron", 41, true, PetPreference.DOGPERSON, "weightlifting", "kinesiology");

Predicate<Person> dogPerson = c -> c.getPetPreference().equals(PetPreference.DOGPERSON);

Predicate<Person> sociable =  c -> c.isExtrovert() == true;
System.out.println(sociable.test(randomPerson));

Predicate<Person> seniorCitizen = c -> c.getAge() > 65;

Ahora, podemos encadenar estos predicados:

1
2
3
4
// Chaining with anonymous predicate
System.out.println(dogPerson.and(c -> c.getName().equals("David")).test(randomPerson));
// Chaining with existing predicate
System.out.println(seniorCitizen.and(dogPerson).test(randomPerson));

Hemos traído de vuelta a Aaron, la ‘persona al azar’ para alimentar nuestras cadenas lógicas, y los predicados ‘persona-perro’, ‘sociable’ y ‘ciudadano mayor’ para que sean un eslabón en ellas.

Veamos el primer predicado compuesto del programa:

1
dogPerson.and(c -> c.getName().equals("David")).test(randomPerson)

randomPerson primero pasa por la prueba del predicado dogPerson. Como a Aaron le gustan los perros, el programa pasa al siguiente enlace para aplicar su prueba. El método and() crea un nuevo Predicado cuyo método funcional test() está definido por la expresión lambda dada. Dado que "Aaron" no es igual a "David", la prueba falla y la cadena devuelve falso.

En la segunda cadena, hemos creado enlaces entre las pruebas seniorCitizen y dogPerson. Dado que la primera prueba que se aplica es seniorCitizen y Aaron aún no tiene 65 años, el primer enlace devuelve falso y el sistema sufre un cortocircuito. La cadena devuelve falso sin necesidad de evaluar el predicado dogPerson.

o()

Podemos conectar dos predicados mediante o() para realizar una operación lógica o (||). Vamos a crear una nueva lista de personas con un par de pasatiempos, inspirada en el elenco de personajes de una película popular:

1
2
3
4
Person jo = new Person("Josephine", 21, true, PetPreference.DOGPERSON, "writing", "reading");
Person meg = new Person("Margaret", 23, true, PetPreference.CATPERSON, "shopping", "reading");
Person beth = new Person("Elizabeth", 19, false, PetPreference.DOGPERSON, "playing piano", "reading");
Person amy = new Person("Amy", 17, true, PetPreference.CATPERSON, "painting");

Ahora, usemos el método filter() para extraer las personas de esta lista a las que les gusta leer o son sociables:

1
2
3
List<Person> lilWomen = Arrays.asList(jo, meg, beth, amy);
List<Person> extrovertOrReader = filter(lilWomen, hobbyMatch("reading").or(sociable));
System.out.println(extrovertOrReader);

Esto resulta en:

1
2
3
4
5
6
[
Person{name='Josephine', age=21, extrovert=true, petPreference=DOGPERSON, hobbies=[writing, reading]}, 
Person{name='Margaret', age=23, extrovert=true, petPreference=CATPERSON, hobbies=[shopping, reading]}, 
Person{name='Elizabeth', age=19, extrovert=false, petPreference=DOGPERSON, hobbies=[playing piano, reading]}, 
Person{name='Amy', age=17, extrovert=true, petPreference=CATPERSON, hobbies=[painting]}
]
negar()

El método negate() invierte el resultado del predicado al que se aplica:

1
sociable.negate().test(jo);

Esta declaración prueba jo para la sociabilidad. Entonces negate() se aplica al resultado de sociable.test() y lo invierte. Dado que ‘jo’ es realmente sociable, la afirmación resulta ‘falsa’.

Podemos usar la llamada sociable.negate() en el método filter() para buscar mujercitas introvertidas y agregar .or(hobbyMatch("painting")) para incluir en los pintores:

1
2
List<Person> shyOrPainter = filter(lilWomen, sociable.negate().or(hobbyMatch("painting")));
System.out.println(shyOrPainter);

Esta pieza de código da como resultado:

1
2
3
4
[
Person{name='Elizabeth', age=19, extrovert=false, petPreference=DOGPERSON, hobbies=[playing piano, reading]}, 
Person{name='Amy', age=17, extrovert=true, petPreference=CATPERSON, hobbies=[painting]}
]
no()

not() es un método estático que funciona de la misma manera que negate(). Mientras negate() opera en un predicado existente, el método estático not() suministra una expresión lambda o un predicado existente a través del cual crea un nuevo predicado con cálculo inverso:

1
2
3
4
5
6
Boolean isJoIntroverted = sociable.negate().test(jo);
Boolean isSheTho = Predicate.not(sociable).test(jo);
Predicate<Person> withALambda = Predicate.not(c -> c.isExtrovert());
Boolean seemsNot = withALambda.test(jo);

System.out.println("Is Jo an introvert? " + isJoIntroverted + " " + isSheTho + " " + seemsNot);

Aunque los tres valores booleanos creados por el programa anterior contienen la misma información (Jo no es introvertida), recopilan la información de diferentes maneras.

Tenga en cuenta que no asignamos Predicate.not(c -> c.isExtrovert()).test(jo) directamente al booleano seemsNot. Primero teníamos que declarar un ‘Predicado’ de tipo ‘Persona’ y obtener el resultado de su método ‘prueba()’ más tarde.

Si intentamos ejecutar la declaración de asignación:

1
Boolean seemsNot = Predicate.not(c -> c.isExtrovert()).test(jo)

El compilador grita horrorizado. No tiene forma de saber qué significa c en la lambda o si c es capaz de ejecutar isExtrovert().

Subtipos de predicado

Existen tres subtipos de Predicado para servir objetos no genéricos. El IntPredicate, LongPredicate y DoublePredicate operan en Integers, Longs y Doubles, respectivamente. Definen los métodos predeterminados del ‘Predicado’ genérico, pero estos métodos están destinados a Enteros, Largos y Dobles.

El método isEqual() no se aplica a estos subtipos simplemente porque la operación se puede lograr fácilmente mediante el uso del operador ==:

1
2
3
4
5
6
7
IntPredicate intPredicate = c -> c <= 5;
LongPredicate longPredicate = c -> c%2 == 0;
DoublePredicate doublePredicate = c -> c > 6.0;

System.out.println(intPredicate.negate().test(2));
System.out.println(longPredicate.test(10L));
System.out.println(doublePredicate.or(c -> c < 11.0).test(7.1));

Esto resulta en:

1
2
3
false
true
true
Predicado binario

Los predicados binarios operan sobre dos objetos (pueden ser del mismo tipo o pueden ser instantes de diferentes clases) en lugar de uno, y están representados por la interfaz BiPredicate.

Podemos crear un predicado binario para verificar si los dos objetos Persona tienen pasatiempos compartidos, por ejemplo:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
BiPredicate<Person, Person> sharesHobbies = (x, y) -> {
    List<String> sharedInterests = new ArrayList<>(x.getHobbies());
    sharedInterests.retainAll(y.getHobbies());
    return sharedInterests.size() > 0;
};

Person x = new Person("Albert", 29, true, PetPreference.DOGPERSON, "football", "existentialism");
Person y = new Person("Jean-Paul", 37, false, PetPreference.CATPERSON, "existentialism");

System.out.println(sharesHobbies.test(x,y));

El predicado binario sharesHobbies funciona de la misma manera que el método hobbyMatch() creado anteriormente, aunque sharesHobbies compara los pasatiempos de dos Personas en lugar de comparar los pasatiempos de una Persona con una lista dada de aficiones.

El código da como resultado:

1
true

Conclusión

La interfaz Predicate se introdujo en Java 8 como parte del paquete java.util.function. El lanzamiento de la versión 8 marca el punto en el que Java adoptó un amplio soporte para las prácticas de programación funcional que se extienden para incluir varias características nuevas, incluidas expresiones lambda, métodos predeterminados e interfaces funcionales predefinidas como el “Predicado” en sí.

El uso de Predicates no necesariamente requiere el alcance completo de la comprensión de la Programación Funcional - pero, sin embargo, introduce a los desarrolladores de programación orientada a objetos a varios conceptos muy útiles y flexibles.

Nos hemos centrado en predicados, un tipo de interfaces funcionales en Java, mostrando cómo se pueden usar en sistemas de filtrado para representar criterios de búsqueda.

Licensed under CC BY-NC-SA 4.0