简体   繁体   中英

Java 8 Map Reduce on HashMap as lambda

I have a String and want to replace some words within. I have a HashMap where the key is the placeholder to be replaced and the value the word to replace it. Here is my old-school code:

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

Is there a way to write this code as a lambda expression?

I tried entrySet().stream().map(...).reduce(...).apply(...); but couldn't make it work.

Thanks in advance.

I don't think that you should try to find a simpler or shorter solution, but rather think about the semantics and efficiency of your approach.

You are iterating over a map which likely has no specified iteration order (like HashMap ) and performing one replacement after another, using the result of a replacement as input to the next, potentially missing matches due to previously applied replaces or replacing contents within the replacements.

Even if we assume that you are passing in a map whose keys and values have no interference, this approach is very inefficient. Note further that replaceAll will interpret the arguments as regular expressions.

If we assume that no regular expressions are intended, we can clear ambiguities between keys by sorting them by length, so that longer keys are attempted first. Then, a solution doing a single replacement operation may look like:

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

starting with Java 9, you can use StringBuilder instead of StringBuffer here

If you test it with

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

you'll get

bar, Bar, Baz

demonstrating that replacing foo gets precedence over replacing f and the b within its replacement bar is not replaced.

A different thing would be if you want to replace matches within replacements again. In that case, you would need either, a mechanism to control the order or implement a repeated replacement that will only return when there are no matches anymore. Of course, the latter requires care to provide replacements which always will converge to a result eventually.

Eg

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

You can use for each loop as following:

  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 method can be defined as:

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

You can also use StringBuffer if you are using multi threading.

Few improvements to @RavindraRanwala 's code.

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

1) Use Map.getOrDefault from Java 8

2) Split by "\\b" to support any delimiter of the words, not only space character

Here's much more robust solution, assuming each word is separated by a space character,

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

For real-life purposes, your non-stream code is just fine. As a fun exercise, you can represent each mapping as a Function<String,String> and reduce on the functions:

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

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM