web-dev-qa-db-fra.com

Java 8 génériques: réduire un flux de consommateurs à un seul consommateur

Comment puis-je écrire une méthode pour combiner un Stream de Consumers en un seul Consumer en utilisant Consumer.andThen(Consumer)?

Ma première version était:

<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 */ };
}

Cette version se compile avec JavaC et Eclipse. Mais c'est trop spécifique: le Stream ne peut pas être un Stream<SpecialConsumer>, Et si les Consumers ne sont pas exactement de type T mais un super type, il ne peut pas être utilisé:

Stream<? extends Consumer<? super Foo>> consumers = ... ;
combine(consumers);

Cela ne se compilera pas, à juste titre. La version améliorée serait:

<T> Consumer<T> combine(Stream<? extends Consumer<? super T>> consumers) {
    return consumers
            .filter(Objects::nonNull)
            .reduce(Consumer::andThen)
            .orElse(noOpConsumer());
}

Mais ni Eclipse ni JavaC ne compilent cela:
Eclipse (4.7.3a):

Le type Consumer ne définit pas andThen(capture#7-of ? extends Consumer<? super T>, capture#7-of ? extends Consumer<? super T>) qui s'applique ici

JavaC (1.8.0172):

erreur: types incompatibles: référence de méthode non valide
.reduce(Consumer::andThen)
types incompatibles: Consumer<CAP#1> ne peut pas être converti en Consumer<? super CAP#2>
T est une variable de type:
T extends Object Déclaré dans la méthode <T>combine(Stream<? extends Consumer<? super T>>)
CAP#1, CAP#2 sont de nouvelles variables de type:
CAP#1 extends Object super: T from capture of ? super T
CAP#2 extends Object super: T from capture of ? super T

Mais cela devrait fonctionner: chaque sous-classe de Consumer peut également être utilisée en tant que Consumer. Et chaque consommateur d'un super-type de X peut également consommer des X. J'ai essayé d'ajouter des paramètres de type à chaque ligne de la version de flux, mais cela n'aidera pas. Mais si je l'écris avec une boucle traditionnelle, il compile:

<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;
}

(Le filtrage des valeurs nulles est omis pour des raisons de concision.)

Par conséquent, ma question est la suivante: comment convaincre JavaC et Eclipse que mon code est correct? Ou, si elle n'est pas correcte: pourquoi la version en boucle est-elle correcte mais pas la version Stream?

31
user194860

Vous utilisez une version à un argument Stream.reduce(accumulator) qui a la signature suivante:

Optional<T> reduce(BinaryOperator<T> accumulator);

Le BinaryOperator<T> accumulator Ne peut accepter que des éléments de type T, mais vous avez:

<? extends Consumer<? super T>>

Je vous propose d'utiliser une version à trois arguments de la méthode Stream.reduce(...) à la place:

<U> U reduce(U identity,
             BiFunction<U, ? super T, U> accumulator
             BinaryOperator<U> combiner);

Le BiFunction<U, ? super T, U> accumulator Peut accepter des paramètres de deux types différents, a une limite moins restrictive et est plus adapté à votre situation. Une solution possible pourrait être:

<T> Consumer<T> combine(Stream<? extends Consumer<? super T>> consumers) {
    return consumers.filter(Objects::nonNull)
                    .reduce(t -> {}, Consumer::andThen, Consumer::andThen);
}

Le troisième argument BinaryOperator<U> combiner Est appelé uniquement dans les flux parallèles, mais de toute façon il serait sage d'en fournir une implémentation correcte.

De plus, pour une meilleure compréhension, on pourrait représenter le code ci-dessus comme suit:

<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);
}

Vous pouvez maintenant écrire:

Stream<? extends Consumer<? super Foo>> consumers = Stream.of();
combine(consumers);
27
Oleksandr Pyrohov

Vous avez oublié une petite chose dans la définition de votre méthode. C'est actuellement:

<T> Consumer<T> combine(Stream<? extends Consumer<? super T>> consumers) {}

Mais vous reprenez Consumer<? super T>. Donc, en changeant le type de retour, cela fonctionne presque. Vous acceptez maintenant un argument consumers de type Stream<? extends Consumer<? super T>>. Actuellement, cela ne fonctionne pas, car vous travaillez avec différentes sous-classes et implémentations de Consumer<? super T> (en raison du caractère générique supérieur extends). Vous pouvez surmonter cela en lançant chaque ? extends Consumer<? super T> dans votre Stream vers un simple Consumer<? super T>. Comme le suivant:

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

Cela devrait maintenant fonctionner

1
Lino

Si vous avez beaucoup de consommateurs, l'application de Consumer.andThen() créera une énorme arborescence de wrappers de consommateurs qui sera traitée de manière récursive pour appeler chaque consommateur d'origine.

Il pourrait donc être plus efficace de simplement construire une liste des consommateurs et de créer un simple consommateur qui les répète:

<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));
}

Alternativement, si vous pouvez garantir que le consommateur résultant ne sera appelé qu'une seule fois et que le Stream sera toujours valide à ce moment, vous pouvez simplement itérer directement sur le flux:

return t -> consumers
        .filter(Objects::nonNull)
        .forEach(c -> c.accept(t));
0
Didier L