¿Cómo filtrar dinámicamente en Java 8?


Sé que en Java 8, puedo hacer el filtrado así:

List<User> olderUsers = users.stream().filter(u -> u.age > 30).collect(Collectors.toList());

Pero ¿qué pasa si tengo una colección y media docena de criterios de filtrado, y quiero probar la combinación de los criterios ?

Por ejemplo tengo una colección de objetos y los siguientes criterios :

<1> Size
<2> Weight
<3> Length
<4> Top 50% by a certain order
<5> Top 20% by a another certain ratio
<6> True or false by yet another criteria

Y quiero probar la combinación de los criterios anteriores, algo así como:

<1> -> <2> -> <3> -> <4> -> <5>
<1> -> <2> -> <3> -> <5> -> <4>
<1> -> <2> -> <5> -> <4> -> <3>
...
<1> -> <5> -> <3> -> <4> -> <2>
<3> -> <2> -> <1> -> <4> -> <5>
...
<5> -> <4> -> <3> -> <3> -> <1>

Si cada orden de prueba puede darme resultados diferentes, cómo escribir un bucle para filtrar automáticamente todas las combinaciones ?

Lo que puedo pensar es usar otro método que genere el orden de prueba como el siguiente:

int[][] getTestOrder(int criteriaCount)
{
 ...
}

So if the criteriaCount is 2, it will return : {{1,2},{2,1}}
If the criteriaCount is 3, it will return : {{1,2,3},{1,3,2},{2,1,3},{2,3,1},{3,1,2},{3,2,1}}
...

Pero entonces, ¿cómo implementarlo de manera más eficiente con el mecanismo de filtrado en expresiones concisas que viene con Java 8 ?

Author: Frank, 2014-04-03

2 answers

Interesante problema. Hay varias cosas pasando aquí. Sin duda esto podría resolverse en menos de media página de Haskell o Lisp, pero esto es Java, así que aquí vamos....

Un problema es que tenemos un número variable de filtros, mientras que la mayoría de los ejemplos que se han mostrado ilustran tuberías fijas.

Otro problema es que algunos de los "filtros" de la OP son sensibles al contexto, como "top 50% by a certain order". Esto no se puede hacer con un simple filter(predicate) construir en una corriente.

La clave es darse cuenta de que, mientras que las lambdas permiten que las funciones se pasen como argumentos (con buenos resultados), también significa que pueden almacenarse en estructuras de datos y que se pueden realizar cálculos sobre ellas. El cálculo más común es tomar múltiples funciones y componerlas.

Supongamos que los valores que se operan son instancias de Widget, que es un POJO que tiene algunos getters obvios:

class Widget {
    String name() { ... }
    int length() { ... }
    double weight() { ... }

    // constructors, fields, toString(), etc.
}

Comencemos con el primer número y averiguar cómo operar con un número variable de predicados simples. Podemos crear una lista de predicados como este:

List<Predicate<Widget>> allPredicates = Arrays.asList(
    w -> w.length() >= 10,
    w -> w.weight() > 40.0,
    w -> w.name().compareTo("c") > 0);

Dada esta lista, podemos permutarlos (probablemente no sean útiles, ya que son independientes del orden) o seleccionar cualquier subconjunto que queramos. Digamos que solo queremos aplicarlos todos. ¿Cómo aplicamos un número variable de predicados a un flujo? Hay un método Predicate.and() que tomará dos predicados y los combinará usando y lógicos, devolviendo un solo predicado predicado. Así que podríamos tomar el primer predicado y escribir un bucle que lo combina con los predicados sucesivos para construir un solo predicado que es un compuesto y de todos ellos:

Predicate<Widget> compositePredicate = allPredicates.get(0);
for (int i = 1; i < allPredicates.size(); i++) {
    compositePredicate = compositePredicate.and(allPredicates.get(i));
}

Esto funciona, pero falla si la lista está vacía, y como estamos haciendo programación funcional ahora, mutar una variable en un bucle es declassé. Pero he aquí! ¡Esto es una reducción! Podemos reducir todos los predicados sobre el operador y obtener un solo predicado compuesto, como esto:

Predicate<Widget> compositePredicate =
    allPredicates.stream()
                 .reduce(w -> true, Predicate::and);

(Crédito: Aprendí esta técnica de @venkat_s. Si alguna vez tienes la oportunidad, ve a verlo hablar en una conferencia. Es bueno.)

Tenga en cuenta el uso de w -> true como el valor de identidad de la reducción. (Esto también podría usarse como el valor inicial de compositePredicate para el bucle, lo que fijaría el caso de lista de longitud cero.)

Ahora que tenemos nuestro predicado compuesto, podemos escribir una tubería corta que simplemente aplica el predicado compuesto a la widgets:

widgetList.stream()
          .filter(compositePredicate)
          .forEach(System.out::println);

Filtros sensibles al contexto

Ahora consideremos lo que me referí como un filtro "sensible al contexto", que está representado por el ejemplo como "top 50% en un cierto orden", digamos el top 50% de widgets por peso. "Context sensitive" no es el mejor término para esto, pero es lo que tengo en este momento, y es algo descriptivo ya que es relativo al número de elementos en la secuencia hasta este punto.

¿Cómo implementaríamos algo como esto usando corrientes? A menos que alguien venga con algo realmente inteligente, creo que tenemos que recoger los elementos en algún lugar primero (por ejemplo, en una lista) antes de que podamos emitir el primer elemento a la salida. Es algo así como sorted() en una canalización que no puede decir cuál es el primer elemento a la salida hasta que ha leído cada elemento de entrada y los ha ordenado.

El enfoque sencillo para encontrar el 50% superior de widgets por peso, utilizando flujos, se vería algo como esto:

List<Widget> temp =
    list.stream()
        .sorted(comparing(Widget::weight).reversed())
        .collect(toList());
temp.stream()
    .limit((long)(temp.size() * 0.5))
    .forEach(System.out::println);

Esto no es complicado, pero es un poco engorroso ya que tenemos que reunir los elementos en una lista y asignarlos a una variable, para poder usar el tamaño de la lista en el cálculo del 50%.

Esto es limitante, sin embargo, ya que es una representación "estática" de este tipo de filtrado. ¿Cómo encadenaríamos esto en un flujo con un número variable de elementos (otros filtros o criterios) como hicimos con los predicados?

Una observación importante es que este el código hace su trabajo real entre el consumo de una corriente y la emisión de una corriente. Sucede que tiene un colector en el medio, pero si encadenas una corriente a su parte delantera y encadenas cosas de su parte posterior, nadie se da cuenta. De hecho, las operaciones de canalización de flujo estándar como map y filter toman un flujo como entrada y emiten un flujo como salida. Así que podemos escribir una función como esta nosotros mismos:

Stream<Widget> top50PercentByWeight(Stream<Widget> stream) {
    List<Widget> temp =
        stream.sorted(comparing(Widget::weight).reversed())
              .collect(toList());
    return temp.stream()
               .limit((long)(temp.size() * 0.5));
}

Un ejemplo similar podría ser encontrar los tres más cortos widgets:

Stream<Widget> shortestThree(Stream<Widget> stream) {
    return stream.sorted(comparing(Widget::length))
                 .limit(3);
}

Ahora podemos escribir algo que combine estos filtros con estado con operaciones de flujo ordinarias:

shortestThree(
    top50PercentByWeight(
        widgetList.stream()
                  .filter(w -> w.length() >= 10)))
.forEach(System.out::println);

Esto funciona, pero es un poco pésimo porque dice "de adentro hacia afuera" y al revés. La fuente de flujo es widgetList que se transmite y se filtra a través de un predicado ordinario. Ahora, yendo hacia atrás, se aplica el filtro superior del 50%, luego se aplica el filtro más corto de tres, y finalmente se aplica la operación de flujo forEach al final. Esto funciona, pero es bastante confuso para leer. Y sigue siendo estático. Lo que realmente queremos es tener una manera de poner estos nuevos filtros dentro de una estructura de datos que podamos manipular, por ejemplo, para ejecutar todas las permutaciones, como en la pregunta original.

Una idea clave en este punto es que estos nuevos tipos de filtros son realmente solo funciones, y tenemos tipos de interfaces funcionales en Java que nos permiten representar funciones como objetos, manipularlas, almacenarlas en estructuras de datos, componerlas, etc. El el tipo de interfaz funcional que toma un argumento de algún tipo y devuelve un valor del mismo tipo es UnaryOperator. El argumento y tipo de retorno en este caso es Stream<Widget>. Si tuviéramos que tomar referencias de métodos como this::shortestThree o this::top50PercentByWeight, los tipos de los objetos resultantes serían

UnaryOperator<Stream<Widget>>

Si tuviéramos que poner estos en una lista, el tipo de esa lista sería{[37]]}

List<UnaryOperator<Stream<Widget>>>

Ugh! Tres niveles de genéricos anidados es demasiado para mí. (Pero Aleksey Shipilev una vez me mostró algún código que utiliza cuatro niveles de genéricos anidados.) La solución para demasiados genéricos es definir nuestro propio tipo. Llamemos Criterio a una de nuestras cosas nuevas. Resulta que hay poco valor que ganar haciendo que nuestro nuevo tipo de interfaz funcional esté relacionado con UnaryOperator, por lo que nuestra definición puede ser simplemente:

@FunctionalInterface
public interface Criterion {
    Stream<Widget> apply(Stream<Widget> s);
}

Ahora podemos crear una lista de criterios como este:

List<Criterion> criteria = Arrays.asList(
    this::shortestThree,
    this::lengthGreaterThan20
);

(Vamos a averiguar cómo utilizar esta lista a continuación.) Este es un paso adelante, ya que ahora podemos manipular el lista dinámicamente, pero sigue siendo algo limitante. Primero, no se puede combinar con predicados ordinarios. En segundo lugar, hay muchos valores codificados aquí, como los tres más cortos: ¿qué tal dos o cuatro? ¿Qué tal un criterio diferente a la longitud? Lo que realmente queremos es una función que cree estos objetos de Criterio para nosotros. Esto es fácil con lambdas.

Esto crea un criterio que selecciona los widgets top N, dado un comparador:

Criterion topN(Comparator<Widget> cmp, long n) {
    return stream -> stream.sorted(cmp).limit(n);
}

Esto crea un criterio que selecciona el porcentaje p superior de widgets, dado un comparador:

Criterion topPercent(Comparator<Widget> cmp, double pct) {
    return stream -> {
        List<Widget> temp =
            stream.sorted(cmp).collect(toList());
        return temp.stream()
                   .limit((long)(temp.size() * pct));
    };
}

Y esto crea un criterio a partir de un predicado ordinario:

Criterion fromPredicate(Predicate<Widget> pred) {
    return stream -> stream.filter(pred);
}

Ahora tenemos una forma muy flexible de crear criterios y ponerlos en una lista, donde pueden ser subconjuntos o permutados o lo que sea:{[37]]}

List<Criterion> criteria = Arrays.asList(
    fromPredicate(w -> w.length() > 10),                    // longer than 10
    topN(comparing(Widget::length), 4L),                    // longest 4
    topPercent(comparing(Widget::weight).reversed(), 0.50)  // heaviest 50%
);

Una vez que tenemos una lista de objetos Criterion, necesitamos encontrar una manera de aplicarlos todos. Una vez más, podemos usar nuestro amigo reduce para combinar todos ellos en un solo Criterio objeto:

Criterion allCriteria =
    criteria.stream()
            .reduce(c -> c, (c1, c2) -> (s -> c2.apply(c1.apply(s))));

La función de identidad c -> c es clara, pero el segundo arg es un poco complicado. Dada una secuencia s primero aplicamos el Criterio c1, luego el Criterio c2, y esto se envuelve en una lambda que toma dos objetos de Criterio c1 y c2 y devuelve una lambda que aplica la composición de c1 y c2 a una secuencia y devuelve la secuencia resultante.

Ahora que hemos compuesto todos los criterios, podemos aplicarlo a un flujo de widgets de la siguiente manera:

allCriteria.apply(widgetList.stream())
           .forEach(System.out::println);

Esto sigue siendo un un poco al revés, pero está bastante bien controlado. Lo más importante es que aborda la pregunta original, que es cómo combinar los criterios dinámicamente. Una vez que los objetos de Criterio están en una estructura de datos, pueden ser seleccionados, subconjuntos, permutados o lo que sea necesario, y todos pueden combinarse en un solo criterio y aplicarse a un flujo utilizando las técnicas anteriores.

Los gurús de la programación funcional probablemente están diciendo "Él simplemente reinventó ... !"lo cual es probablemente cierto. Estoy seguro esto probablemente ya se ha inventado en algún lugar, pero es nuevo para Java, porque antes de lambda, simplemente no era factible escribir código Java que utiliza estas técnicas.

Actualización 2014-04-07

He limpiado y publicado el código de ejemplo completo en un gist.

 63
Author: Stuart Marks,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2017-05-23 11:54:28

Podríamos añadir un contador con un mapa para saber cuántos elementos tenemos después de los filtros. He creado una clase helper que tiene un método que cuenta y devuelve el mismo objeto pasado:

class DoNothingButCount<T> {
    AtomicInteger i;
    public DoNothingButCount() {
        i = new AtomicInteger(0);
    }
    public T pass(T p) {
        i.incrementAndGet();
        return p;
    }
}

public void runDemo() {
    List<Person>persons = create(100);
    DoNothingButCount<Person> counter = new DoNothingButCount<>();

    persons.stream().filter(u -> u.size > 12).filter(u -> u.weitght > 12).
            map((p) -> counter.pass(p)).
            sorted((p1, p2) -> p1.age - p2.age).
            collect(Collectors.toList()).stream().
            limit((int) (counter.i.intValue() * 0.5)).
            sorted((p1, p2) -> p2.length - p1.length).
            limit((int) (counter.i.intValue() * 0.5 * 0.2)).forEach((p) -> System.out.println(p));
}

Tuve que convertir la secuencia a lista y volver a la secuencia en el medio porque el límite usaría el conteo inicial de lo contrario. Es todo un "hackish" pero es todo lo que podía pensar.

Podría hacerlo un poco diferente usando una función para mi clase asignada:

class DoNothingButCount<T > implements Function<T, T> {
    AtomicInteger i;
    public DoNothingButCount() {
        i = new AtomicInteger(0);
    }
    public T apply(T p) {
        i.incrementAndGet();
        return p;
    }
}

La única cosa cambiará en la corriente es:

            map((p) -> counter.pass(p)).

Se convertirá en:

            map(counter).

Mi clase de prueba completa incluyendo los dos ejemplos:

import java.util.*;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.function.Function;
import java.util.stream.Collectors;

public class Demo2 {
    Random r = new Random();
    class Person {
        public int size, weitght,length, age;
        public Person(int s, int w, int l, int a){
            this.size = s;
            this.weitght = w;
            this.length = l;
            this.age = a;
        }
        public String toString() {
            return "P: "+this.size+", "+this.weitght+", "+this.length+", "+this.age+".";
        }
    }

    public List<Person>create(int size) {
        List<Person>persons = new ArrayList<>();
        while(persons.size()<size) {
            persons.add(new Person(r.nextInt(10)+10, r.nextInt(10)+10, r.nextInt(10)+10,r.nextInt(20)+14));
        }
        return persons;
    }

    class DoNothingButCount<T> {
        AtomicInteger i;
        public DoNothingButCount() {
            i = new AtomicInteger(0);
        }
        public T pass(T p) {
            i.incrementAndGet();
            return p;
        }
    }

    class PDoNothingButCount<T > implements Function<T, T> {
        AtomicInteger i;
        public PDoNothingButCount() {
            i = new AtomicInteger(0);
        }
        public T apply(T p) {
            i.incrementAndGet();
            return p;
        }
    }

    public void runDemo() {
        List<Person>persons = create(100);
        PDoNothingButCount<Person> counter = new PDoNothingButCount<>();

        persons.stream().filter(u -> u.size > 12).filter(u -> u.weitght > 12).
                map(counter).
                sorted((p1, p2) -> p1.age - p2.age).
                collect(Collectors.toList()).stream().
                limit((int) (counter.i.intValue() * 0.5)).
                sorted((p1, p2) -> p2.length - p1.length).
                limit((int) (counter.i.intValue() * 0.5 * 0.2)).forEach((p) -> System.out.println(p));
    }

    public void runDemo2() {
        List<Person>persons = create(100);
        DoNothingButCount<Person> counter = new DoNothingButCount<>();

        persons.stream().filter(u -> u.size > 12).filter(u -> u.weitght > 12).
                map((p) -> counter.pass(p)).
                sorted((p1, p2) -> p1.age - p2.age).
                collect(Collectors.toList()).stream().
                limit((int) (counter.i.intValue() * 0.5)).
                sorted((p1, p2) -> p2.length - p1.length).
                limit((int) (counter.i.intValue() * 0.5 * 0.2)).forEach((p) -> System.out.println(p));
    }

    public static void main(String str[]) {
        Demo2 demo = new Demo2();
        System.out.println("Demo 2:");
        demo.runDemo2();
        System.out.println("Demo 1:");
        demo.runDemo();

    }
}
 2
Author: Raul Guiu,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2014-04-04 09:49:00