wildcards parameter method generic español java generics java-8 java-stream

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 y andThen(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: el Consumer<CAP#1> no puede convertirse a Consumer<? super CAP#2> Consumer<? super CAP#2>
donde T es una variable de tipo:
T extends Object declarado en el método <T>combine(Stream<? extends Consumer<? super T>>)
donde CAP#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);