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 Thing
s à 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?
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();
_
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.
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.
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.
La méthode que vous avez utilisée est en fait la meilleure.
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 );
Comme mon réponse précédente ne semblait pas être très populaire, je vais essayer à nouveau.
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:
Optional<Result>
this::resolve
paresseusement au besointhis::resolve
ne sera pas appelé après le premier résultat non videOptional<Result>
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:
this.resolve
,Optional.isPresent
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
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(...)
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();
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.
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.
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();
_