Utilisation de Java 8 en option avec Stream :: flatMap


240

Le nouveau cadre de flux Java 8 et ses amis créent un code java très concis, mais j'ai rencontré une situation apparemment simple qui est difficile à faire de manière concise.

Considérez une List<Thing> thingsméthode et Optional<Other> resolve(Thing thing). Je veux mapper les Things à Optional<Other>s et obtenir le premier Other. La solution évidente serait d'utiliser things.stream().flatMap(this::resolve).findFirst(), mais flatMapnécessite que vous renvoyiez un flux et qu'il Optionaln'ait pas de stream()méthode (ou s'agit-il d'un Collectionou fournissez une méthode pour le convertir ou l'afficher en tant que Collection).

Le mieux que je puisse trouver est le suivant:

things.stream()
    .map(this::resolve)
    .filter(Optional::isPresent)
    .map(Optional::get)
    .findFirst();

Mais cela semble terriblement long pour ce qui semble être un cas très courant. Quelqu'un a une meilleure idée?


Après avoir codé un peu avec votre exemple, je trouve en fait la version explicite plus lisible que celle concernant, si elle avait existé .flatMap(Optional::toStream), avec votre version vous voyez réellement ce qui se passe.
skiwi

19
@skiwi Eh bien, Optional.streamexiste dans JDK 9 maintenant ....
Stuart Marks

Je suis curieux de savoir où cela est documenté et quel a été le processus d'obtention. Il existe d'autres méthodes qui semblent vraiment exister, et je suis curieux de savoir où se déroulent les discussions sur les modifications de l'API.
Yona Appletree


10
Le plus drôle, c'est que JDK-8050820 fait référence à cette question dans sa description!
Didier L

Réponses:


265

Java 9

Optional.stream a été ajouté à JDK 9. Cela vous permet d'effectuer les opérations suivantes, sans avoir besoin d'aucune méthode d'assistance:

Optional<Other> result =
    things.stream()
          .map(this::resolve)
          .flatMap(Optional::stream)
          .findFirst();

Java 8

Oui, c'était un petit trou dans l'API, en ce sens qu'il est quelque peu gênant de transformer un Optional<T>en une longueur nulle ou un Stream<T>. Vous pouvez faire ceci:

Optional<Other> result =
    things.stream()
          .map(this::resolve)
          .flatMap(o -> o.isPresent() ? Stream.of(o.get()) : Stream.empty())
          .findFirst();

flatMapCependant, avoir l'opérateur ternaire à l'intérieur du est un peu lourd, il serait donc préférable d'écrire une petite fonction d'aide pour le faire:

/**
 * Turns an Optional<T> into a Stream<T> of length zero or one depending upon
 * whether a value is present.
 */
static <T> Stream<T> streamopt(Optional<T> opt) {
    if (opt.isPresent())
        return Stream.of(opt.get());
    else
        return Stream.empty();
}

Optional<Other> result =
    things.stream()
          .flatMap(t -> streamopt(resolve(t)))
          .findFirst();

Ici, j'ai souligné l'appel au resolve()lieu d'avoir une map()opération séparée , mais c'est une question de goût.


2
Je ne pense pas que l'API puisse changer jusqu'à Java 9 maintenant.
assylias

5
@Hypher Merci. La technique .filter (). Map () n'est pas trop mauvaise et évite les dépendances sur les méthodes d'assistance. «Ce serait bien s'il y avait un moyen plus concis. J'examinerai comment ajouter Optional.stream ().
Stuart marque le

43
Je préfère:static <T> Stream<T> streamopt(Optional<T> opt) { return opt.map(Stream::of).orElse(Stream.empty()); }
kubek2k

5
Je souhaite qu'ils ajoutent simplement une Optionalsurcharge à Stream#flatMap... de cette façon, vous pourriez simplement écrirestream().flatMap(this::resolve)
flocons

4
@flkes Oui, nous avons lancé cette idée, mais cela ne semble pas ajouter beaucoup de valeur maintenant (dans JDK 9) Optional.stream().
Stuart marque

69

J'ajoute cette deuxième réponse basée sur une modification proposée par l'utilisateur srborlongan à mon autre réponse . Je pense que la technique proposée était intéressante, mais elle ne convenait pas vraiment comme modification de ma réponse. D'autres ont accepté et la modification proposée a été rejetée. (Je ne faisais pas partie des électeurs.) Cependant, la technique a du mérite. Il aurait été préférable que srborlongan ait affiché sa propre réponse. Cela ne s'est pas encore produit, et je ne voulais pas que la technique soit perdue dans les brumes de l'historique des modifications rejetées par StackOverflow, j'ai donc décidé de le faire apparaître comme une réponse distincte moi-même.

Fondamentalement, la technique consiste à utiliser certaines des Optionalméthodes de manière intelligente pour éviter d'avoir à utiliser un opérateur ternaire ( ? :) ou une instruction if / else.

Mon exemple en ligne serait réécrit de cette façon:

Optional<Other> result =
    things.stream()
          .map(this::resolve)
          .flatMap(o -> o.map(Stream::of).orElseGet(Stream::empty))
          .findFirst();

Un mon exemple qui utilise une méthode d'assistance serait réécrit de cette façon:

/**
 * Turns an Optional<T> into a Stream<T> of length zero or one depending upon
 * whether a value is present.
 */
static <T> Stream<T> streamopt(Optional<T> opt) {
    return opt.map(Stream::of)
              .orElseGet(Stream::empty);
}

Optional<Other> result =
    things.stream()
          .flatMap(t -> streamopt(resolve(t)))
          .findFirst();

COMMENTAIRE

Comparons directement les versions originales et modifiées:

// original
.flatMap(o -> o.isPresent() ? Stream.of(o.get()) : Stream.empty())

// modified
.flatMap(o -> o.map(Stream::of).orElseGet(Stream::empty))

L'original est une approche simple mais professionnelle: nous obtenons un Optional<Other>; s'il a une valeur, nous renvoyons un flux contenant cette valeur, et s'il n'a pas de valeur, nous renvoyons un flux vide. Assez simple et facile à expliquer.

La modification est astucieuse et présente l'avantage d'éviter les conditionnels. (Je sais que certaines personnes n'aiment pas l'opérateur ternaire. S'il est mal utilisé, il peut en effet rendre le code difficile à comprendre.) Cependant, parfois les choses peuvent être trop intelligentes. Le code modifié commence également par un Optional<Other>. Ensuite, il appelle Optional.mapce qui est défini comme suit:

Si une valeur est présente, appliquez-lui la fonction de mappage fournie et, si le résultat n'est pas nul, renvoyez un Facultatif décrivant le résultat. Sinon, renvoyez un Facultatif vide.

L' map(Stream::of)appel renvoie un Optional<Stream<Other>>. Si une valeur était présente dans l'entrée Facultatif, le Facultatif renvoyé contient un Flux qui contient le seul résultat Autre. Mais si la valeur n'était pas présente, le résultat est un Facultatif vide.

Ensuite, l'appel à orElseGet(Stream::empty)renvoie une valeur de type Stream<Other>. Si sa valeur d'entrée est présente, il obtient la valeur, qui est l'élément unique Stream<Other>. Sinon (si la valeur d'entrée est absente), elle renvoie un vide Stream<Other>. Le résultat est donc correct, le même que le code conditionnel d'origine.

Dans les commentaires sur ma réponse, concernant le montage rejeté, j'avais décrit cette technique comme "plus concise mais aussi plus obscure". Je m'en tiens à cela. Il m'a fallu un certain temps pour comprendre ce qu'il faisait, et il m'a également fallu un certain temps pour rédiger la description ci-dessus de ce qu'il faisait. La subtilité clé est la transformation de Optional<Other>à Optional<Stream<Other>>. Une fois que vous avez réfléchi, cela a du sens, mais ce n'était pas évident pour moi.

Je reconnais cependant que les choses initialement obscures peuvent devenir idiomatiques avec le temps. Il se peut que cette technique finisse par être le meilleur moyen dans la pratique, au moins jusqu'à ce qu'elle Optional.streamsoit ajoutée (si jamais elle le fait).

MISE À JOUR: Optional.stream a été ajouté à JDK 9.


16

Vous ne pouvez pas le faire plus concis comme vous le faites déjà.

Vous prétendez que vous ne voulez pas .filter(Optional::isPresent) et .map(Optional::get) .

Cela a été résolu par la méthode décrite par @StuartMarks, mais en conséquence, vous le mappez maintenant à un Optional<T>, donc vous devez maintenant utiliser .flatMap(this::streamopt)et un get()à la fin.

Il se compose donc toujours de deux instructions et vous pouvez désormais obtenir des exceptions avec la nouvelle méthode! Parce que, que se passe-t-il si chaque option est vide? Ensuite, le findFirst()retournera une option vide et votre get()échouera!

Donc ce que vous avez:

things.stream()
    .map(this::resolve)
    .filter(Optional::isPresent)
    .map(Optional::get)
    .findFirst();

est en fait le meilleur moyen d'accomplir ce que vous voulez, c'est-à-dire que vous souhaitez enregistrer le résultat en tant que T, et non en tant que Optional<T>.

Je pris la liberté de créer une CustomOptional<T>classe qui enveloppe le Optional<T>et fournit une méthode supplémentaire, flatStream(). Notez que vous ne pouvez pas étendre Optional<T>:

class CustomOptional<T> {
    private final Optional<T> optional;

    private CustomOptional() {
        this.optional = Optional.empty();
    }

    private CustomOptional(final T value) {
        this.optional = Optional.of(value);
    }

    private CustomOptional(final Optional<T> optional) {
        this.optional = optional;
    }

    public Optional<T> getOptional() {
        return optional;
    }

    public static <T> CustomOptional<T> empty() {
        return new CustomOptional<>();
    }

    public static <T> CustomOptional<T> of(final T value) {
        return new CustomOptional<>(value);
    }

    public static <T> CustomOptional<T> ofNullable(final T value) {
        return (value == null) ? empty() : of(value);
    }

    public T get() {
        return optional.get();
    }

    public boolean isPresent() {
        return optional.isPresent();
    }

    public void ifPresent(final Consumer<? super T> consumer) {
        optional.ifPresent(consumer);
    }

    public CustomOptional<T> filter(final Predicate<? super T> predicate) {
        return new CustomOptional<>(optional.filter(predicate));
    }

    public <U> CustomOptional<U> map(final Function<? super T, ? extends U> mapper) {
        return new CustomOptional<>(optional.map(mapper));
    }

    public <U> CustomOptional<U> flatMap(final Function<? super T, ? extends CustomOptional<U>> mapper) {
        return new CustomOptional<>(optional.flatMap(mapper.andThen(cu -> cu.getOptional())));
    }

    public T orElse(final T other) {
        return optional.orElse(other);
    }

    public T orElseGet(final Supplier<? extends T> other) {
        return optional.orElseGet(other);
    }

    public <X extends Throwable> T orElseThrow(final Supplier<? extends X> exceptionSuppier) throws X {
        return optional.orElseThrow(exceptionSuppier);
    }

    public Stream<T> flatStream() {
        if (!optional.isPresent()) {
            return Stream.empty();
        }
        return Stream.of(get());
    }

    public T getTOrNull() {
        if (!optional.isPresent()) {
            return null;
        }
        return get();
    }

    @Override
    public boolean equals(final Object obj) {
        return optional.equals(obj);
    }

    @Override
    public int hashCode() {
        return optional.hashCode();
    }

    @Override
    public String toString() {
        return optional.toString();
    }
}

Vous verrez que j'ai ajouté flatStream(), comme ici:

public Stream<T> flatStream() {
    if (!optional.isPresent()) {
        return Stream.empty();
    }
    return Stream.of(get());
}

Utilisé comme:

String result = Stream.of("a", "b", "c", "de", "fg", "hij")
        .map(this::resolve)
        .flatMap(CustomOptional::flatStream)
        .findFirst()
        .get();

Vous devrez toujours renvoyer un Stream<T>ici, car vous ne pouvez pas retourner T, car si !optional.isPresent(), alors T == nullsi vous le déclarez tel, mais alors vous .flatMap(CustomOptional::flatStream)tenteriez d'ajouter nullà un flux et ce n'est pas possible.

Comme exemple:

public T getTOrNull() {
    if (!optional.isPresent()) {
        return null;
    }
    return get();
}

Utilisé comme:

String result = Stream.of("a", "b", "c", "de", "fg", "hij")
        .map(this::resolve)
        .map(CustomOptional::getTOrNull)
        .findFirst()
        .get();

Va maintenant jeter NullPointerExceptionà l'intérieur des opérations de flux.

Conclusion

La méthode que vous avez utilisée est en fait la meilleure méthode.


6

Une version légèrement plus courte utilisant reduce:

things.stream()
  .map(this::resolve)
  .reduce(Optional.empty(), (a, b) -> a.isPresent() ? a : b );

Vous pouvez également déplacer la fonction de réduction vers une méthode utilitaire statique, puis elle devient:

  .reduce(Optional.empty(), Util::firstPresent );

6
J'aime cela, mais il convient de souligner que cela évaluera chaque élément du flux, tandis que findFirst () n'évaluera que jusqu'à ce qu'il trouve un élément actuel.
Duncan McGregor

1
Et malheureusement, l'exécution de chaque résolution est une rupture. Mais c'est intelligent.
Yona Appletree

5

Comme ma réponse précédente ne semblait pas être très populaire, je vais essayer à nouveau.

Une réponse courte:

Vous êtes surtout sur la bonne voie. Le code le plus court pour arriver à la sortie souhaitée que j'ai pu trouver est le suivant:

things.stream()
      .map(this::resolve)
      .filter(Optional::isPresent)
      .findFirst()
      .flatMap( Function.identity() );

Cela s'adaptera à toutes vos exigences:

  1. Il trouvera une première réponse qui résout un non vide Optional<Result>
  2. Il appelle this::resolveparesseusement au besoin
  3. this::resolve ne sera pas appelé après le premier résultat non vide
  4. Il reviendra Optional<Result>

Réponse plus longue

La seule modification par rapport à la version initiale OP était que j'ai supprimé .map(Optional::get)avant l'appel .findFirst()et ajouté .flatMap(o -> o)comme dernier appel de la chaîne.

Cela a un bel effet de se débarrasser du double-Facultatif, chaque fois que stream trouve un résultat réel.

Vous ne pouvez pas vraiment aller plus court que cela en Java.

L'extrait de code alternatif utilisant la fortechnique de boucle plus conventionnelle va être à peu près le même nombre de lignes de code et avoir plus ou moins le même ordre et le nombre d'opérations que vous devez effectuer:

  1. Appel this.resolve,
  2. filtrage basé sur Optional.isPresent
  3. renvoyant le résultat et
  4. un moyen de gérer un résultat négatif (quand rien n'a été trouvé)

Juste pour prouver que ma solution fonctionne comme annoncé, j'ai écrit un petit programme de test:

public class StackOverflow {

    public static void main( String... args ) {
        try {
            final int integer = Stream.of( args )
                    .peek( s -> System.out.println( "Looking at " + s ) )
                    .map( StackOverflow::resolve )
                    .filter( Optional::isPresent )
                    .findFirst()
                    .flatMap( o -> o )
                    .orElseThrow( NoSuchElementException::new )
                    .intValue();

            System.out.println( "First integer found is " + integer );
        }
        catch ( NoSuchElementException e ) {
            System.out.println( "No integers provided!" );
        }
    }

    private static Optional<Integer> resolve( String string ) {
        try {
            return Optional.of( Integer.valueOf( string ) );
        }
        catch ( NumberFormatException e )
        {
            System.out.println( '"' + string + '"' + " is not an integer");
            return Optional.empty();
        }
    }

}

(Il a quelques lignes supplémentaires pour déboguer et vérifier que seulement autant d'appels à résoudre que nécessaire ...)

En exécutant cela sur une ligne de commande, j'ai obtenu les résultats suivants:

$ java StackOferflow a b 3 c 4
Looking at a
"a" is not an integer
Looking at b
"b" is not an integer
Looking at 3
First integer found is 3

Je pense comme Roland Tepp. Pourquoi quelqu'un ferait-il stream <stream <? >> et flat alors que vous pouvez simplement flat avec une option <optional <? >>
Young Hyun Yoo

3

Si cela ne vous dérange pas d'utiliser une bibliothèque tierce, vous pouvez utiliser Javaslang . C'est comme Scala, mais implémenté en Java.

Il est livré avec une bibliothèque de collection immuable complète qui est très similaire à celle connue de Scala. Ces collections remplacent les collections de Java et le flux de Java 8. Il a également sa propre implémentation d'Option.

import javaslang.collection.Stream;
import javaslang.control.Option;

Stream<Option<String>> options = Stream.of(Option.some("foo"), Option.none(), Option.some("bar"));

// = Stream("foo", "bar")
Stream<String> strings = options.flatMap(o -> o);

Voici une solution pour l'exemple de la question initiale:

import javaslang.collection.Stream;
import javaslang.control.Option;

public class Test {

    void run() {

        // = Stream(Thing(1), Thing(2), Thing(3))
        Stream<Thing> things = Stream.of(new Thing(1), new Thing(2), new Thing(3));

        // = Some(Other(2))
        Option<Other> others = things.flatMap(this::resolve).headOption();
    }

    Option<Other> resolve(Thing thing) {
        Other other = (thing.i % 2 == 0) ? new Other(i + "") : null;
        return Option.of(other);
    }

}

class Thing {
    final int i;
    Thing(int i) { this.i = i; }
    public String toString() { return "Thing(" + i + ")"; }
}

class Other {
    final String s;
    Other(String s) { this.s = s; }
    public String toString() { return "Other(" + s + ")"; }
}

Avertissement: je suis le créateur de Javaslang.


3

Tard à la fête, mais qu'en est-il

things.stream()
    .map(this::resolve)
    .filter(Optional::isPresent)
    .findFirst().get();

Vous pouvez vous débarrasser du dernier get () si vous créez une méthode util pour convertir facultativement en flux manuel:

things.stream()
    .map(this::resolve)
    .flatMap(Util::optionalToStream)
    .findFirst();

Si vous renvoyez le flux immédiatement à partir de votre fonction de résolution, vous enregistrez une ligne de plus.


3

J'aimerais promouvoir les méthodes d'usine pour créer des assistants pour les API fonctionnelles:

Optional<R> result = things.stream()
        .flatMap(streamopt(this::resolve))
        .findFirst();

La méthode d'usine:

<T, R> Function<T, Stream<R>> streamopt(Function<T, Optional<R>> f) {
    return f.andThen(Optional::stream); // or the J8 alternative:
    // return t -> f.apply(t).map(Stream::of).orElseGet(Stream::empty);
}

Raisonnement:

  • Comme avec les références de méthode en général, par rapport aux expressions lambda, vous ne pouvez pas capturer accidentellement une variable de la portée accessible, comme:

    t -> streamopt(resolve(o))

  • Il est composable, vous pouvez par exemple appeler Function::andThenle résultat de la méthode d'usine:

    streamopt(this::resolve).andThen(...)

    Alors que dans le cas d'un lambda, vous devez d'abord le lancer:

    ((Function<T, Stream<R>>) t -> streamopt(resolve(t))).andThen(...)


3

Null est pris en charge par le flux fourni Ma bibliothèque AbacusUtil . Voici le code:

Stream.of(things).map(e -> resolve(e).orNull()).skipNull().first();

3

Si vous êtes bloqué avec Java 8 mais avez accès à Guava 21.0 ou plus récent, vous pouvez utiliser Streams.stream pour convertir une option en un flux.

Ainsi, étant donné

import com.google.common.collect.Streams;

tu peux écrire

Optional<Other> result =
    things.stream()
        .map(this::resolve)
        .flatMap(Streams::stream)
        .findFirst();

0

Que dire de cela?

private static List<String> extractString(List<Optional<String>> list) {
    List<String> result = new ArrayList<>();
    list.forEach(element -> element.ifPresent(result::add));
    return result;
}

https://stackoverflow.com/a/58281000/3477539


Pourquoi faire cela quand vous pouvez diffuser et collecter?
OneCricketeer

return list.stream().filter(Optional::isPresent).map(Optional::get).collect(Collectors.toList())), tout comme la question (et votre réponse liée) a ...
OneCricketeer

Je peux me tromper, mais je considère que l'utilisation de isPresent () puis get () n'est pas une bonne pratique. Alors j'essaye de m'éloigner de ça.
rastaman

Si vous utilisez .get() sans isPresent() , alors vous obtenez un avertissement dans IntelliJ
OneCricketeer

-5

Vous vous trompez très probablement.

Java 8 facultatif n'est pas destiné à être utilisé de cette manière. Il est généralement réservé aux opérations de flux de terminal qui peuvent ou non renvoyer une valeur, comme find par exemple.

Dans votre cas, il peut être préférable d'essayer d'abord de trouver un moyen bon marché de filtrer les éléments qui peuvent être résolus, puis d'obtenir le premier élément en option et de le résoudre comme une dernière opération. Mieux encore - au lieu de filtrer, trouvez le premier élément résoluble et résolvez-le.

things.filter(Thing::isResolvable)
      .findFirst()
      .flatMap(this::resolve)
      .get();

La règle générale est que vous devez vous efforcer de réduire le nombre d'éléments dans le flux avant de les transformer en autre chose. YMMV bien sûr.


6
Je pense que la méthode resol () de l'OP renvoyant Facultatif <Autre> est une utilisation parfaitement sensée de Facultatif. Je ne peux pas parler du domaine problématique du PO, bien sûr, mais il se pourrait que la façon de déterminer si quelque chose est résolu est d'essayer de le résoudre. Si tel est le cas, Facultatif fusionne un résultat booléen "était-ce résoluble" avec le résultat de la résolution, en cas de succès, en un seul appel d'API.
Stuart marque

2
Stuart est fondamentalement correct. J'ai un ensemble de termes de recherche par ordre de désirabilité, et je cherche à trouver le résultat du premier qui renvoie quoi que ce soit. Donc en gros Optional<Result> searchFor(Term t). Cela semble correspondre à l'intention de l'option. De plus, les stream () doivent être évalués paresseusement, donc aucun travail supplémentaire de résolution des termes après le premier correspondant ne doit se produire.
Yona Appletree

La question est parfaitement sensible et l'utilisation de flatMap avec facultatif est souvent pratiquée dans d'autres langages de programmation similaires, tels que Scala.
dzs
En utilisant notre site, vous reconnaissez avoir lu et compris notre politique liée aux cookies et notre politique de confidentialité.
Licensed under cc by-sa 3.0 with attribution required.