ejemplo - java stream filter multiple conditions
¿Cómo forzar max() para devolver TODOS los valores máximos en una secuencia Java? (5)
Creo que el OP está utilizando un Comparador para dividir la entrada en clases de equivalencia, y el resultado deseado es una lista de miembros de la clase de equivalencia que es el máximo según ese Comparador.
Desafortunadamente, el uso de valores
int
como un problema de muestra es un ejemplo terrible.
Todos los valores
int
iguales son fungibles, por lo que no existe la noción de preservar el orden de los valores equivalentes.
Quizás un mejor ejemplo es usar longitudes de cadena, donde el resultado deseado es devolver una lista de cadenas de una entrada que tengan la longitud más larga dentro de esa entrada.
No conozco ninguna forma de hacer esto sin almacenar al menos resultados parciales en una colección.
Dada una colección de entrada, digamos
List<String> list = ... ;
es lo suficientemente simple como para hacer esto en dos pasadas, la primera para obtener la longitud más larga y la segunda para filtrar las cadenas que tienen esa longitud:
int longest = list.stream()
.mapToInt(String::length)
.max()
.orElse(-1);
List<String> result = list.stream()
.filter(s -> s.length() == longest)
.collect(toList());
Si la entrada es una secuencia, que no puede atravesarse más de una vez , es posible calcular el resultado en una sola pasada utilizando un recopilador. Escribir un coleccionista de este tipo no es difícil, pero es un poco tedioso ya que hay varios casos que deben manejarse. Una función auxiliar que genera dicho recopilador, dado un comparador, es la siguiente:
static <T> Collector<T,?,List<T>> maxList(Comparator<? super T> comp) {
return Collector.of(
ArrayList::new,
(list, t) -> {
int c;
if (list.isEmpty() || (c = comp.compare(t, list.get(0))) == 0) {
list.add(t);
} else if (c > 0) {
list.clear();
list.add(t);
}
},
(list1, list2) -> {
if (list1.isEmpty()) {
return list2;
}
if (list2.isEmpty()) {
return list1;
}
int r = comp.compare(list1.get(0), list2.get(0));
if (r < 0) {
return list2;
} else if (r > 0) {
return list1;
} else {
list1.addAll(list2);
return list1;
}
});
}
Esto almacena resultados intermedios en una
ArrayList
.
Lo invariable es que todos los elementos dentro de cualquier lista son equivalentes en términos del Comparador.
Al agregar un elemento, si es menor que los elementos en la lista, se ignora;
si es igual, se agrega;
y si es mayor, la lista se vacía y se agrega el nuevo elemento.
La fusión tampoco es demasiado difícil: se devuelve la lista con los elementos más grandes, pero si sus elementos son iguales, se añaden las listas.
Dado un flujo de entrada, esto es bastante fácil de usar:
Stream<String> input = ... ;
List<String> result = input.collect(maxList(comparing(String::length)));
He probado un poco la función max () en Java 8 lambdas y streams, y parece que en caso de que max () se ejecute, incluso si más de un objeto se compara con 0, devuelve un elemento arbitrario dentro de los candidatos vinculados sin para mayor consideracion.
¿Existe algún truco o función evidente para un comportamiento esperado máximo de modo que se devuelvan todos los valores máximos? No veo nada en la API, pero estoy seguro de que debe existir algo mejor que compararlo manualmente.
Por ejemplo:
//myComparator is an IntegerComparator
Stream.of(1,3,5,3,2,3,5).max(myComparator).forEach(System.out::println);
//Would print 5,5 in any order.
Implementé una solución de recopilador más genérica con un recopilador descendente personalizado. Probablemente algunos lectores lo encuentren útil:
public static <T, A, D> Collector<T, ?, D> maxAll(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);
}
Entonces, por defecto, se puede recopilar para enumerar:
public static <T> Collector<T, ?, List<T>> maxAll(Comparator<? super T> comparator) {
return maxAll(comparator, Collectors.toList());
}
Pero también puede usar otros colectores posteriores:
public static String joinLongestStrings(Collection<String> input) {
return input.stream().collect(
maxAll(Comparator.comparingInt(String::length), Collectors.joining(","))));
}
No estoy realmente seguro de si estás tratando de
- (a) encuentre el número de ocurrencias del artículo máximo, o
-
(b) Encuentre todos los valores máximos en el caso de un
Comparator
que no sea consistente conequals
.
Un ejemplo de (a) sería
[1, 5, 4, 5, 1, 1] -> [5, 5]
.
Un ejemplo de (b) sería:
Stream.of("Bar", "FOO", "foo", "BAR", "Foo")
.max((s, t) -> s.toLowerCase().compareTo(t.toLowerCase()));
que desea dar
[Foo, foo, Foo]
, en lugar de solo
FOO
u
Optional[FOO]
.
En ambos casos, hay formas inteligentes de hacerlo en una sola pasada.
Pero estos enfoques tienen un valor dudoso porque necesitaría realizar un seguimiento de la información innecesaria en el camino.
Por ejemplo, si comienza con
[2, 0, 2, 2, 1, 6, 2]
, solo cuando llegue a
6
se dará cuenta de que no es necesario hacer un seguimiento de todos los
2
s.
Creo que el mejor enfoque es el obvio;
use
max
, y luego repita los elementos nuevamente poniendo todos los lazos en una colección de su elección.
Esto funcionará tanto para (a) como para (b).
Si entendí bien, desea la frecuencia del valor
max
en la secuencia.
Una forma de lograrlo sería almacenar los resultados en un
TreeMap<Integer, List<Integer>
cuando recopile elementos del Stream.
Luego, toma la última clave (o la primera según el comparador que proporcione) para obtener el valor que contendrá la lista de valores máximos.
List<Integer> maxValues = st.collect(toMap(i -> i,
Arrays::asList,
(l1, l2) -> Stream.concat(l1.stream(), l2.stream()).collect(toList()),
TreeMap::new))
.lastEntry()
.getValue();
Recogerlo de la
Stream(4, 5, -2, 5, 5)
le dará una
List [5, 5, 5]
.
Otro enfoque con el mismo espíritu sería utilizar un grupo por operación combinado con el colector
counting()
:
Entry<Integer, Long> maxValues = st.collect(groupingBy(i -> i,
TreeMap::new,
counting())).lastEntry(); //5=3 -> 5 appears 3 times
Básicamente, primero obtienes un
Map<Integer, List<Integer>>
.
Luego, el recopilador de
counting()
descendente
counting()
devolverá el número de elementos en cada lista asignada por su clave, lo que dará como resultado un Mapa.
Desde allí tomas la entrada máxima.
Los primeros enfoques requieren almacenar todos los elementos de la secuencia.
El segundo es mejor (ver el comentario de Holger) ya que la
List
intermedia no está construida.
En ambos abordados, el resultado se calcula en una sola pasada.
Si obtiene la fuente de una colección, es posible que desee usar
Collections.max
una vez para encontrar el valor máximo seguido de
Collections.frequency
para encontrar cuántas veces aparece este valor.
Requiere dos pases pero usa menos memoria ya que no tiene que construir la estructura de datos.
El flujo equivalente sería
coll.stream().max(...).get(...)
seguido de
coll.stream().filter(...).count()
.
TreeMap
por valor y almacenaría los valores en un
TreeMap
para ordenar mis valores, luego obtendría el valor máximo obteniendo la última entrada como la siguiente:
Stream.of(1, 3, 5, 3, 2, 3, 5)
.collect(groupingBy(Function.identity(), TreeMap::new, toList()))
.lastEntry()
.getValue()
.forEach(System.out::println);
Salida:
5
5