Java 8中减少UnaryOperators列表

10
在Java 8中,将一组UnaryOperator减少到一个可以调用apply的UnaryOperator的首选方法是什么? 例如,我有以下内容:
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);

}

但是这段代码在 (MyFilter)f1.andThen(f2) 处抛出了 ClassCastException 异常。 我确实想要最终达到这段代码的效果:
MyObject o = obj;
for(MyFilter f:filters){
  o = f.apply(o);
}
return o;

但我也很好奇如何使用 composeandThen 将一组函数简化为一个函数。

5个回答

18

使用 composeandThen 的问题在于它们都内置于 Function 接口和类型中,无论是编译时类型还是运行时类型,它们返回的函数类型都是 Function 而不是 UnaryOperator 或你定义的任何子接口。例如,假设我们有以下代码:

UnaryOperator<String> a = s -> s + "bar";
UnaryOperator<String> b = s -> s + s;

有人可能会认为我们可以写

UnaryOperator<String> c = a.compose(b);

但是这不起作用!相反,我们必须写成

Function<String, String> c = a.compose(b);

为了使这个工作正常,UnaryOperator必须提供andThencompose的协变覆盖。 (可以说这是API中的一个错误)。您可以在子接口中执行相同的操作,或者手动编写lambda表达式也很简单。例如:

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"));
}

这会打印出[foobar] [foobar]。请注意,我使用了reduce的双参数形式,以避免处理Optional

或者,如果您经常进行函数组合,可以在自己的接口中重新实现所需的方法。这并不太难。这些基于java.util.Function中的实现,但将本示例中使用的具体String类型替换为泛型。

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));
    }
}

这将用于以下方面:

MyOperator composite =
    list.stream()
        .reduce(MyOperator.identity(), (a, b) -> a.andThen(b));

增加接口的复杂性,以便使用andThen代替嵌套的lambda表达式,可能只是个人偏好问题。


1
非常好的答案,谢谢。而且使用两个参数的reduce形式可以摆脱Optional。 - ABT

7

通过在抽象方法上使用方法引用语法,可以将一个函数式接口转换为另一个函数式接口。

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"));
    }
}

使用'::apply'的建议对我很有帮助。比起首选答案中给出的抛弃UnaryOperator并转向Function的建议更好。 - Julian Hyde

6

MyFilterFunction中继承了方法andThen,因此返回的类型是Function,不能被强制转换为MyFilter。但由于它具有所需的函数签名,您可以使用lambda表达式或方法引用创建MyFilter实例。

例如,将(f1,f2)->(MyFilter)f1.andThen(f2)更改为(f1,f2)-> f1.andThen(f2)::apply

通过这个改变,整个方法看起来像:

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);
}

但是您应该重新考虑设计。实际上,没有必要让结果函数成为 MyFilter 的一个实例,甚至可以放宽输入限制以接受更多不仅仅是 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);
}

另外,使用 Stuart Marks 提供的消除 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);
}

为了完整起见,你也可以在流上链接你的MyFilter而不是组合一个新函数:
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();
}

也是一份很棒和完整的答案。你最终的代码正是我正在寻找的替代方案,谢谢。 - ABT
快速提问,Collections.unmodifiableList是否优化了stream()和reduce()函数,使其比原始列表更加不可变? - ABT
1
不,我不期望有任何差别。这只是在这里从 List<? extends X>List<X> 的类型安全转换。 - Holger

3

在执行缩减操作之前,您可以将其转换为 Function<?, ?> 类型:

interface MyFilter extends UnaryOperator<MyObject>{};

Function<MyObject, MyObject> mf = filters.stream()
    .map(f -> (Function<MyObject, MyObject>) f)
    .reduce(Function.identity(), Function::compose);

这让您可以使用 Function.identity()Function::compose 方法,无需在您的接口中重新实现它们(就像 Stuart Marks 建议的那样)。
转换始终安全,因为它是向上/扩展的。

如果需要一个已输入引用,可以使用 MyFilter mfTyped = mf::apply; 来在 Function<T, T>UnaryOperator<T> 之间进行转换。 - Gediminas Rimsa

2

简短回答:永远不要使用接口 UnaryOperator<T>,而是使用 Function<T, T>。如果这样做,方法 compose 和 andThen 将按预期工作。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接