J'ai plusieurs fonctions:
String first(){}
String second(){}
...
String default(){}
Chacun peut renvoyer une valeur nulle, sauf la valeur par défaut. chaque fonction peut prendre différents paramètres. Par exemple, le premier pourrait ne prendre aucun argument, le second pourrait prendre une chaîne, le troisième pourrait prendre trois arguments, etc. Ce que j'aimerais faire est quelque chose comme:
ObjectUtils.firstNonNull(first(), second(), ..., default());
Le problème est qu'à cause de l'appel de fonction, cela fait une évaluation désirée. Où voudrais-je quitter tôt, disons après la deuxième fonction (car les appels de fonction peuvent être coûteux, pensez aux appels API, etc.). Dans d'autres langues, vous pouvez faire quelque chose de similaire à ceci:
return first() || second() || ... || default()
En Java, je sais que je peux faire quelque chose comme:
String value;
if (value = first()) == null || (value = second()) == null ...
return value;
Ce n'est pas très lisible IMO à cause de toutes les vérifications == null . ObjectUtils.firstNonNull () crée d'abord une collection, puis itère, ce qui est correct tant que la fonction est évaluée paresseusement.
Suggestions? (en plus de faire un tas d'if)
String s = Stream.<Supplier<String>>of(this::first, this::second /*, ... */)
.map(Supplier::get)
.filter(Objects::nonNull)
.findFirst()
.orElseGet(this::defaultOne);
Il s'arrête sur la première valeur non nulle ou définit la valeur renvoyée par defaultOne
. Tant que vous restez séquentiel, vous êtes en sécurité. Bien sûr, cela nécessite Java 8 ou version ultérieure.
La raison pour laquelle il s'arrête à la première occurrence d'une valeur non nulle est due à la façon dont Stream
gère chaque étape. map
est une opération intermédiaire , de même filter
. Le findFirst
de l'autre côté est un fonctionnement de la borne de court-circuit . Il continue donc avec l'élément suivant jusqu'à ce que l'un corresponde au filtre. Si aucun élément ne correspond, une option vide est renvoyée et donc le fournisseur orElseGet
- est appelé.
this::first
, etc. ne sont que des références de méthode. S'ils sont statiques, remplacez-le par YourClassName::first
, etc.
Voici un exemple si la signature de vos méthodes diffère:
String s = Stream.<Supplier<String>>of(() -> first("takesOneArgument"),
() -> second("takes", 3, "arguments")
/*, ... */)
.map(Supplier::get)
.filter(Objects::nonNull)
.findFirst()
.orElseGet(this::defaultOne);
Notez que Supplier
n'est évalué que lorsque vous appelez get
dessus. De cette façon, vous obtenez votre comportement d'évaluation paresseux. Les paramètres de méthode dans votre expression fournisseur-lambda doivent être définitifs ou effectivement définitifs.
Cela peut être fait assez proprement avec un flux de Suppliers
.
Optional<String> result = Stream.<Supplier<String>> of(
() -> first(),
() -> second(),
() -> third() )
.map( x -> x.get() )
.filter( s -> s != null)
.findFirst();
La raison pour laquelle cela fonctionne est que malgré les apparences, l'exécution entière est dirigée par findFirst()
, qui extrait un élément de filter()
, qui extrait paresseusement des éléments de map()
, qui appelle get()
pour gérer chaque pull. findFirst()
arrêtera de tirer du flux lorsqu'un élément aura passé le filtre, donc les fournisseurs suivants n'auront pas get()
appelé.
Bien que je personnellement trouve le nettoyant déclaratif de style Stream et plus expressif, vous n'avez pas ayez d'utiliser Stream pour travailler avec Supplier
s si vous n'aimez pas le style:
Optional<String> firstNonNull(List<Supplier<String>> suppliers {
for(Supplier<String> supplier : suppliers) {
String s = supplier.get();
if(s != null) {
return Optional.of(s);
}
}
return Optional.empty();
}
Il devrait être évident que, au lieu de renvoyer Optional
, vous pouvez également retourner un String
, soit en retournant null (yuk), une chaîne par défaut, ou en levant une exception, si vous épuisez les options de la liste.
Il n'est pas lisible car vous avez affaire à un tas de fonctions distinctes qui n'expriment aucun type de connexion les unes avec les autres. Lorsque vous essayez de les assembler, le manque de direction est apparent.
Essayez plutôt
public String getFirstValue() {
String value;
value = first();
if (value != null) return value;
value = second();
if (value != null) return value;
value = third();
if (value != null) return value;
...
return value;
}
Sera-ce long? Probablement. Mais vous appliquez du code au-dessus d'une interface qui n'est pas conviviale envers votre approche.
Maintenant, si vous pouviez changer l'interface, vous pourriez rendre l'interface plus conviviale. Un exemple possible serait que les étapes soient des objets "ValueProvider".
public interface ValueProvider {
public String getValue();
}
Et puis vous pouvez l'utiliser comme
public String getFirstValue(List<ValueProvider> providers) {
String value;
for (ValueProvider provider : providers) {
value = provider.getValue();
if (value != null) return value;
}
return null;
}
Et il existe diverses autres approches, mais elles nécessitent de restructurer le code pour être plus orienté objet. Rappelez-vous, juste parce que Java est un langage de programmation orienté objet, cela ne signifie pas qu'il sera toujours utilisé de manière orientée objet. La first()
... last()
La liste des méthodes n'est pas très orientée objet, car elle ne modélise pas un List
. Même si les noms de méthode sont expressifs, un List
contient des méthodes qui permettre une intégration facile avec des outils comme for
boucles et Iterators
.
Si vous utilisez Java 8, vous pouvez convertir ces appels de fonction en lambdas.
public static<T> T firstNonNull(Supplier<T> defaultSupplier, Supplier<T>... funcs){
return Arrays.stream(funcs).filter(p -> p.get() != null).findFirst().orElse(defaultSupplier).get();
}
Si vous ne voulez pas l'implémentation générique et ne l'utilisez que pour String
s continuez et remplacez simplement T
par String
:
public static String firstNonNull(Supplier<String> defaultSupplier, Supplier<String>... funcs){
return Arrays.stream(funcs).filter(p -> p.get() != null).findFirst().orElse(defaultSupplier).get();
}
Et puis appelez-le comme:
firstNonNull(() -> getDefault(), () -> first(arg1, arg2), () -> second(arg3));
P.S. btw default
est un mot clé réservé, vous ne pouvez donc pas l'utiliser comme nom de méthode :)
EDIT: ok, la meilleure façon de le faire serait de retourner facultatif, alors vous n'avez pas besoin de passer séparément le fournisseur par défaut:
@SafeVarargs
public static<T> Optional<T> firstNonNull(Supplier<T>... funcs){
return Arrays.stream(funcs).filter(p -> p.get() != null).map(s -> s.get()).findFirst();
}
Si vous souhaitez l'intégrer dans une méthode utilitaire, vous devrez envelopper chaque fonction dans quelque chose qui diffère l'exécution. Peut-être quelque chose comme ça:
public interface Wrapper<T> {
T call();
}
public static <T> T firstNonNull(Wrapper<T> defaultFunction, Wrapper<T>... funcs) {
T val;
for (Wrapper<T> func : funcs) {
if ((val = func.call()) != null) {
return val;
}
}
return defaultFunction.call();
}
Vous pouvez utiliser Java.util.concurrent.Callable
Au lieu de définir votre propre classe Wrapper
, mais vous devrez alors faire face à l'exception que Callable.call()
est déclarée lever.
Cela peut ensuite être appelé avec:
String value = firstNonNull(
new Wrapper<>() { @Override public String call() { return defaultFunc(); },
new Wrapper<>() { @Override public String call() { return first(); },
new Wrapper<>() { @Override public String call() { return second(); },
...
);
Dans Java 8, comme le souligne @dorukayhan, vous pouvez vous dispenser de définir votre propre classe Wrapper
et utiliser simplement l'interface Supplier
. De plus, l'appel peut être fait beaucoup plus proprement avec les lambdas:
String value = firstNonNull(
() -> defaultFunc(),
() -> first(),
() -> second(),
...
);
Vous pouvez également (comme le suggère @Oliver Charlesworth) utiliser des références de méthode comme raccourci pour les expressions lambda:
String value = firstNonNull(
MyClass::defaultFunc,
MyClass::first,
MyClass::second,
...
);
Je suis de deux esprits quant à ce qui est plus lisible.
Alternativement, vous pouvez utiliser l'une des solutions de streaming proposées par de nombreuses autres réponses.
Créez simplement une classe avec une fonction comme celle-ci:
class ValueCollector {
String value;
boolean v(String val) { this.value = val; return val == null; }
}
ValueCollector c = new ValueCollector();
if c.v(first()) || c.v(second()) ...
return c.value;