Java 8 Map Reduce на HashMap как лямбда

у меня есть 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;
  }

есть ли способ, чтобы написать этот код, как лямбда-выражения?

пробовал entrySet().stream().map(...).reduce(...).apply(...); но ничего не получилось.

спасибо заранее.

5 ответов


я не думаю, что вы должны пытаться найти более простое или короткое решение, а скорее думать о семантике и эффективности вашего подхода.

вы перебираете карты, которая, вероятно, не имеет указанного порядка итерации (например,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 и b в пределах его замены bar не заменен.

другое дело, если бы вы хочу заменить опять играм в замен. В этом случае вам понадобится либо механизм управления заказом, либо реализация повторной замены, которая вернется только тогда, когда совпадений больше нет. Конечно, последнее требует осторожности, чтобы обеспечить замены, которые всегда будут сходиться к результату в конечном итоге.

Э. Г.

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

вы можете использовать для каждого цикла следующим образом:

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

заменить весь метод можно определить как:

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 Если вы используете многопоточность.


несколько улучшений в коде @RavindraRanwala.

    String replacement = Stream.of(text.split("\b"))
            .map(token -> map.getOrDefault(token, token))
            .collect(Collectors.joining(""));

1) Использовать Map.getOrDefault из Java 8

2) разделить на "\b", чтобы поддержать любой разделитель слов, а не только пробел


вот гораздо более надежное решение, предполагая, что каждое слово разделено пробелом,

String replacement = Stream.of(source.split(" ")).map(token -> map.get(token) != null ? map.get(token) : token)
        .collect(Collectors.joining(" "));

для реальных целей ваш код без потока просто прекрасен. В качестве забавного упражнения вы можете представить каждое отображение как 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);