Java 8中使用lambda表达式对HashMap进行Map Reduce操作

9

我有一个String,想要替换其中的一些单词。我有一个HashMap,其中键是要替换的占位符,值是要替换它的单词。以下是我的老式代码:

  private String replace(String text, Map<String, String> map) {
    for (Entry<String, String> entry : map.entrySet()) {
      text = text.replaceAll(entry.getKey(), entry.getValue());
    }
    return text;
  }

有没有办法将这段代码编写成Lambda表达式?
我尝试使用 entrySet().stream().map(...).reduce(...).apply(...); 但无法使其正常工作。
提前感谢。

需要注意的一件事是,在较大的占位符中包含短占位符。请确保您的占位符按长度排序,最长的排在前面。 - Ryan Leach
4个回答

10

我认为你不应该试图寻找更简单或更短的解决方案,而是要考虑你的方法的语义和效率。

你正在迭代一个可能没有指定迭代顺序的映射(如HashMap),并一次又一次地执行替换操作,使用替换的结果作为下一个输入,由于之前应用的替换或替换替换内容可能会导致错过匹配。

即使我们假设你正在传入一个键和值之间没有干扰的映射,这种方法也非常低效。请注意,replaceAll将把参数解释为正则表达式。

如果我们假设不打算使用正则表达式,则可以通过按长度排序键来消除键之间的歧义,以便首先尝试更长的键。那么,进行单个替换操作的解决方案可能如下:

private static String replace(String text, Map<String, String> map) {
    if(map.isEmpty()) return text;
    String pattern = map.keySet().stream()
        .sorted(Comparator.comparingInt(String::length).reversed())
        .map(Pattern::quote)
        .collect(Collectors.joining("|"));
    Matcher m = Pattern.compile(pattern).matcher(text);
    if(!m.find()) return text;
    StringBuffer sb = new StringBuffer();
    do m.appendReplacement(sb, Matcher.quoteReplacement(map.get(m.group())));
       while(m.find());
    return m.appendTail(sb).toString();
}

从Java 9开始,您可以在此处使用StringBuilder而不是StringBuffer

如果您进行测试,则为:

Map<String, String> map = new HashMap<>();
map.put("f", "F");
map.put("foo", "bar");
map.put("b", "B");
System.out.println(replace("foo, bar, baz", map));

你将得到

bar, Bar, Baz

演示了将foo替换为f时优先级高于将其替换为bar中的b不被替换。

如果您需要再次在替换结果内部进行替换,则需要控制顺序或实现一个重复替换,仅当没有匹配项时才返回。当然,后者需要注意提供始终会最终收敛到结果的替换。

例如:

private static String replaceRepeatedly(String text, Map<String, String> map) {
    if(map.isEmpty()) return text;
    String pattern = map.keySet().stream()
        .sorted(Comparator.comparingInt(String::length).reversed())
        .map(Pattern::quote)
        .collect(Collectors.joining("|"));
    Matcher m = Pattern.compile(pattern).matcher(text);
    if(!m.find()) return text;
    StringBuffer sb;
    do {
        sb = new StringBuffer();
        do m.appendReplacement(sb, Matcher.quoteReplacement(map.get(m.group())));
           while(m.find());
        m.appendTail(sb);
    } while(m.reset(sb).find());
    return sb.toString();
}
Map<String, String> map = new HashMap<>();
map.put("a", "e1");
map.put("e", "o2");
map.put("o", "x3");
System.out.println(replaceRepeatedly("foo, bar, baz", map));
fx3x3, bx321r, bx321z

3
@RavindraRanwala的代码有一些改进。
    String replacement = Stream.of(text.split("\\b"))
            .map(token -> map.getOrDefault(token, token))
            .collect(Collectors.joining(""));

1) 使用Java 8中的Map.getOrDefault

2) 使用"\b"进行分割,以支持单词的任意分隔符,而不仅限于空格字符


2
还有一种方法是 Pattern.compile("\\b").splitAsStream(text) - 这样可以重复使用预编译的 Pattern,避免创建数组。 - Hulk

2
您可以使用以下方法来使用 for each 循环:
  private String replace(String text, Map<String, String> map) {
      final StringBuilder sb = new StringBuilder(text);
      map.forEach((k,v)->replaceAll(sb, k, v));
      return sb.toString();
  }

Replace all方法可以定义为:

public static void replaceAll(StringBuilder builder, String from, String to){
    int index = builder.indexOf(from);
    while (index != -1)
    {
        builder.replace(index, index + from.length(), to);
        index += to.length(); // Move to the end of the replacement
        index = builder.indexOf(from, index);
    }
}

如果你正在使用多线程,你也可以使用StringBuffer


“text”是一个不可变的字符串,因此必须再次保存在变量“text”中,但这是不可能的,因为“text”不是最终的。 - Wilquina
看到这些编辑,你可以使用可变的StringBuffer。 - Kaushal28
@Kaushal28 StringBuffer 没有 replaceAll 方法。 - Nikolai Shevchenko

1

对于实际用途,你的非流代码就足够了。作为一个有趣的练习,你可以将每个映射表示为Function<String,String>并在函数上进行缩减:

Function<String,String> combined = map.entrySet().stream()
        .reduce(
                Function.identity(),
                (f, e) -> x -> f.apply(x).replaceAll(e.getKey(), e.getValue()),
                Function::andThen
        );

return combined.apply(text);

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