【问题标题】:How pipeline multiple maps in Java 8如何在 Java 8 中流水线化多个映射
【发布时间】:2015-03-23 00:32:06
【问题描述】:

我想合并大量文本文件,每个文件包含约 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 );

显然,如果想要添加/删除替代项,特别是在运行时,这段代码 sn-p 不容易扩展。我想到的一种解决方案是将所有序列存储在一个映射中,例如replacingMap,然后迭代它以替换所有序列。

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

现在原始代码可以重写如下,其中fs 作为字符串。根据给定的映射,它替换所有序列并返回替换的字符串。

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

我对@9​​87654327@ 的实现是命令式的,所有的序列都在一个基本的for 循环中被替换。

我的问题是f 如何在不使用命令式循环的情况下以功能齐全的风格编写?

【问题讨论】:

    标签: dictionary merge functional-programming java-8 java-stream


    【解决方案1】:

    您可能想要的是将不同的字符串映射函数组合到一个函数中,然后您可以将其传递给map() 操作。最终组合的函数可以在运行时使用程序逻辑、数据结构中的数据等来确定。

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

    • 不要使用reduce((a, b) -&gt; 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 "));
    

    我们想减少这个任意数量的函数列表到一个函数,通过流式传输列表并减少Function.compose()compose 所做的就是取两个函数 fg 并创建一个调用 g 的新函数,然后调用 f 的结果是调用了 g。这似乎是倒退的,但在数学上是有道理的。如果您有 y = f(g(x)),则首先应用 g。 (还有另一个函数Function.andThen 以相反的顺序应用这些函数。)

    执行此操作的代码如下所示:

        Function<String,String> mapper = replList.stream()
            .reduce(Function.identity(), Function::compose);
    

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

        System.out.println(
            Arrays.stream(input)
                .map(mapper)
                .collect(Collectors.joining()));
    

    (请注意,我在上面使用了Function&lt;String,String&gt;,而不是可以说是等效的UnaryOperator&lt;String&gt;。问题是没有返回UnaryOperatorcompose 方法,所以我们必须坚持使用Function 请输入。)

    如果您碰巧已经编写了要应用的功能,则此方法有效。如果您想根据从某处加载的数据进行替换,那么使用Map 是一个合理的想法。我们该怎么做?

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

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

    我们希望流式传输映射条目,但我们希望简化为单个函数。这与上面的情况不同,我们有许多相同类型的函数,我们希望将它们简化为相同类型的单个函数。在这种情况下,我们希望输入类型是映射条目,但结果类型是函数。我们如何做到这一点?

    我们需要使用reduce 的三参数重载,它接受一个identity、一个accumulator 和一个combiner。我们的身份函数和以前一样是Function.identity()。组合器也很简单,因为我们已经知道如何使用Function.compose() 组合两个函数。

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

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

        (func, entry) ->
            func.compose(s -> s.replaceAll(entry.getKey(), entry.getValue()))
    

    所有类型都会被推断出来,所以它们没有被声明,但是func 的类型是Function&lt;String,String&gt;entry 的类型是Map.Entry&lt;String,String&gt;,考虑到我们正在解决的问题。

    这是流中的样子:

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

    现在我们可以像上面一样在输入数据的流中使用生成的mapper 函数。

    我认为这不太可能成为问题,但关于上述的一点是,复合函数每次处理输入元素时都会捕获每个映射条目并从每个条目中获取键和值。如果这让您感到困扰(它让我有点困扰),您可以编写一个稍大的 lambda,在将数据捕获到返回的 lambda 之前提取数据:

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

    我认为这个函数本身更清晰一些,但使用多行 lambda 往往会使上游管道混乱。

    无论如何,让我们把它们放在一起。给定输入:

    String[] input = {
        "[", "_A_", "_O_", "_X_", "_O_", "_M_", "_O_", "_X_", "_O_", "_A_", "]"
    };
    

    以及映射中的替换字符串集:

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

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

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

    然后用它来处理输入:

        System.out.println(
            Arrays.stream(input)
                .map(mapper)
                .collect(Collectors.joining()));
    

    最后,结果是:

    [ and  or  xor  or _M_ or  xor  or  and ]
    

    2015-02-05 更新

    根据 Marko Topolnik 和 Holger 的一些建议,以下是映射器的简化版本:

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

    这有两个简化。首先,从MapEntryFunction 的映射在归约步骤之前完成,因此我们可以使用reduce 的更简单形式。请注意,我必须在此映射步骤中对Function&lt;String,String&gt; 进行显式强制转换,因为我无法使类型推断起作用。 (这是在 JDK 8u25 上。)其次,我们可以使用 one-arg 形式,而不是使用 Function.identity() 作为双参数 reduce 操作的标识值,它返回一个 Optional,然后替换 @如果结果Optional 中不存在该值,则为987654367@。整洁!

    【讨论】:

    • +1 以获得出色而详细的答案;但是我发现了一件会让读者感到困惑的事情:在第一个示例中,compose 被用作累加器,但没有明确提及;然后在第二个示例中,使用相同的函数作为 combiner,同时引入另一个累加器。双参数 reduce 实际上重用累加器作为组合器,因此两个示例都使用 compose 作为组合器。所有这些都非常令人困惑,需要更明确的解释。
    • 另外,看看这个:replMap.entrySet().stream().reduce(Function.identity() (func, entry) -&gt; func.compose(s -&gt; s.replaceAll(entry.getKey(), entry.getValue())), Function::compose); 我想说你在这里真正要做的是将一个条目映射到一个函数,然后用compose 减少。考虑到这一点,它可以被简化:replMap.entrySet().stream().map(e -&gt; s -&gt; s.replaceAll(entry.getKey(), entry.getValue())).reduce(identity(), Function::compose); 这样与第一个示例的类比更加清晰(引入了解决“阻抗不匹配”的映射步骤)。
    • @Marko Topolnik:是的,我更喜欢reduce(Function::compose).orElse(identity()) 避免用恒等函数组合函数……
    • @MarkoTopolnik 感谢您的建议。我确实认为从归约中提取映射可以简化事情。我无法让类型推断起作用,所以我不得不进行强制转换,但这仍然是一个改进。虽然在这一点上有点悬而未决,但我不确定是否值得以reduce 的不同形式提出 accumulatorcombiner 的问题。特别要注意,一元和二元形式的累加器与三元形式的累加器是不同的类型,但与三元组合器的类型相同。
    • @MarkoTopolnik 在这种情况下,我认为演员表更清晰。我们有一个返回 lambda 的 lambda,格式为 map(e -&gt; s -&gt; s.foo())。如果我们必须帮助编译器处理其类型,则将强制转换放入,例如map(e -&gt; (Type) s -&gt; s.foo()) 明确返回的类型。对于&lt;Type&gt;map(e -&gt; s -&gt; s.foo())Type 在这种情况下的含义并不明显,您必须查看map 的定义才能弄清楚。
    【解决方案2】:

    一种方法是使用正则表达式来拆分字符串,然后使用Map.getOrDefault 替换所有匹配项。但是,以下示例假定所有匹配条目都基于相同的模式(字母前后的_)。

    // Replacement data
    final Map<String, String> m = new HashMap<>();
    m.put("_A_", " and ");
    m.put("_O_", " or ");
    m.put("_X_", " xor ");
    
    // This version keeps the lines as an array. It creates an inner
    // stream for each line and in the end all is mapped to a new String[]
    final String[] multipleLines = Arrays.stream(lines)
            .map(line -> Arrays.stream(line.split("((?<=_.?_)|(?=_.?_))"))
                    .map(word -> m.getOrDefault(word, word))
                    .collect(Collectors.joining()))
            .toArray(String[]::new);
    
    // This version simply joins all strings to one big String.
    // The flatMap-method combines a series of Streams to one Stream
    final String oneLongString = Arrays.stream(lines)
            .flatMap(line -> Arrays.stream(line.split("((?<=_.?_)|(?=_.?_))")))
            .map(word -> m.getOrDefault(word, word))
            .collect(Collectors.joining());
    

    【讨论】:

      猜你喜欢
      • 2021-12-20
      • 1970-01-01
      • 2021-08-28
      • 1970-01-01
      • 2018-12-22
      • 1970-01-01
      • 1970-01-01
      • 2022-01-18
      • 2014-06-06
      相关资源
      最近更新 更多