陷阱 - 循环中的字符串连接不会缩放
请考虑以下代码作为说明:
public String joinWords(List<String> words) {
String message = "";
for (String word : words) {
message = message + " " + word;
}
return message;
}
不幸的是,如果 words
列表很长,这段代码效率很低。问题的根源是这句话:
message = message + " " + word;
对于每个循环迭代,此语句创建一个新的 message
字符串,其中包含原始 message
字符串中所有字符的副本,并附加了额外的字符。这会生成许多临时字符串,并进行大量复制。
当我们分析 joinWords
时,假设有 N 个平均长度为 M 的单词,我们发现创建了 O(N)
个临时字符串,并且在此过程中将复制 O(MN 2 )个字符。N 2 组分特别麻烦。
针对此类问题 1 的推荐方法是使用 StringBuilder
而不是字符串连接,如下所示:
public String joinWords2(List<String> words) {
StringBuilder message = new StringBuilder();
for (String word : words) {
message.append(" ").append(word);
}
return message.toString();
}
对 joinWords2
的分析需要考虑生长保存构建器角色的 StringBuilder
后备阵列的开销。但是,事实证明,创建的新对象的数量是 O(logN)
,并且复制的字符数是 O(MN)
字符。后者包括在最后的 toString()
调用中复制的字符。
(可以通过创建具有正确容量的 StringBuilder
来进一步调整它。但是,整体复杂性保持不变。)
回到最初的 joinWords
方法,事实证明,关键语句将由典型的 Java 编译器优化为:
StringBuilder tmp = new StringBuilder();
tmp.append(message).append(" ").append(word);
message = tmp.toString();
但是,Java 编译器不会将 StringBuilder
提升到循环之外,正如我们在 joinWords2
的代码中所做的那样。
参考:
1 - 在 Java 8 及更高版本中,Joiner
类可用于解决此特定问题。然而,这不是这个例子真正应该是什么。