parameter - wildcard en java
Java 8 Generics: reducción de un flujo de consumidores a un solo consumidor (3)
¿Cómo puedo escribir un método para combinar un Stream
de Consumers
en un solo Consumer
usando Consumer.andThen(Consumer)
?
Mi primera versión fue:
<T> Consumer<T> combine(Stream<Consumer<T>> consumers) {
return consumers
.filter(Objects::nonNull)
.reduce(Consumer::andThen)
.orElse(noOpConsumer());
}
<T> Consumer<T> noOpConsumer() {
return value -> { /* do nothing */ };
}
Esta versión compila con JavaC y Eclipse. Pero es demasiado específico: el Stream
no puede ser un Stream<SpecialConsumer>
, y si los Consumers
no son exactamente del tipo T
sino de un super tipo, no se puede usar:
Stream<? extends Consumer<? super Foo>> consumers = ... ;
combine(consumers);
Eso no compilará, con razón. La versión mejorada sería:
<T> Consumer<T> combine(Stream<? extends Consumer<? super T>> consumers) {
return consumers
.filter(Objects::nonNull)
.reduce(Consumer::andThen)
.orElse(noOpConsumer());
}
Pero ni Eclipse ni JavaC compilan eso:
Eclipse (4.7.3a):
El tipo
Consumer
no define yandThen(capture#7-of ? extends Consumer<? super T>, capture#7-of ? extends Consumer<? super T>)
que es aplicable aquí
JavaC (1.8.0172):
error: tipos incompatibles: referencia de método inválido
.reduce(Consumer::andThen)
tipos incompatibles: elConsumer<CAP#1>
no puede convertirse aConsumer<? super CAP#2>
Consumer<? super CAP#2>
dondeT
es una variable de tipo:
T extends Object
declarado en el método<T>combine(Stream<? extends Consumer<? super T>>)
dondeCAP#1
,CAP#2
son nuevas variables de tipo:
CAP#1 extends Object super: T from capture of ? super T
CAP#2 extends Object super: T from capture of ? super T
Pero debería funcionar: cada subclase de Consumidor puede usarse también como Consumidor. Y cada consumidor de un súper tipo de X también puede consumir X. Intenté agregar parámetros de tipo a cada línea de la versión de secuencia, pero eso no ayudará. Pero si lo escribo con un ciclo tradicional, compila:
<T> Consumer<T> combine(Collection<? extends Consumer<? super T>> consumers) {
Consumer<T> result = noOpConsumer()
for (Consumer<? super T> consumer : consumers) {
result = result.andThen(consumer);
}
return result;
}
(El filtrado de los valores nulos queda fuera de la concisión).
Por lo tanto, mi pregunta es: ¿cómo puedo convencer a JavaC y Eclipse de que mi Código es correcto? O, si no es correcto: ¿por qué es correcta la versión de bucle pero no la versión de Stream
?
Has olvidado una pequeña cosa en tu definición de método. Actualmente es:
<T> Consumer<T> combine(Stream<? extends Consumer<? super T>> consumers) {}
¿Pero estás retomando al Consumer<? super T>
Consumer<? super T>
. Así que al cambiar el tipo de devolución casi funciona. Ahora acepta un argumento consumers
de tipo Stream<? extends Consumer<? super T>>
Stream<? extends Consumer<? super T>>
Stream<? extends Consumer<? super T>>
. Actualmente no funciona, porque estás trabajando con posibles subclases diferentes e implementaciones de Consumer<? super T>
Consumer<? super T>
(debido a que el comodín de límite superior se extends
). Puedes superar esto, lanzando cada ? extends Consumer<? super T>
? extends Consumer<? super T>
? extends Consumer<? super T>
en tu Stream
a un simple Consumer<? super T>
Consumer<? super T>
. Como el siguiente:
<T> Consumer<? super T> combine(Stream<? extends Consumer<? super T>> consumers) {
return consumers
.filter(Objects::nonNull)
.map(c -> (Consumer<? super T>) c)
.reduce(Consumer::andThen)
.orElse(noOpConsumer());
}
Esto debería funcionar ahora
Si tiene muchos consumidores, la aplicación Consumer.andThen()
creará un enorme árbol de envoltorios para el consumidor que se procesa de forma recursiva para llamar a cada consumidor original.
Por lo tanto, podría ser más eficiente simplemente construir una lista de los consumidores y crear un consumidor simple que se repita sobre ellos:
<T> Consumer<T> combine(Stream<? extends Consumer<? super T>> consumers) {
List<Consumer<? super T>> consumerList = consumers
.filter(Objects::nonNull)
.collect(Collectors.toList());
return t -> consumerList.forEach(c -> c.accept(t));
}
Alternativamente, si puede garantizar que solo se llamará al consumidor resultante una vez, y que la Stream
seguirá siendo válida en ese momento, simplemente puede iterar directamente sobre la transmisión:
return t -> consumers
.filter(Objects::nonNull)
.forEach(c -> c.accept(t));
Utiliza una versión Stream.reduce(accumulator)
un argumento que tiene la siguiente firma:
Optional<T> reduce(BinaryOperator<T> accumulator);
El BinaryOperator<T> accumulator
solo puede aceptar elementos de tipo T
, pero tiene:
<? extends Consumer<? super T>>
Le propongo que use una versión de tres argumentos del método Stream.reduce(...)
lugar:
<U> U reduce(U identity,
BiFunction<U, ? super T, U> accumulator
BinaryOperator<U> combiner);
El BiFunction<U, ? super T, U> accumulator
BiFunction<U, ? super T, U> accumulator
puede aceptar parámetros de dos tipos diferentes, tiene un límite menos restrictivo y es más adecuado para su situación. Una posible solución podría ser:
<T> Consumer<T> combine(Stream<? extends Consumer<? super T>> consumers) {
return consumers.filter(Objects::nonNull)
.reduce(t -> {}, Consumer::andThen, Consumer::andThen);
}
El tercer argumento del BinaryOperator<U> combiner
se llama solo en las secuencias paralelas, pero de todos modos sería prudente proporcionar una implementación correcta del mismo.
Además, para una mejor comprensión, uno podría representar el código anterior de la siguiente manera:
<T> Consumer<T> combine(Stream<? extends Consumer<? super T>> consumers) {
Consumer<T> identity = t -> {};
BiFunction<Consumer<T>, Consumer<? super T>, Consumer<T>> acc = Consumer::andThen;
BinaryOperator<Consumer<T>> combiner = Consumer::andThen;
return consumers.filter(Objects::nonNull)
.reduce(identity, acc, combiner);
}
Ahora puedes escribir:
Stream<? extends Consumer<? super Foo>> consumers = Stream.of();
combine(consumers);