如何在Java 8中管道多个映射

use*_*493 5 merge dictionary functional-programming java-8 java-stream

我想合并大量的文本文件,每个文件包含〜1000个字符.在合并期间,我想用它们的对替换几个序列.我对Java8中发布的功能特性并不是很熟悉,因此我的第一个解决方案是将序列映射到其替换为map函数,即

Arrays.asList(String[]).stream().
                map( s -> s.replaceAll("_A_", " and ") ).
                map( s -> s.replaceAll("_O_", " or ") ).
                map( s -> s.replaceAll("_X_", " xor ") ).
                reduce( (a,b) -> a + b );
Run Code Online (Sandbox Code Playgroud)

显然,如果想要在运行时添加/删除子语句,则此代码段不易扩展.我想到的一个解决方案是将所有序列存储在地图中replacingMap,然后迭代它以替换所有序列.

final Map<String, String> replacingMap = new HashMap();
replacingMap.put("_A_"," and ");
replacingMap.put("_O_"," or ");
replacingMap.put("_x_"," xor ");
Run Code Online (Sandbox Code Playgroud)

现在,原来的代码可以改写为以下,其中f需要s为一个字符串.基于给定的映射,它替换所有序列并返回替换的字符串.

Arrays.asList(String[]).stream().
                map( s -> f(s) ).
                reduce( (a,b) -> a + b );
Run Code Online (Sandbox Code Playgroud)

我的实现f是命令式的,其中所有序列都在基本for循环中被替换.

我的问题是如何f在不使用命令式循环的情况下以完整功能的方式编写?

Stu*_*rks 5

你可能需要的是组成不同的字符串映射功能集成到一个单一的功能,然后可以传递给map()操作.最终获得合成的函数可以在运行时使用程序逻辑,数据结构中的数据等来确定.

在我们深入研究之前,我会在示例中使用一些不相关的提示:

  • 不要reduce((a, b) -> a + b)用于连接字符串,因为它具有O(n ^ 2)复杂度.请collect(Collectors.joining())改用.

  • 如果您从一个字符串数组开始,您可以使用它Arrays.stream()来流式传输而不将它们包装在List第一个字符串中.

  • 如果您正在从文件中读取行,则可以使用它BufferedReader.lines()来获取行流,而无需先将它们加载到数据结构中.(我的例子中没有显示.)

首先让我们通过一个要编写的函数列表开始显示函数组合.

    List<Function<String,String>> replList = new ArrayList<>();
    replList.add(s -> s.replaceAll("_A_", " and "));
    replList.add(s -> s.replaceAll("_O_", " or "));
    replList.add(s -> s.replaceAll("_X_", " xor "));
Run Code Online (Sandbox Code Playgroud)

我们希望通过流式传输列表并减少来减少这个任意数量的函数列表,直到一个函数Function.compose().什么compose做的是采取两个函数˚F并创建调用一个新的功能,然后调用˚F与具有称为结果.这可能看起来像是倒退,但它在数学上是有道理的.如果你有y = f(g(x))则首先应用g.(还有另一个函数Function.andThen以相反的顺序应用函数.)

以下是执行此操作的代码:

    Function<String,String> mapper = replList.stream()
        .reduce(Function.identity(), Function::compose);
Run Code Online (Sandbox Code Playgroud)

现在func是一个复合函数,它调用所有函数replList.我们现在可以使用它作为map()流管道中单个操作的参数:

    System.out.println(
        Arrays.stream(input)
            .map(mapper)
            .collect(Collectors.joining()));
Run Code Online (Sandbox Code Playgroud)

(注意我Function<String,String>在上面使用而不是可论证的等价UnaryOperator<String>.问题是没有compose返回a的方法UnaryOperator,所以我们必须坚持使用Function类型.)

如果您碰巧已经编写了要应用的函数,则此方法有效.如果你想根据从某个地方加载的数据进行替换,那么使用Mapfor这是一个合理的想法.我们怎么做?

您可以浏览地图并从每个键值对生成一个函数,将它们收集到一个列表中,并减少该列表,如上所示.但是没有必要拥有中间列表,因为可以对地图条目流进行缩减.让我们从你的例子开始:

    Map<String,String> replMap = new HashMap<>();
    replMap.put("_A_", " and ");
    replMap.put("_O_", " or ");
    replMap.put("_X_", " xor ");
Run Code Online (Sandbox Code Playgroud)

我们想要流式传输地图条目,但我们希望减少到单个函数.这与上面的情况不同,我们有许多相同类型的函数,我们希望将它们简化为相同类型的单个函数.在这种情况下,我们希望输入类型是映射条目,但结果类型是函数.我们怎么做?

我们需要使用三个arg重载reduce,它带有一个标识符,一个累加器和一个组合器.我们的身份功能和以前一样Function.identity().组合器也很简单,因为我们已经知道如何使用组合两个函数Function.compose().

棘手的是累加器功能.在每次调用时,获取输入类型的值并将其应用于中间结果,并返回该应用程序的结果.使这更棘手的是结果类型本身就是一个函数.所以我们的累加器需要一个函数,积累一些东西(在?上),并返回另一个函数.

这是一个执行该操作的lambda表达式:

    (func, entry) ->
        func.compose(s -> s.replaceAll(entry.getKey(), entry.getValue()))
Run Code Online (Sandbox Code Playgroud)

该类型都将被推断,所以他们没有宣布,但类型funcIS Function<String,String>和种类entryMap.Entry<String,String>其考虑到我们正在处理的问题应该不会太令人惊讶.

以下是流中的内容:

    Function<String,String> mapper = replMap.entrySet().stream()
        .reduce(Function.identity(),
                (func, entry) ->
                    func.compose(s -> s.replaceAll(entry.getKey(), entry.getValue())),
                Function::compose);
Run Code Online (Sandbox Code Playgroud)

现在我们可以mapper在输入数据的流中使用结果函数,就像我们上面所做的那样.

我认为这不太可能是一个问题,但关于上述问题的一点是,复合函数捕获每个映射条目,并在每次处理输入元素时从每个条目中获取键和值.如果这困扰你(它困扰我,一点点)你可以编写一个稍大的lambda来提取数据,然后在返回的lambda中捕获它们:

    (func, entry) -> {
        String key = entry.getKey();
        String value = entry.getValue();
        return func.compose(s -> s.replaceAll(key, value));
     },
Run Code Online (Sandbox Code Playgroud)

我认为这个函数本身更清晰,但使用多行lambda会使流管道混乱.

无论如何,让我们把它们放在一起.鉴于输入:

String[] input = {
    "[", "_A_", "_O_", "_X_", "_O_", "_M_", "_O_", "_X_", "_O_", "_A_", "]"
};
Run Code Online (Sandbox Code Playgroud)

和地图中的替换字符串集:

    Map<String,String> replMap = new HashMap<>();
    replMap.put("_A_", " and ");
    replMap.put("_O_", " or ");
    replMap.put("_X_", " xor ");
Run Code Online (Sandbox Code Playgroud)

我们生成一个组合映射函数:

    Function<String,String> mapper = replMap.entrySet().stream()
        .reduce(Function.identity(),
                (func, entry) -> {
                    String key = entry.getKey();
                    String value = entry.getValue();
                    return func.compose(s -> s.replaceAll(key, value));
                },
                Function::compose);
Run Code Online (Sandbox Code Playgroud)

然后用它来处理输入:

    System.out.println(
        Arrays.stream(input)
            .map(mapper)
            .collect(Collectors.joining()));
Run Code Online (Sandbox Code Playgroud)

最后,结果是:

[ and  or  xor  or _M_ or  xor  or  and ]
Run Code Online (Sandbox Code Playgroud)

更新2015-02-05

基于Marko Topolnik和Holger的一些建议,这里是mapper的简化版本:

    Function<String,String> mapper = replMap.entrySet().stream()
        .map(entry -> (Function<String,String>) s -> s.replaceAll(entry.getKey(), entry.getValue()))
        .reduce(Function::compose)
        .orElse(Function.identity());
Run Code Online (Sandbox Code Playgroud)

这有两个简化.首先,在缩减步骤之前完成从a MapEntry到a 的映射Function,因此我们可以使用更简单的形式reduce.请注意,我必须将一个显式强制转换Function<String,String>放入此映射步骤,因为我无法使用类型推断工作.(这是在JDK 8u25上.)其次,我们可以使用one-arg形式,它返回一个,然后替换,如果结果中不存在该值,而不是使用Function.identity()两个arg reduce操作的标识值..整齐!OptionalFunction.identity()Optional

  • 另外,看看这个:`replMap.entrySet().stream().reduce(Function.identity()(func,entry) - > func.compose(s - > s.replaceAll(entry.getKey(),entry. getValue())),Function :: compose);`我想说你在这里真正做的是将一个条目映射到一个函数,然后用`compose`减少.考虑到这一点,它可以简化:`replMap.entrySet().stream().map(e - > s - > s.replaceAll(entry.getKey(),entry.getValue())).reduce(identity( ),Function :: compose);`这样,与第一个例子的类比更加清晰(引入了解决"阻抗不匹配"的映射步骤). (2认同)
  • @Marko Topolnik:是的,我更喜欢`reduce(Function :: compose).orElse(identity())`以避免使用身份函数编写函数... (2认同)