web-dev-qa-db-fra.com

Utilisation de Java 8 en option avec Stream :: flatMap

Le nouveau framework de flux Java 8 et ses amis constituent un code très concis Java, mais je suis tombé sur une situation apparemment simple qui est délicate à faire de manière concise.

Considérons un List<Thing> things et une méthode Optional<Other> resolve(Thing thing). Je veux mapper la Things à Optional<Other>s et obtenir le premier Other. La solution évidente serait d’utiliser things.stream().flatMap(this::resolve).findFirst(), mais flatMap exige que vous retourniez un flux, et Optional n’a pas de méthode stream() (ou est-ce un Collection ou fournissez une méthode pour le convertir ou l’afficher en tant que Collection).

Le mieux que je puisse trouver est la suivante:

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?

211
Yona Appletree

Java 9

Optional.stream a été ajouté à JDK 9. Ceci vous permet d'effectuer les opérations suivantes, sans recourir à une méthode d'assistance:

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

Java 8

Oui, il s'agissait d'un petit trou dans l'API, en ce sens qu'il est quelque peu gênant de transformer un flux optionnel en un flux de longueur zéro ou égal à un. Vous pourriez faire ceci:

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

Avoir l'opérateur ternaire à l'intérieur de la flatMap est un peu lourd, cependant, il serait peut-être préférable d'écrire une petite fonction d'assistance 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 mis en ligne l'appel à resol () au lieu d'avoir une opération map () séparée, mais c'est une question de goût.

229
Stuart Marks

J'ajoute cette deuxième réponse sur la base d'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 à ma réponse. D'autres ont accepté et le montage proposé a été rejeté. (Je n'étais pas l'un des électeurs.) La technique a du mérite, cependant. Il aurait été préférable que srborlongan ait posté sa propre réponse. Cela n'est pas encore arrivé 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 la présenter comme une réponse séparée moi-même.

La technique consiste essentiellement à utiliser certaines des méthodes Optional de manière astucieuse pour éviter de devoir 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 exemple qui utilise une méthode d'assistance serait récrit de la manière suivante:

/**
 * 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 s'il s'agit d'une procédure de travail: nous obtenons un Optional<Other>; s'il a une valeur, nous retournons 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 conditions. (Je sais que certaines personnes n'aiment pas l'opérateur ternaire. Si elle est mal utilisée, cela peut 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.map qui est défini comme suit:

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

L'appel map(Stream::of) renvoie un Optional<Stream<Other>>. Si une valeur était présente dans l'entrée Facultatif, l'élément facultatif renvoyé contient un flux contenant le résultat unique Autre. Mais si la valeur n'était pas présente, le résultat est un optionnel 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), il retourne un Stream<Other> vide. Donc, le résultat est correct, le même que le code conditionnel d'origine.

Dans les commentaires sur ma réponse concernant le montage refusé, 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 que cela faisait et aussi pour écrire la description ci-dessus de ce qu'il faisait. La subtilité clé est la transformation de Optional<Other> en Optional<Stream<Other>>. Une fois que vous en avez parlé, 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 la meilleure solution dans la pratique, du moins jusqu'à ce que Optional.stream soit ajouté (le cas échéant).

UPDATE: Optional.stream a été ajouté à JDK 9.

65
Stuart Marks

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

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

Ceci a été résolu par la méthode décrite par @StuartMarks. Cependant, vous devez maintenant le mapper sur un Optional<T>, vous devez donc utiliser .flatMap(this::streamopt) et un get() à la fin.

Donc, il reste toujours deux déclarations et vous pouvez maintenant obtenir des exceptions avec la nouvelle méthode! Parce que, si chaque option est vide? Alors la findFirst() retournera un optionnel vide et votre get() échouera!

Alors qu'est-ce que vous avez:

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

is est en fait le meilleur moyen d’accomplir ce que vous voulez, c’est-à-dire que vous voulez enregistrer le résultat en tant que T, pas en tant que Optional<T>.

J'ai pris la liberté de créer une classe CustomOptional<T> 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 encore devrez renvoyer un Stream<T> ici, car vous ne pouvez pas retourner T, car si !optional.isPresent(), alors T == null si vous le déclarez tel, mais votre .flatMap(CustomOptional::flatStream) essaierait d'ajouter null à un flux, ce qui n'est pas possible.

À titre d'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();

Jetons maintenant un NullPointerException dans les opérations de flux.

Conclusion

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

14
skiwi

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 d’utilité statique, qui devient alors:

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

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

Une réponse courte:

Vous êtes principalement sur la bonne voie. Le code le plus court pour arriver à la sortie désirée que je pourrais trouver est le suivant:

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

Cela conviendra à toutes vos exigences:

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

Réponse plus longue

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

Cela a pour effet de supprimer le double-facultatif chaque fois que le flux 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 technique de boucle plus conventionnelle for va consister en un nombre identique de lignes de code et doit avoir à peu près le même ordre et le même nombre d'opérations à effectuer:

  1. Appeler this.resolve,
  2. filtrage basé sur Optional.isPresent
  3. renvoyer le résultat et
  4. un moyen de traiter 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 ne comporte que quelques lignes supplémentaires pour le débogage et la vérification du nombre d'appels à résoudre, le cas échéant ...)

En exécutant ceci sur une ligne de commande, j'ai eu 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
5
Roland Tepp

Je voudrais promouvoir méthodes d'usine pour la création d'auxiliaires 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 pour les références de méthode en général, comparé aux expressions lambda, vous ne pouvez pas capturer accidentellement une variable de la portée accessible, comme:

    t -> streamopt(resolve(o))

  • C'est composable, vous pouvez par exemple Appelez Function::andThen sur le résultat de la méthode usine:

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

    Alors que dans le cas d'un lambda, il faut d'abord le lancer:

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

3
charlie

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
user_3380739

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 collections complète immuable qui ressemble beaucoup à celle 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 + ")"; }
}

Disclaimer: Je suis le créateur de Javaslang.

3
Daniel Dietrich

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

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

Vous pouvez vous débarrasser de la dernière méthode get () si vous créez une méthode util pour la conversion facultative en flux manuellement:

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

Si vous renvoyez le flux immédiatement après votre fonction de résolution, vous enregistrez une ligne supplémentaire.

3
Ljubopytnov

Si vous êtes coincé avec Java 8 mais que vous avez accès à Guava 21.0 ou plus récent, vous pouvez utiliser Streams.stream pour convertir un fichier facultatif en 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
Nicolas Payette