Réduire une liste d'UnaryOperators en Java 8
-
20-12-2019 - |
Question
Quelle est la manière préférée de réduire une liste d'UnaryOperators dans Java 8 jusqu'à ce qu'ils représentent un UnaryOperator sur lequel je peux appeler ?Par exemple j'ai ce qui suit
interface MyFilter extends UnaryOperator<MyObject>{};
public MyObject filterIt(List<MyFilter> filters,MyObject obj){
Optional<MyFilter> mf = filters
.stream()
.reduce( (f1,f2)->(MyFilter)f1.andThen(f2));
return mf.map(f->f.apply(obj)).orElse(obj);
}
Mais ce code lance un ClassCastException
à (MyFilter)f1.andThen(f2)
.Je veux vraiment l'effet de ce code au final :
MyObject o = obj;
for(MyFilter f:filters){
o = f.apply(o);
}
return o;
Mais je suis également curieux de savoir comment réduire un ensemble de fonctions à une seule fonction, en utilisant compose
ou andThen
.
La solution
Le problème de l'utilisation compose
ou andThen
c'est qu'ils sont intégrés au Function
interface et le type -- à la fois au moment de la compilation et au moment de l'exécution -- des fonctions qu'ils renvoient est Function
et pas UnaryOperator
ou une sous-interface telle que vous l'avez définie.Par exemple, supposons que nous ayons
UnaryOperator<String> a = s -> s + "bar";
UnaryOperator<String> b = s -> s + s;
On pourrait penser qu'on pourrait écrire
UnaryOperator<String> c = a.compose(b);
mais ça ne marche pas !Il faut plutôt écrire
Function<String, String> c = a.compose(b);
Pour que cela fonctionne, UnaryOperator
devrait fournir des remplacements de covariants de andThen
et compose
.(Il s'agit sans doute d'un bug dans l'API.) Vous feriez la même chose dans votre sous-interface.Ou bien, c'est assez simple d'écrire les lambdas à la main.Par exemple,
interface MyOperator extends UnaryOperator<String> { }
public static void main(String[] args) {
List<MyOperator> list =
Arrays.asList(s -> s + "bar",
s -> "[" + s + "]",
s -> s + s);
MyOperator composite =
list.stream()
.reduce(s -> s, (a, b) -> s -> b.apply(a.apply(s)));
System.out.println(composite.apply("foo"));
}
Ceci s'imprime [foobar][foobar]
.Notez que j'ai utilisé la forme à deux arguments de reduce
afin d'éviter d'avoir à gérer Optional
.
Alternativement, si vous faites beaucoup de composition de fonctions, vous pouvez réimplémenter les méthodes dont vous avez besoin dans votre propre interface.Ce n'est pas trop difficile.Ceux-ci sont basés sur les implémentations dans java.util.Function
mais avec le béton String
le type que j'ai utilisé dans cet exemple en remplacement des génériques.
interface MyOperator extends UnaryOperator<String> {
static MyOperator identity() {
return s -> s;
}
default MyOperator andThen(MyOperator after) {
Objects.requireNonNull(after);
return s -> after.apply(this.apply(s));
}
default MyOperator compose(MyOperator before) {
Objects.requireNonNull(before);
return s -> this.apply(before.apply(s));
}
}
Celui-ci serait utilisé comme suit :
MyOperator composite =
list.stream()
.reduce(MyOperator.identity(), (a, b) -> a.andThen(b));
Qu'il s'agisse de gonfler l'interface pour écrire andThen
au lieu d'un lambda imbriqué, c'est une question de goût, je suppose.
Autres conseils
MyFilter
hérite de la méthode andThen
depuis Function
et donc le type renvoyé est Function
et ne peut pas être converti en MyFilter
.Mais comme il possède la signature de fonction souhaitée, vous pouvez créer le MyFilter
instance en utilisant un lambda ou une référence de méthode.
Par exemple.changement (f1,f2)->(MyFilter)f1.andThen(f2)
à (f1,f2)-> f1.andThen(f2)::apply
.
Avec ce changement, la méthode entière ressemble à :
public static MyObject filterIt(List<MyFilter> filters, MyObject obj) {
Optional<MyFilter> mf =
filters.stream().reduce( (f1,f2)-> f1.andThen(f2)::apply);
return mf.map(f->f.apply(obj)).orElse(obj);
}
Mais vous devriez repenser votre conception.Il n'est pas nécessaire que la fonction résultante soit une instance de MyFilter
, en fait, même la saisie peut être assouplie pour accepter plus que simplement List<MyFilter>
:
// will accept List<MyFilter> as input
public static MyObject filterIt(
List<? extends Function<MyObject,MyObject>> filters, MyObject obj) {
List<Function<MyObject,MyObject>> l=Collections.unmodifiableList(filters);
Optional<Function<MyObject,MyObject>> mf=l.stream().reduce(Function::andThen);
return mf.orElse(Function.identity()).apply(obj);
}
ou, en utilisant l'astuce de Stuart Marks pour se débarrasser du Optional
:
// will accept List<MyFilter> as input
public static MyObject filterIt(
List<? extends Function<MyObject,MyObject>> filters,MyObject obj) {
List<Function<MyObject,MyObject>> l=Collections.unmodifiableList(filters);
return l.stream().reduce(Function.identity(), Function::andThen).apply(obj);
}
Juste pour être complet, vous pouvez également enchaîner vos MyFilter
s sur un flux plutôt que de composer une nouvelle fonction :
public static MyObject filterIt2(List<MyFilter> filters,MyObject obj) {
Stream<MyObject> s=Stream.of(obj);
for(MyFilter f: filters) s=s.map(f);
return s.findAny().get();
}
Une réponse courte :N'utilisez jamais l'interface UnaryOperator<T>
, utiliser Function<T, T>
plutôt.Si vous faites cela, les méthodes composent et andThen
fonctionnera comme prévu.
Il est possible de convertir une interface fonctionnelle en une autre interface fonctionnelle en utilisant une syntaxe de référence de méthode sur sa méthode abstraite.
import java.util.function.UnaryOperator;
import java.util.stream.Stream;
public class Example {
public static void main(String[] args) {
Stream<UnaryOperator<String>> stringMappers = Stream.of(
s -> s + "bar",
s -> "[" + s + "]",
s -> s + s
);
UnaryOperator<String> f = stringMappers.reduce(
s -> s,
(a, b) -> a.andThen(b)::apply
);
System.out.println(f.apply("foo"));
}
}
Vous pouvez faire un casting pour Function<?, ?>
avant de faire la réduction :
interface MyFilter extends UnaryOperator<MyObject>{};
Function<MyObject, MyObject> mf = filters.stream()
.map(f -> (Function<MyObject, MyObject>) f)
.reduce(Function.identity(), Function::compose);
Cela vous permet d'utiliser le Function.identity()
et Function::compose
méthodes, évitant ainsi d'avoir à les réimplémenter dans votre interface (comme l'a suggéré Stuart Marks).
Le plâtre est toujours en sécurité car il s'élève/s'élargit.