example collection java java-8 java-stream

collection - Java 8 Stream API: seleccione la clave más baja después de agrupar por



map function java 8 (7)

Aquí hay una alternativa con un stream y un reductor personalizado. La idea es primero ordenar y luego recopilar solo los 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 usar collect 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 );

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 ''s que todos tienen la variableCount más baja.

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 transformarlo en lo que quiero.

Gracias por adelantado


Aquí hay una solución que:

  1. Sólo transmite la lista una vez.
  2. No construye un mapa u otra estructura que contenga todos los elementos de entrada (a menos que los recuentos de variables sean todos iguales), solo se mantienen los mínimos actuales.
  3. Es O (n) tiempo, O (n) espacio. Es completamente posible que todos los Foo tengan el mismo número de variables, en cuyo caso esta solución almacenaría todos los elementos como otras soluciones. Pero en la práctica, con valores diferentes y variados y mayor cardinalidad, es probable que el número de elementos en la lista sea mucho menor.

Editado

He mejorado mi solución de acuerdo a las sugerencias de los comentarios.

Implementé un objeto acumulador, que suministra funciones al Collector para esto.

/** * Accumulator object to hold the current min * and the list of Foos that are the min. */ class Accumulator { Integer min; List<Foo> foos; Accumulator() { min = Integer.MAX_VALUE; foos = new ArrayList<>(); } void accumulate(Foo f) { if (f.getVariableCount() != null) { if (f.getVariableCount() < min) { min = f.getVariableCount(); foos.clear(); foos.add(f); } else if (f.getVariableCount() == min) { foos.add(f); } } } Accumulator combine(Accumulator other) { if (min < other.min) { return this; } else if (min > other.min) { return other; } else { foos.addAll(other.foos); return this; } } List<Foo> getFoos() { return foos; } }

Luego, todo lo que tenemos que hacer es collect , haciendo referencia a los métodos del acumulador para sus funciones.

List<Foo> mins = foos.stream().collect(Collector.of( Accumulator::new, Accumulator::accumulate, Accumulator::combine, Accumulator::getFoos ) );

Probando con

List<Foo> foos = Arrays.asList(new Foo(3), new Foo(3), new Foo(2), new Foo(1), new Foo(1), new Foo(4));

La salida es (con una toString adecuada definida en Foo ):

[Foo{1}, Foo{1}]


Para evitar crear el mapa, puedes usar dos flujos:

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


Para evitar crear todo el mapa y también evitar la transmisión doble, copié un recopilador personalizado desde aquí https://.com/a/30497254/1264846 y lo modifiqué para que funcione con min en lugar de max. Ni siquiera sabía que los coleccionistas personalizados fueran posibles, así que agradezco a @lexicore por indicarme 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); }


Podría usar la collect inteligente 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 número de variables igual al del primer elemento de la lista.

Un ejemplo completo de trabajo 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 la variableCount mínima en una secuencia y usar ese filtro interno de otra corriente.

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 utilizar dentro del predicado. Incluso si está utilizando el mapa para agrupar los valores.

¡Aclamaciones!


Puede usar un mapa ordenado para agrupar y luego obtener la primera entrada. Algo a lo largo de las líneas:

Collectors.groupingBy( Foo::getVariableCount, TreeMap::new, Collectors.toList()) .firstEntry() .getValue()


Si está bien la transmisión (iteración) 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()); }