Tengo un flujo de objetos Foo.

class Foo {
    private int variableCount;
    public Foo(int vars) {
        this.variableCount = vars; 
    }
    public Integer getVariableCount() { 
      return variableCount; 
    }
}

Quiero una lista de Foo que tengan el menor variableCount.

Por ejemplo,

new Foo(3), new Foo(3), new Foo(2), new Foo(1), new Foo(1)

Solo quiero que la transmisión devuelva los últimos 2 Foo s

He intentado hacer una recopilación con agrupación por

.collect(Collectors.groupingBy((Foo foo) -> {
                    return foo.getVariableCount();
})

Y eso devuelve un Map<Integer, List<Foo>> y no estoy seguro de cómo transformar eso en lo que quiero.

Gracias de antemano

17
James Kleeh 27 feb. 2018 a las 20:10

6 respuestas

SI está bien transmitir (iterar) dos veces:

private static List<Foo> mins(List<Foo> foos) {
    return foos.stream()
            .map(Foo::getVariableCount)
            .min(Comparator.naturalOrder())
            .map(x -> foos.stream()
                          .filter(y -> y.getVariableCount() == x)
                          .collect(Collectors.toList()))
            .orElse(Collections.emptyList());
}
6
Eugene 27 feb. 2018 a las 17:41

Aquí hay una alternativa con una corriente y un reductor personalizado. La idea es ordenar primero y luego recopilar solo elementos con el primer valor mínimo:

    List<Foo> newlist = list.stream()
    .sorted( Comparator.comparing(Foo::getVariableCount) )
    .reduce( new ArrayList<>(), 
         (l, f) -> { 
             if ( l.isEmpty() || l.get(0).getVariableCount() == f.getVariableCount() ) l.add(f); 
             return l;
         }, 
         (l1, l2) -> {
             l1.addAll(l2); 
             return l1;
         } 
    );

O el uso de recoger es aún más compacto:

    List<Foo> newlist = list.stream()
    .sorted( Comparator.comparing(Foo::getVariableCount) )
    .collect( ArrayList::new, 
         (l, f) -> if ( l.isEmpty() || l.get(0).getVariableCount() == f.getVariableCount() ) l.add(f),
         List::addAll
    );
1
shmosel 27 feb. 2018 a las 18:20

Para evitar crear el mapa, puede usar dos transmisiones:

  • el primero encuentra el valor mínimo.
  • el segundo filtra elementos con este valor.

Podría dar:

List<Foo> foos = ...;
int min = foos.stream()
              .mapToInt(Foo::getVariableCount)
              .min()
              .orElseThrow(RuntimeException::new); // technical error

List<Foo> minFoos = foos.stream()
    .filter(f -> f.getVariableCount() == min)
    .collect(Collectors.toList());
1
davidxxx 27 feb. 2018 a las 18:34

Puede usar collect sabiamente en la lista ordenada y en el acumulador agregar la lógica para agregar solo el primer elemento a la lista vacía o agregar cualquier otro Foo que tenga un recuento variable igual que el primer elemento de la lista.

Un ejemplo de trabajo completo a continuación:

import java.util.ArrayList;
import java.util.Arrays;
import java.util.Comparator;
import java.util.List;

class Foo {
    private int variableCount;

    public Foo(int vars) {
        this.variableCount = vars;
    }

    public Integer getVariableCount() {
        return variableCount;
    }

    public static void main(String[] args) {
        List<Foo> list = Arrays.asList(
                new Foo(2),
                new Foo(2),
                new Foo(3),
                new Foo(3),
                new Foo(1),
                new Foo(1)
        );

        System.out.println(list.stream()
                .sorted(Comparator.comparing(Foo::getVariableCount))
                .collect(() -> new ArrayList<Foo>(),
                        (ArrayList<Foo> arrayList, Foo e) -> {
                            if (arrayList.isEmpty()
                                    || arrayList.get(0).getVariableCount() == e.getVariableCount()) {
                                arrayList.add(e);
                            }
                        },
                        (ArrayList<Foo> foos, ArrayList<Foo> foo) -> foos.addAll(foo)
                )

        );
    }

    @Override
    public String toString() {
        return "Foo{" +
                "variableCount=" + variableCount +
                '}';
    }
}

Además, primero puede encontrar el mínimo variableCount en una secuencia y usar ese filtro interno de otra secuencia.

    list.sort(Comparator.comparing(Foo::getVariableCount));
    int min = list.get(0).getVariableCount();
    list.stream().filter(foo -> foo.getVariableCount() == min)
            .collect(Collectors.toList());

Creo que, en cualquier caso, se requiere una clasificación o una forma de encontrar el número mínimo que luego se puede usar dentro del predicado. Incluso si está utilizando el mapa para agrupar los valores.

¡Salud!

1
Vinay Prajapati 27 feb. 2018 a las 18:08

Para evitar crear el mapa completo y también evitar la transmisión dos veces, copié un recopilador personalizado desde aquí https://stackoverflow.com/a/30497254 / 1264846 y lo modificó para que funcione con min en lugar de max. Ni siquiera sabía que los coleccionistas personalizados eran posibles, así que agradezco a @lexicore por señalarme en esa dirección.

Esta es la función resultante minAll

public static <T, A, D> Collector<T, ?, D> minAll(Comparator<? super T> comparator,
                                                  Collector<? super T, A, D> downstream) {
    Supplier<A> downstreamSupplier = downstream.supplier();
    BiConsumer<A, ? super T> downstreamAccumulator = downstream.accumulator();
    BinaryOperator<A> downstreamCombiner = downstream.combiner();
    class Container {
        A acc;
        T obj;
        boolean hasAny;

        Container(A acc) {
            this.acc = acc;
        }
    }
    Supplier<Container> supplier = () -> new Container(downstreamSupplier.get());
    BiConsumer<Container, T> accumulator = (acc, t) -> {
        if(!acc.hasAny) {
            downstreamAccumulator.accept(acc.acc, t);
            acc.obj = t;
            acc.hasAny = true;
        } else {
            int cmp = comparator.compare(t, acc.obj);
            if (cmp < 0) {
                acc.acc = downstreamSupplier.get();
                acc.obj = t;
            }
            if (cmp <= 0)
                downstreamAccumulator.accept(acc.acc, t);
        }
    };
    BinaryOperator<Container> combiner = (acc1, acc2) -> {
        if (!acc2.hasAny) {
            return acc1;
        }
        if (!acc1.hasAny) {
            return acc2;
        }
        int cmp = comparator.compare(acc1.obj, acc2.obj);
        if (cmp < 0) {
            return acc1;
        }
        if (cmp > 0) {
            return acc2;
        }
        acc1.acc = downstreamCombiner.apply(acc1.acc, acc2.acc);
        return acc1;
    };
    Function<Container, D> finisher = acc -> downstream.finisher().apply(acc.acc);
    return Collector.of(supplier, accumulator, combiner, finisher);
}
1
James Kleeh 27 feb. 2018 a las 17:40

Puede usar un mapa ordenado para agrupar y luego obtener la primera entrada. Algo en la línea:

Collectors.groupingBy(
    Foo::getVariableCount,
    TreeMap::new,
    Collectors.toList())
.firstEntry()
.getValue()
14
lexicore 27 feb. 2018 a las 17:16