ChatGPT解决这个技术问题 Extra ChatGPT

如何以一个不会最终替换另一个字符串的方式替换两个字符串?

假设我有以下代码:

String word1 = "bar";
String word2 = "foo";
String story = "Once upon a time, there was a foo and a bar."
story = story.replace("foo", word1);
story = story.replace("bar", word2);

此代码运行后,story 的值将是 "Once upon a time, there was a foo and a foo."

如果我以相反的顺序替换它们,则会出现类似的问题:

String word1 = "bar";
String word2 = "foo";
String story = "Once upon a time, there was a foo and a bar."
story = story.replace("bar", word2);
story = story.replace("foo", word1);

story 的值为 "Once upon a time, there was a bar and a bar."

我的目标是将 story 变成 "Once upon a time, there was a bar and a foo." 我怎样才能做到这一点?

+1 肯定应该有一些函数 swap(String s1, String s2, String s3) 将所有出现的 s2s3 交换,反之亦然。
我们可以假设输入中每个可交换的词只出现一次吗?
极端情况:在“ababababababa”中交换“ab”和“ba”时,我们期望输出什么?
您在下面有一些很好的解决方案,但是您了解为什么您的方法不起作用吗?首先,你有“有一个 foo 和一个酒吧”。在第一次替换 ("foo"->"bar") 之后,你有“有一个 bar 和一个 bar”。您现在出现了 2 次“bar”,因此您的第二次替换没有达到您的预期 - 它无法知道您只想替换上次没有替换的那个。 @HagenvonEitzen 有趣。我希望一个可行的解决方案能够匹配并替换它找到的任一字符串中的第一个,然后从替换部分的末尾重复。
当我需要进行批量重命名时,Jeroen 的解决方案是我经常在文本编辑器中使用的解决方案。它简单,易于理解,不需要特殊的库,只需稍加思考就可以万无一失。

i
ivan_pozdeev

使用 Apache Commons StringUtils 中的 replaceEach() 方法:

StringUtils.replaceEach(story, new String[]{"foo", "bar"}, new String[]{"bar", "foo"})

知道 replaceEach 在内部到底做了什么吗?
@Marek很可能该函数会搜索并索引找到的每个项目,然后在它们全部被索引后替换它们。
您可以在第 4684 行附近找到此 here 的来源。
遗憾的是,当 null 被传递时,它是空操作。
C
Community

您使用中间值(句子中尚未出现)。

story = story.replace("foo", "lala");
story = story.replace("bar", "foo");
story = story.replace("lala", "bar");

作为对批评的回应:如果您使用足够大的不常见字符串,例如 zq515sqdqs5d5sq1dqs4d1q5dqqé"&é5d4sqjshsjddjhodfqsqc, nvùq^µù;d&€sdq: d: ;)àçàçlala 并使用它,那么我什至不会辩论它知道用户是否会输入此内容的唯一方法是了解源代码,此时您的担忧完全不同。

是的,也许有花哨的正则表达式方式。我更喜欢可读的东西,我知道也不会在我身上爆发。

还重申 @David Conrad in the comments 给出的出色建议:

不要巧妙地(愚蠢地)使用一些不太可能的字符串。使用来自 Unicode Private Use Area 的字符,U+E000..U+F8FF。首先删除任何此类字符,因为它们不应该合法地出现在输入中(它们仅在某些应用程序中具有特定于应用程序的含义),然后在替换时将它们用作占位符。


@arshajii 我想这取决于你对“更好”的定义......如果它有效并且性能可以接受,那么继续下一个编程任务并在重构期间稍后改进它将是我的方法。
显然“lala”只是一个例子。在生产中,您应该使用“zq515sqdqs5d5sq1dqs4d1q5dqqé”&é&€sdq:d:;)àçàçlala”。
不要巧妙地(愚蠢地)使用一些不太可能的字符串。使用来自 Unicode Private Use Area 的字符,U+E000..U+F8FF。首先删除任何此类字符,因为它们不应该合法地出现在输入中(它们仅在某些应用程序中具有特定于应用程序的含义),然后在替换时将它们用作占位符。
实际上,在阅读 Unicode FAQ on it 之后,我认为 U+FDD0..U+FDEF 范围内的非字符将是一个更好的选择。
@Taemyr 当然,但是必须有人清理输入,对吗?我希望字符串替换函数适用于所有字符串,但是这个函数会因不安全的输入而中断。
a
arshajii

您可以使用 Matcher#appendReplacementMatcher#appendTail 尝试类似的操作:

String word1 = "bar";
String word2 = "foo";
String story = "Once upon a time, there was a foo and a bar.";

Pattern p = Pattern.compile("foo|bar");
Matcher m = p.matcher(story);
StringBuffer sb = new StringBuffer();
while (m.find()) {
    /* do the swap... */
    switch (m.group()) {
    case "foo":
        m.appendReplacement(sb, word1);
        break;
    case "bar":
        m.appendReplacement(sb, word2);
        break;
    default:
        /* error */
        break;
    }
}
m.appendTail(sb);

System.out.println(sb.toString());
Once upon a time, there was a bar and a foo.

如果 foobarstory 都具有未知值,这是否有效?
@StephenP 我基本上硬编码了 OP 在他的代码中的 "foo""bar" 替换字符串,但是即使这些值不知道,相同类型的方法也可以正常工作(你必须使用if/else if 而不是 while 循环中的 switch)。
您必须小心创建正则表达式。 Pattern.quote\Q\E 会派上用场。
@arshajii - 是的,向我自己证明了它是一种将 word1、word2 和故事作为参数的“swapThese”方法。 +1
更简洁的方法是使用模式 (foo)|(bar) 然后检查 m.group(1) != null,以避免重复匹配的单词。
C
Community

这不是一个容易的问题。你拥有的搜索替换参数越多,它就越棘手。你有几个选择,分散在丑陋-优雅、高效-浪费的调色板上:

按照@AlanHay 的建议,使用来自 Apache Commons 的 StringUtils.replaceEach。如果您可以在项目中随意添加新的依赖项,这是一个不错的选择。您可能会很幸运:依赖项可能已经包含在您的项目中

使用@Jeroen 建议的临时占位符,并分两步执行替换: 用原始文本中不存在的唯一标签替换所有搜索模式 用真正的目标替换替换占位符 这不是一个好方法,因为几个原因:需要保证第一步使用的标签真的是唯一的;它执行了比实际需要更多的字符串替换操作

将所有搜索模式替换为原始文本中不存在的唯一标签

用真正的目标替换替换占位符

从所有模式构建一个正则表达式,并按照@arshajii 的建议使用带有 Matcher 和 StringBuffer 的方法。这并不可怕,但也不是那么好,因为构建正则表达式有点骇人听闻,而且它涉及到 StringBuffer,它在不久前已经过时,有利于 StringBuilder。

使用@mjolka 提出的递归解决方案,通过在匹配的模式处拆分字符串,并在剩余的段上递归。这是一个很好的解决方案,紧凑且非常优雅。它的弱点是潜在的许多子字符串和连接操作,以及适用于所有递归解决方案的堆栈大小限制

正如@msandiford 建议的那样,将文本拆分为单词并使用Java 8 流优雅地执行替换,但当然,只有在您可以在单词边界处拆分时才有效,这使得它不适合作为一般解决方案

这是我的版本,基于从 Apache's implementation 借来的想法。它既不简单也不优雅,但它可以工作,并且应该相对高效,没有不必要的步骤。简而言之,它的工作原理是这样的:在文本中反复查找下一个匹配的搜索模式,并使用 StringBuilder 来累积不匹配的段和替换。

public static String replaceEach(String text, String[] searchList, String[] replacementList) {
    // TODO: throw new IllegalArgumentException() if any param doesn't make sense
    //validateParams(text, searchList, replacementList);

    SearchTracker tracker = new SearchTracker(text, searchList, replacementList);
    if (!tracker.hasNextMatch(0)) {
        return text;
    }

    StringBuilder buf = new StringBuilder(text.length() * 2);
    int start = 0;

    do {
        SearchTracker.MatchInfo matchInfo = tracker.matchInfo;
        int textIndex = matchInfo.textIndex;
        String pattern = matchInfo.pattern;
        String replacement = matchInfo.replacement;

        buf.append(text.substring(start, textIndex));
        buf.append(replacement);

        start = textIndex + pattern.length();
    } while (tracker.hasNextMatch(start));

    return buf.append(text.substring(start)).toString();
}

private static class SearchTracker {

    private final String text;

    private final Map<String, String> patternToReplacement = new HashMap<>();
    private final Set<String> pendingPatterns = new HashSet<>();

    private MatchInfo matchInfo = null;

    private static class MatchInfo {
        private final String pattern;
        private final String replacement;
        private final int textIndex;

        private MatchInfo(String pattern, String replacement, int textIndex) {
            this.pattern = pattern;
            this.replacement = replacement;
            this.textIndex = textIndex;
        }
    }

    private SearchTracker(String text, String[] searchList, String[] replacementList) {
        this.text = text;
        for (int i = 0; i < searchList.length; ++i) {
            String pattern = searchList[i];
            patternToReplacement.put(pattern, replacementList[i]);
            pendingPatterns.add(pattern);
        }
    }

    boolean hasNextMatch(int start) {
        int textIndex = -1;
        String nextPattern = null;

        for (String pattern : new ArrayList<>(pendingPatterns)) {
            int matchIndex = text.indexOf(pattern, start);
            if (matchIndex == -1) {
                pendingPatterns.remove(pattern);
            } else {
                if (textIndex == -1 || matchIndex < textIndex) {
                    textIndex = matchIndex;
                    nextPattern = pattern;
                }
            }
        }

        if (nextPattern != null) {
            matchInfo = new MatchInfo(nextPattern, patternToReplacement.get(nextPattern), textIndex);
            return true;
        }
        return false;
    }
}

单元测试:

@Test
public void testSingleExact() {
    assertEquals("bar", StringUtils.replaceEach("foo", new String[]{"foo"}, new String[]{"bar"}));
}

@Test
public void testReplaceTwice() {
    assertEquals("barbar", StringUtils.replaceEach("foofoo", new String[]{"foo"}, new String[]{"bar"}));
}

@Test
public void testReplaceTwoPatterns() {
    assertEquals("barbaz", StringUtils.replaceEach("foobar",
            new String[]{"foo", "bar"},
            new String[]{"bar", "baz"}));
}

@Test
public void testReplaceNone() {
    assertEquals("foofoo", StringUtils.replaceEach("foofoo", new String[]{"x"}, new String[]{"bar"}));
}

@Test
public void testStory() {
    assertEquals("Once upon a foo, there was a bar and a baz, and another bar and a cat.",
            StringUtils.replaceEach("Once upon a baz, there was a foo and a bar, and another foo and a cat.",
                    new String[]{"foo", "bar", "baz"},
                    new String[]{"bar", "baz", "foo"})
    );
}

m
mjolka

搜索要替换的第一个单词。如果它在字符串中,则在出现之前的字符串部分上递归,在出现之后的字符串部分上递归。

否则,继续下一个要替换的单词。

一个天真的实现可能看起来像这样

public static String replaceAll(String input, String[] search, String[] replace) {
  return replaceAll(input, search, replace, 0);
}

private static String replaceAll(String input, String[] search, String[] replace, int i) {
  if (i == search.length) {
    return input;
  }
  int j = input.indexOf(search[i]);
  if (j == -1) {
    return replaceAll(input, search, replace, i + 1);
  }
  return replaceAll(input.substring(0, j), search, replace, i + 1) +
         replace[i] +
         replaceAll(input.substring(j + search[i].length()), search, replace, i);
}

示例用法:

String input = "Once upon a baz, there was a foo and a bar.";
String[] search = new String[] { "foo", "bar", "baz" };
String[] replace = new String[] { "bar", "baz", "foo" };
System.out.println(replaceAll(input, search, replace));

输出:

Once upon a foo, there was a bar and a baz.

一个不那么天真的版本:

public static String replaceAll(String input, String[] search, String[] replace) {
  StringBuilder sb = new StringBuilder();
  replaceAll(sb, input, 0, input.length(), search, replace, 0);
  return sb.toString();
}

private static void replaceAll(StringBuilder sb, String input, int start, int end, String[] search, String[] replace, int i) {
  while (i < search.length && start < end) {
    int j = indexOf(input, search[i], start, end);
    if (j == -1) {
      i++;
    } else {
      replaceAll(sb, input, start, j, search, replace, i + 1);
      sb.append(replace[i]);
      start = j + search[i].length();
    }
  }
  sb.append(input, start, end);
}

不幸的是,Java 的 String 没有 indexOf(String str, int fromIndex, int toIndex) 方法。我在这里省略了 indexOf 的实现,因为我不确定它是否正确,但可以在 ideone 上找到它,以及此处发布的各种解决方案的一些粗略时间安排。


尽管使用像 apache commons 这样的现有库无疑是解决这个相当普遍的问题的最简单方法,但您已经展示了一个适用于部分单词、运行时决定的单词并且不需要用魔术标记替换子字符串的实现,不像(目前)投票率较高的答案。 +1
很漂亮,但在提供 100 mb 的输入文件时会落地。
V
Vitalii Fedorenko

Java 8 中的单行代码:

    story = Pattern
        .compile(String.format("(?<=%1$s)|(?=%1$s)", "foo|bar"))
        .splitAsStream(story)
        .map(w -> ImmutableMap.of("bar", "foo", "foo", "bar").getOrDefault(w, w))
        .collect(Collectors.joining());

环视正则表达式 (?<=, ?=):http://www.regular-expressions.info/lookaround.html

如果单词可以包含特殊的正则表达式字符,请使用 Pattern.quote 转义它们。

为了简洁起见,我使用 guava ImmutableMap,但显然任何其他 Map 也可以完成这项工作。


c
clstrfsck

这是一个 Java 8 流的可能性,可能对某些人来说很有趣:

String word1 = "bar";
String word2 = "foo";

String story = "Once upon a time, there was a foo and a bar.";

// Map is from untranslated word to translated word
Map<String, String> wordMap = new HashMap<>();
wordMap.put(word1, word2);
wordMap.put(word2, word1);

// Split on word boundaries so we retain whitespace.
String translated = Arrays.stream(story.split("\\b"))
    .map(w -> wordMap.getOrDefault(w,  w))
    .collect(Collectors.joining());

System.out.println(translated);

这是 Java 7 中相同算法的近似值:

String word1 = "bar";
String word2 = "foo";
String story = "Once upon a time, there was a foo and a bar.";

// Map is from untranslated word to translated word
Map<String, String> wordMap = new HashMap<>();
wordMap.put(word1, word2);
wordMap.put(word2, word1);

// Split on word boundaries so we retain whitespace.
StringBuilder translated = new StringBuilder();
for (String w : story.split("\\b"))
{
  String tw = wordMap.get(w);
  translated.append(tw != null ? tw : w);
}

System.out.println(translated);

当您要替换的内容是由空格(或类似内容)分隔的实际单词时,这是一个很好的建议,但这不适用于替换单词的子字符串。
+1 用于 Java8 流。太糟糕了,这需要一个分隔符。
f
fastcodejava

如果您想替换句子中由空格分隔的单词,如示例中所示,您可以使用这个简单的算法。

在空白处拆分故事替换每个元素,如果 foo 将其替换为 bar 和副 varsa 将数组重新加入一个字符串

如果在空间上拆分是不可接受的,则可以遵循此替代算法。您需要先使用较长的字符串。如果字符串是foo和傻瓜,则需要先使用傻瓜,然后再使用foo。

拆分单词 foo 将 bar 替换为 foo 数组的每个元素加入该数组,在除最后一个元素之外的每个元素之后添加 bar


这也是我想建议的。虽然它增加了一个限制,即文本是用空格包围的单词。 :)
@MariusŽilėnas 我添加了一种替代算法。
W
WillingLearner

这是使用 Map 的一个不太复杂的答案。

private static String replaceEach(String str,Map<String, String> map) {

         Object[] keys = map.keySet().toArray();
         for(int x = 0 ; x < keys.length ; x ++ ) {
             str = str.replace((String) keys[x],"%"+x);
         }

         for(int x = 0 ; x < keys.length ; x ++) {
             str = str.replace("%"+x,map.get(keys[x]));
         }
         return str;
     }

并且方法被调用

Map<String, String> replaceStr = new HashMap<>();
replaceStr.put("Raffy","awesome");
replaceStr.put("awesome","Raffy");
String replaced = replaceEach("Raffy is awesome, awesome awesome is Raffy Raffy", replaceStr);

输出是:awesome is Raffy, Raffy Raffy is awesome awesome


在此之后运行 replaced.replaceAll("Raffy", "Barney"); 将使其成为传奇......等待它;达里!!!
v
ventsyv

如果您希望能够处理要替换的搜索字符串的多次出现,您可以通过在每个搜索词上拆分字符串然后替换它来轻松地做到这一点。这是一个例子:

String regex = word1 + "|" + word2;
String[] values = Pattern.compile(regex).split(story);

String result;
foreach subStr in values
{
   subStr = subStr.replace(word1, word2);
   subStr = subStr.replace(word2, word1);
   result += subStr;
}

L
Leonardo Braga

您可以使用以下代码块来实现您的目标:

String word1 = "bar";
String word2 = "foo";
String story = "Once upon a time, in a foo, there was a foo and a bar.";
story = String.format(story.replace(word1, "%1$s").replace(word2, "%2$s"),
    word2, word1);

无论顺序如何,它都会替换单词。您可以将此原则扩展为实用方法,例如:

private static String replace(String source, String[] targets, String[] replacements) throws IllegalArgumentException {
    if (source == null) {
        throw new IllegalArgumentException("The parameter \"source\" cannot be null.");
    }

    if (targets == null || replacements == null) {
        throw new IllegalArgumentException("Neither parameters \"targets\" or \"replacements\" can be null.");
    }

    if (targets.length == 0 || targets.length != replacements.length) {
        throw new IllegalArgumentException("The parameters \"targets\" and \"replacements\" must have at least one item and have the same length.");
    }

    String outputMask = source;
    for (int i = 0; i < targets.length; i++) {
        outputMask = outputMask.replace(targets[i], "%" + (i + 1) + "$s");
    }

    return String.format(outputMask, (Object[])replacements);
}

这将被消耗为:

String story = "Once upon a time, in a foo, there was a foo and a bar.";
story = replace(story, new String[] { "bar", "foo" },
    new String[] { "foo", "bar" }));

M
MarcG

这很有效并且很简单:

public String replaceBoth(String text, String token1, String token2) {            
    return text.replace(token1, "\ufdd0").replace(token2, token1).replace("\ufdd0", token2);
    }

你像这样使用它:

replaceBoth("Once upon a time, there was a foo and a bar.", "foo", "bar");

注意:这取决于不包含字符 \ufdd0 的字符串,该字符 永久 保留供 Unicode 内部使用(参见 http://www.unicode.org/faq/private_use.html):

我认为没有必要,但如果你想绝对安全,你可以使用:

public String replaceBoth(String text, String token1, String token2) {
    if (text.contains("\ufdd0") || token1.contains("\ufdd0") || token2.contains("\ufdd0")) throw new IllegalArgumentException("Invalid character.");
    return text.replace(token1, "\ufdd0").replace(token2, token1).replace("\ufdd0", token2);
    }

i
icza

仅交换一次出现

如果输入中每个可交换字符串只出现一次,您可以执行以下操作:

在进行任何替换之前,获取单词出现的索引。之后,我们只替换在这些索引中找到的单词,而不是所有出现的单词。此解决方案使用 StringBuilder 并且不会产生像 String.replace() 这样的中间 String

需要注意的一点:如果可交换的词具有不同的长度,则在第一个替换之后,第二个索引可能会发生变化(如果第一个词出现在第二个之前),恰好与 2 个长度的差异。因此,即使我们交换不同长度的单词,对齐第二个索引也将确保它有效。

public static String swap(String src, String s1, String s2) {
    StringBuilder sb = new StringBuilder(src);
    int i1 = src.indexOf(s1);
    int i2 = src.indexOf(s2);

    sb.replace(i1, i1 + s1.length(), s2); // Replace s1 with s2
    // If s1 was before s2, idx2 might have changed after the replace
    if (i1 < i2)
        i2 += s2.length() - s1.length();
    sb.replace(i2, i2 + s2.length(), s1); // Replace s2 with s1

    return sb.toString();
}

交换任意出现次数

与前一种情况类似,我们将首先收集单词的索引(出现次数),但在这种情况下,它将为每个单词提供一个整数列表,而不仅仅是一个 int。为此,我们将使用以下实用方法:

public static List<Integer> occurrences(String src, String s) {
    List<Integer> list = new ArrayList<>();
    for (int idx = 0;;)
        if ((idx = src.indexOf(s, idx)) >= 0) {
            list.add(idx);
            idx += s.length();
        } else
            return list;
}

使用它,我们将通过减少索引(这可能需要在两个可交换的单词之间交替)来替换另一个单词,这样我们甚至不必在替换后更正索引:

public static String swapAll(String src, String s1, String s2) {
    List<Integer> l1 = occurrences(src, s1), l2 = occurrences(src, s2);

    StringBuilder sb = new StringBuilder(src);

    // Replace occurrences by decreasing index, alternating between s1 and s2
    for (int i1 = l1.size() - 1, i2 = l2.size() - 1; i1 >= 0 || i2 >= 0;) {
        int idx1 = i1 < 0 ? -1 : l1.get(i1);
        int idx2 = i2 < 0 ? -1 : l2.get(i2);
        if (idx1 > idx2) { // Replace s1 with s2
            sb.replace(idx1, idx1 + s1.length(), s2);
            i1--;
        } else { // Replace s2 with s1
            sb.replace(idx2, idx2 + s2.length(), s1);
            i2--;
        }
    }

    return sb.toString();
}

我不确定java如何处理unicode,但是这个代码的C#等价物是不正确的。问题在于,由于 unicode 字符串等价的特性,indexOf 匹配的子字符串的长度可能与搜索字符串的长度不同。
@CodesInChaos 它在 Java 中完美运行,因为 Java String 是字符数组而不是字节数组。 StringStringBuilder 的所有方法都对字符而不是字节进行操作,它们是“无编码”的。因此 indexOf 匹配具有与搜索字符串完全相同的(字符)长度。
在 C# 和 java 中,字符串都是 UTF-16 代码单元的序列。问题是存在 unicode 认为等效的不同代码点序列。例如,ä 可以编码为单个代码点,也可以编码为 a,然后是组合 ¨。还有一些代码点被忽略,例如零宽度(非)连接符。字符串是否由字节、字符或其他组成无关紧要,但使用哪种比较规则 indexOf 并不重要。它可能通过代码单元比较(“Ordinal”)简单地使用代码单元,或者它可能实现 unicode 等价。我不知道选择了哪一个java。
例如,"ab\u00ADc".IndexOf("bc") 在 .net 中返回 1,将两个字符串 bc 匹配为三个字符串。
@CodesInChaos 我现在明白你的意思了。在 Java 中,"ab\u00ADc".indexOf("bc") 返回 -1,这意味着在 "ab\u00ADc" 中找不到 "bc"。因此,在 Java 中,上述算法仍然有效,indexOf() 匹配具有与搜索字符串完全相同的(字符)长度,并且 indexOf() 仅在字符序列(代码点)匹配时才报告匹配。
B
Boann

使用 String.regionMatches 很容易编写一个方法来执行此操作:

public static String simultaneousReplace(String subject, String... pairs) {
    if (pairs.length % 2 != 0) throw new IllegalArgumentException(
        "Strings to find and replace are not paired.");
    StringBuilder sb = new StringBuilder();
    outer:
    for (int i = 0; i < subject.length(); i++) {
        for (int j = 0; j < pairs.length; j += 2) {
            String find = pairs[j];
            if (subject.regionMatches(i, find, 0, find.length())) {
                sb.append(pairs[j + 1]);
                i += find.length() - 1;
                continue outer;
            }
        }
        sb.append(subject.charAt(i));
    }
    return sb.toString();
}

测试:

String s = "There are three cats and two dogs.";
s = simultaneousReplace(s,
    "cats", "dogs",
    "dogs", "budgies");
System.out.println(s);

输出:

有三只狗和两只虎皮鹦鹉。

这不是很明显,但是像这样的函数仍然可以依赖于指定替换的顺序。考虑:

String truth = "Java is to JavaScript";
truth += " as " + simultaneousReplace(truth,
    "JavaScript", "Hamster",
    "Java", "Ham");
System.out.println(truth);

输出:

Java 之于 JavaScript 就像 Ham 之于仓鼠

但是反转替换:

truth += " as " + simultaneousReplace(truth,
    "Java", "Ham",
    "JavaScript", "Hamster");

输出:

Java 之于 JavaScript 就像 Ham 之于 HamScript

哎呀! :)

因此,确保查找 最长 匹配有时很有用(例如,PHP 的 strtr 函数就是这样做的)。此版本的方法将执行此操作:

public static String simultaneousReplace(String subject, String... pairs) {
    if (pairs.length % 2 != 0) throw new IllegalArgumentException(
        "Strings to find and replace are not paired.");
    StringBuilder sb = new StringBuilder();
    for (int i = 0; i < subject.length(); i++) {
        int longestMatchIndex = -1;
        int longestMatchLength = -1;
        for (int j = 0; j < pairs.length; j += 2) {
            String find = pairs[j];
            if (subject.regionMatches(i, find, 0, find.length())) {
                if (find.length() > longestMatchLength) {
                    longestMatchIndex = j;
                    longestMatchLength = find.length();
                }
            }
        }
        if (longestMatchIndex >= 0) {
            sb.append(pairs[longestMatchIndex + 1]);
            i += longestMatchLength - 1;
        } else {
            sb.append(subject.charAt(i));
        }
    }
    return sb.toString();
}

请注意,上述方法区分大小写。如果您需要不区分大小写的版本,则可以轻松修改上述内容,因为 String.regionMatches 可以采用 ignoreCase 参数。


P
Pier-Alexandre Bouchard

如果您不想要任何依赖项,您可以简单地使用一个只允许一次性更改的数组。这不是最有效的解决方案,但它应该可以工作。

public String replace(String sentence, String[]... replace){
    String[] words = sentence.split("\\s+");
    int[] lock = new int[words.length];
    StringBuilder out = new StringBuilder();

    for (int i = 0; i < words.length; i++) {
        for(String[] r : replace){
            if(words[i].contains(r[0]) && lock[i] == 0){
                words[i] = words[i].replace(r[0], r[1]);
                lock[i] = 1;
            }
        }

        out.append((i < (words.length - 1) ? words[i] + " " : words[i]));
    }

    return out.toString();
}

然后,它应该工作。

String story = "Once upon a time, there was a foo and a bar.";

String[] a = {"foo", "bar"};
String[] b = {"bar", "foo"};
String[] c = {"there", "Pocahontas"};
story = replace(story, a, b, c);

System.out.println(story); // Once upon a time, Pocahontas was a bar and a foo.

S
Salman A

您正在对输入执行多个搜索替换操作。当替换字符串包含搜索字符串时,这将产生不希望的结果。考虑 foo->bar, bar-foo 的例子,这里是每次迭代的结果:

曾几何时,有一个 foo 和一个酒吧。 (输入)从前,有一个酒吧和一个酒吧。 (foo->bar) 从前,有一个 foo 和一个 foo。 (bar->foo, 输出)

您需要在一次迭代中执行替换而不返回。蛮力解决方案如下:

在输入中从当前位置到结尾搜索多个搜索字符串,直到找到匹配项 将匹配的搜索字符串替换为对应的替换字符串 将当前位置设置为替换字符串之后的下一个字符 重复

String.indexOfAny(String[]) -> int[]{index, whichString} 之类的函数会很有用。这是一个示例(不是最有效的示例):

private static String replaceEach(String str, String[] searchWords, String[] replaceWords) {
    String ret = "";
    while (str.length() > 0) {
        int i;
        for (i = 0; i < searchWords.length; i++) {
            String search = searchWords[i];
            String replace = replaceWords[i];
            if (str.startsWith(search)) {
                ret += replace;
                str = str.substring(search.length());
                break;
            }
        }
        if (i == searchWords.length) {
            ret += str.substring(0, 1);
            str = str.substring(1);
        }
    }
    return ret;
}

一些测试:

System.out.println(replaceEach(
    "Once upon a time, there was a foo and a bar.",
    new String[]{"foo", "bar"},
    new String[]{"bar", "foo"}
));
// Once upon a time, there was a bar and a foo.

System.out.println(replaceEach(
    "a p",
    new String[]{"a", "p"},
    new String[]{"apple", "pear"}
));
// apple pear

System.out.println(replaceEach(
    "ABCDE",
    new String[]{"A", "B", "C", "D", "E"},
    new String[]{"B", "C", "E", "E", "F"}
));
// BCEEF

System.out.println(replaceEach(
    "ABCDEF",
    new String[]{"ABCDEF", "ABC", "DEF"},
    new String[]{"XXXXXX", "YYY", "ZZZ"}
));
// XXXXXX
// note the order of search strings, longer strings should be placed first 
// in order to make the replacement greedy

Demo on IDEONE
Demo on IDEONE, alternate code


P
Pokechu22

您始终可以将其替换为您确定不会出现在字符串中其他位置的单词,然后稍后再进行第二次替换:

String word1 = "bar";
String word2 = "foo";
String story = "Once upon a time, there was a foo and a bar."
story = story.replace("foo", "StringYouAreSureWillNeverOccur").replace("bar", "word2").replace("StringYouAreSureWillNeverOccur", "word1");

请注意,如果确实发生了 "StringYouAreSureWillNeverOccur",这将无法正常工作。


使用来自 Unicode Private Use Area 的字符,U+E000..U+F8FF,创建一个 StringThatCannotEverOccur。您可以预先将它们过滤掉,因为它们不应该存在于输入中。
或 U+FDD0..U+FDEF,即“非字符”,保留供内部使用。
I
Imheroldman

考虑使用 StringBuilder

然后存储每个字符串应该开始的索引。如果您在每个位置使用占位符字符,则将其删除,然后插入用户字符串。然后,您可以通过将字符串长度添加到开始位置来映射结束位置。

String firstString = "???";
String secondString  = "???"

StringBuilder story = new StringBuilder("One upon a time, there was a " 
    + firstString
    + " and a "
    + secondString);

int  firstWord = 30;
int  secondWord = firstWord + firstString.length() + 7;

story.replace(firstWord, firstWord + firstString.length(), userStringOne);
story.replace(secondWord, secondWord + secondString.length(), userStringTwo);

firstString = userStringOne;
secondString = userStringTwo;

return story;

L
Leonel Sarmiento

我只能分享的是我自己的方法。

您可以使用临时的 String temp = "<?>";String.Format();

这是我在控制台应用程序中通过 -“Idea Only,Not Exact Answer” 创建的示例代码。

static void Main(string[] args)
    {
        String[] word1 = {"foo", "Once"};
        String[] word2 = {"bar", "time"};
        String story = "Once upon a time, there was a foo and a bar.";

        story = Switcher(story,word1,word2);
        Console.WriteLine(story);
        Console.Read();
    }
    // Using a temporary string.
    static string Switcher(string text, string[] target, string[] value)
    {
        string temp = "<?>";
        if (target.Length == value.Length)
        {
            for (int i = 0; i < target.Length; i++)
            {
                text = text.Replace(target[i], temp);
                text = text.Replace(value[i], target[i]);
                text = text.Replace(temp, value[i]);
            }
        }
        return text;
    }

或者您也可以使用 String.Format();

static string Switcher(string text, string[] target, string[] value)
        {
            if (target.Length == value.Length)
            {
                for (int i = 0; i < target.Length; i++)
                {
                    text = text.Replace(target[i], "{0}").Replace(value[i], "{1}");
                    text = String.Format(text, value[i], target[i]);
                }
            }
            return text;
        }

输出: time upon a Once, there was a bar and a foo.


这很hacky。如果他想替换“_”,你会怎么做?
@Pier-AlexandreBouchard 在方法中,我将 temp 的值从 "_" 更改为 <?>。但是如果需要,他可以做的是在方法中添加另一个参数来改变温度。 -“最好保持简单对吧?”
我的观点是,你不能保证预期的结果,因为如果 temp == 替换,你的方式将行不通。
K
Khaled.K

这是我的版本,它是基于单词的:

class TextReplace
{

    public static void replaceAll (String text, String [] lookup,
                                   String [] replacement, String delimiter)
    {

        String [] words = text.split(delimiter);

        for (int i = 0; i < words.length; i++)
        {

            int j = find(lookup, words[i]);

            if (j >= 0) words[i] = replacement[j];

        }

        text = StringUtils.join(words, delimiter);

    }

    public static  int find (String [] array, String key)
    {

        for (int i = 0; i < array.length; i++)
            if (array[i].equals(key))
                return i;

        return (-1);

    }

}

K
Key_coder
String word1 = "bar";
String word2 = "foo";
String story = "Once upon a time, there was a foo and a bar."

有点棘手的方法,但你需要做更多的检查。

1.将字符串转换为字符数组

   String temp[] = story.split(" ");//assume there is only spaces.

2.在 temp 上循环并将 foo 替换为 bar 并将 bar 替换为 foo,因为没有机会再次获得可替换的字符串。


E
Elvis Lima

嗯,简短的答案是......

String word1 = "bar";
String word2 = "foo";
String story = "Once upon a time, there was a foo and a bar.";
story = story.replace("foo", "@"+ word1).replace("bar", word2).replace("@" + word2, word1);
System.out.println(story);

C
Community

使用找到的答案 here,您可以找到您希望替换的所有字符串。

因此,例如,您运行上述 SO 答案中的代码。创建两个索引表(假设 bar 和 foo 在您的字符串中不只出现一次),您可以使用这些表在您的字符串中替换它们。

现在要替换特定索引位置,您可以使用:

public static String replaceStringAt(String s, int pos, String c) {
   return s.substring(0,pos) + c + s.substring(pos+1);
}

pos 是您的字符串开始的索引(来自我上面引用的索引表)。因此,假设您为每一个创建了两个索引表。我们称它们为 indexBarindexFoo

现在在替换它们时,您可以简单地运行两个循环,一个用于您希望进行的替换。

for(int i=0;i<indexBar.Count();i++)
replaceStringAt(originalString,indexBar[i],newString);

同样,indexFoo 的另一个循环。

这可能不如这里的其他答案那么有效,但它比地图或其他东西更容易理解。

这将始终为您提供您想要的结果以及每个字符串可能多次出现的情况。只要你存储每次出现的索引。

这个答案也不需要递归,也不需要任何外部依赖。就复杂性而言,它可能是 O(n squared),而 n 是两个单词出现次数的总和。


P
Pokechu22

我开发的这段代码将解决问题:

public static String change(String s,String s1, String s2) {
   int length = s.length();
   int x1 = s1.length();
   int x2 = s2.length();
   int x12 = s.indexOf(s1);
   int x22 = s.indexOf(s2);
   String s3=s.substring(0, x12);
   String s4 =s.substring(x12+3, x22);
   s=s3+s2+s4+s1;
   return s;
}

在主要使用change(story,word2,word1).


仅当每个字符串都出现一次时才有效
A
Amir Saniyan
String word1 = "bar";
String word2 = "foo";

String story = "Once upon a time, there was a foo and a bar."

story = story.replace("foo", "<foo />");
story = story.replace("bar", "<bar />");

story = story.replace("<foo />", word1);
story = story.replace("<bar />", word2);