连接字符串的最有效方法

Most efficient way to concatenate Strings

我的印象是 StringBuffer 是连接字符串的最快方法,但我看到了 this Stack Overflow post saying that concat() is the fastest method. I tried the 2 given examples in Java 1.5, 1.6 and 1.7 but I never got the results they did. My results are almost identical to this

  1. 谁能解释一下我在这里不明白的地方?在 Java 中连接字符串的真正最快的方法是什么?

  2. 当寻求连接两个字符串的最快方法和连接多个字符串时是否有不同的答案?

我从别人的回答中了解到的内容如下:

如果需要线程安全,使用StringBuffer

如果不需要线程安全:

如果字符串是事先已知的,并且由于某些原因需要多次使用相同的代码 运行,请使用“+”,因为编译器会在编译时本身对其进行优化和处理。

如果只需要连接两个字符串,请使用 concat(),因为它不需要创建 StringBuilder/StringBuffer 个对象。感谢@nickb

如果需要连接多个字符串,请使用 StringBuilder。

如果连接 两个 字符串,

String.concat+ 运算符快......虽然这可以随时修复,甚至可能据我所知,已在 java 8 中修复。

您在引用的第一个 post 中遗漏的是作者正在准确连接 两个 字符串,而快速方法是其中大小新的字符数组预先计算为str1.length() + str2.length(),所以底层字符数组只需要分配一次。

在不指定最终大小的情况下使用 StringBuilder(),这也是 + 内部工作的方式,通常需要对底层数组进行更多分配和复制。

如果您需要将一堆字符串连接在一起,那么您应该使用 StringBuilder。如果可行,则预先计算最终大小,以便底层数组只需要分配一次。

加入很长的列表 os 字符串,从头到尾天真地添加它们非常慢:填充的缓冲区逐渐增长,并一次又一次地重新分配,制作额外的副本(并征求很多垃圾收集器)。

连接长列表的 most 有效方法是始终从连接相邻字符串对开始,其中 ose 总长度是所有其他候选对中最小的;然而,这将需要复杂的查找来找到最佳对(类似于众所周知的河内塔问题),并且发现它只是为了将副本数量减少到严格的最小值会减慢速度。

您需要一个使用“分而治之”递归算法的智能算法,该算法具有良好的启发式算法,非常接近此最优值:

  1. 如果您没有要加入的字符串,return空字符串。
  2. 如果您只有 1 个字符串要加入,只需 return 即可。
  3. 否则,如果您只有 2 个字符串要连接,请连接它们并 return 结果。
  4. 计算最终结果的总长度。
  5. 然后确定从左开始连接的字符串数,直到达到总数的一半,以确定将字符串集分成两个非空部分的“划分”点(每个部分必须至少包含 1 个字符串, 分割点不能是 te set to join 中的第一个或最后一个字符串)。
  6. 如果至少有 2 个字符串要连接,则连接最小的部分,否则连接另一部分(递归使用此算法)。
  7. 循环回到开头 (1.) 以完成其他连接。

请注意,必须忽略集合中的空字符串,就好像它们不是集合的一部分一样。

在各种库中发现的 String.join(table of string, optional separator) 的许多默认实现都很慢,因为它们使用从左到右的简单增量连接;当您需要连接许多小字符串以生成非常大的字符串时,上面的分而治之算法将胜过它。

这种情况并不例外,它发生在文本预处理器和生成器中,或者在HTML处理中(例如在“Element.getInnerText()”中,当元素是包含许多文本元素的大文档时由许多命名元素分隔或包含)。

当源字符串全部(或 almost 全部被垃圾收集以仅保留最终结果时,上述策略有效。如果结果与源字符串列表一样长, 最好的选择是只为结果的总长度分配一次结果的最终大缓冲区,然后从左到右复制源字符串。

在这两种情况下,这都需要首先遍历所有字符串以计算它们的总长度。

如果您使用可重新分配的“字符串缓冲区”,如果“字符串缓冲区”不断重新分配,这将无法正常工作。但是,字符串缓冲区在执行第一遍时可能很有用,可以预先加入一些可以放入其中的短字符串,具有合理的(中等)大小(例如,一页内存为 4KB):一旦满了,替换子集字符串缓冲区的内容,并分配一个新的。

这可以大大减少源集中的小字符串的数量,并且在第一遍之后,您有最终缓冲区的总长度来分配给结果,您将在其中逐步复制所有剩余的介质-size strings collected in the first pass 这在源字符串列表来自解析器函数或生成器时非常有效,其中总长度在 parsing/generation 结束之前不完全已知:您将仅使用中间值中等大小的字符串缓冲区,最后您将生成最终缓冲区,而无需再次重新解析输入(以获取许多增量片段)或无需重复调用生成器(这会很慢或对某些生成器不起作用,或者如果输入解析器已消耗且无法从一开始就恢复)。

请注意,此评论不仅适用于 joinind 字符串,还适用于文件 I/O:增量写入文件也会受到重新分配或碎片的影响:您应该能够预先计算文件的总最终长度生成的文件。否则你需要一个分类缓冲区(在 most file I/O 库中实现,通常在内存中大小约为 4KB 的一个内存页,但你应该分配更多,因为 file I/O 是相当慢,当文件碎片由一个“集群”的太小单元增量分配时,碎片成为以后文件访问的性能问题;使用大约 1MB 的缓冲区将避免由碎片引起的 most 性能问题由于碎片在文件系统上的分配将相当大;像 NTFS 这样的文件系统经过优化以支持高达 64MB 的碎片,超过 64MB 碎片不再是一个明显的问题;Unix/Linux 文件系统也是如此,它会撕裂到仅对最大片段大小进行碎片整理,并且可以使用按 1 个簇、2 个簇、4 个簇、8 个簇的最小大小组织的空闲簇“池”有效地处理小片段的分配......以 2 的幂,以便对这些池进行碎片整理是直接的tforward 而不是很 costly,并且可以在 I/O activity).

的低级别时在后台异步完成

在所有现代操作系统中,内存管理与磁盘存储管理相关,使用内存映射文件处理缓存:内存由存储支持,由虚拟内存管理器管理(这意味着您可以分配更多动态内存比你有物理 RAM,其余的将在需要时调出到磁盘):你用于管理非常大缓冲区的 RAM 的策略往往与 I/O 的分页性能相关:使用内存映射文件是一个很好的解决方案,所有与文件 I/O 一起工作的事情现在都可以在非常大的(虚拟)内存中完成。