234

我们必须一直为日志输出等构建字符串。在 JDK 版本中,我们了解了何时使用StringBuffer(许多附加,线程安全)和StringBuilder(许多附加,非线程安全)。

使用上有什么建议String.format()?它是有效的,还是我们被迫坚持对性能很重要的单行代码进行串联?

例如丑陋的旧风格,

String s = "What do you get if you multiply " + varSix + " by " + varNine + "?";

与整洁的新样式(String.format,可能更慢)相比,

String s = String.format("What do you get if you multiply %d by %d?", varSix, varNine);

注意:我的具体用例是贯穿我的代码的数百个“单行”日志字符串。它们不涉及循环,所以StringBuilder太重了。我String.format()特别感兴趣。

4

13 回答 13

249

我采用了hhafez代码并添加了内存测试

private static void test() {
    Runtime runtime = Runtime.getRuntime();
    long memory;
    ...
    memory = runtime.freeMemory();
    // for loop code
    memory = memory-runtime.freeMemory();

我为每种方法分别运行它,'+' 运算符、String.format 和 StringBuilder(调用 toString()),因此使用的内存不会受到其他方法的影响。我添加了更多连接,使字符串为“Blah”+ i +“Blah”+ i +“Blah”+ i +“Blah”。

结果如下(平均每次运行 5 次):
接近时间(毫秒) 分配的内存(长)
'+' 运算符 747 320,504
String.format 16484 373,312
StringBuilder 769 57,344

我们可以看到 String '+' 和 StringBuilder 在时间上实际上是相同的,但是 StringBuilder 在内存使用方面要高效得多。当我们在足够短的时间间隔内有许多日志调用(或任何其他涉及字符串的语句)时,这一点非常重要,因此垃圾收集器不会清理由“+”运算符产生的许多字符串实例。

顺便说一句,不要忘记在构建消息之前检查日志记录级别。

结论:

  1. 我将继续使用 StringBuilder。
  2. 我有太多的时间或太少的生活。
于 2009-08-15T11:03:05.757 回答
134

我写了一个小类来测试两者的性能更好,并且 + 领先于格式。乘以 5 到 6 倍。自己尝试一下

import java.io.*;
import java.util.Date;

public class StringTest{

    public static void main( String[] args ){
    int i = 0;
    long prev_time = System.currentTimeMillis();
    long time;

    for( i = 0; i< 100000; i++){
        String s = "Blah" + i + "Blah";
    }
    time = System.currentTimeMillis() - prev_time;

    System.out.println("Time after for loop " + time);

    prev_time = System.currentTimeMillis();
    for( i = 0; i<100000; i++){
        String s = String.format("Blah %d Blah", i);
    }
    time = System.currentTimeMillis() - prev_time;
    System.out.println("Time after for loop " + time);

    }
}

对不同的 N 运行上述代码表明两者的行为都是线性的,但String.format速度要慢 5-30 倍。

原因是在当前的实现中,String.format首先用正则表达式解析输入,然后再填充参数。另一方面,与 plus 的连接由 javac(而不是 JIT)优化并StringBuilder.append直接使用。

运行时比较

于 2009-02-04T22:35:34.883 回答
33

这里提供的所有基准都有一些缺陷,因此结果不可靠。

我很惊讶没有人使用JMH进行基准测试,所以我这样做了。

结果:

Benchmark             Mode  Cnt     Score     Error  Units
MyBenchmark.testOld  thrpt   20  9645.834 ± 238.165  ops/s  // using +
MyBenchmark.testNew  thrpt   20   429.898 ±  10.551  ops/s  // using String.format

单位是每秒的操作次数,越多越好。基准源代码。使用 OpenJDK IcedTea 2.5.4 Java 虚拟机。

因此,旧样式(使用 +)要快得多。

于 2015-06-25T21:52:34.897 回答
21

JAVAC 1.6 自动将您的旧丑陋样式编译为:

StringBuilder sb = new StringBuilder("What do you get if you multiply ");
sb.append(varSix);
sb.append(" by ");
sb.append(varNine);
sb.append("?");
String s =  sb.toString();

所以这和使用 StringBuilder 完全没有区别。

String.format 的重量级要大得多,因为它创建了一个新的 Formatter,解析您的输入格式字符串,创建一个 StringBuilder,将所有内容附加到它并调用 toString()。

于 2011-06-29T14:34:33.080 回答
12

Java 的 String.format 是这样工作的:

  1. 它解析格式字符串,分解成格式块列表
  2. 它迭代格式块,渲染成 StringBuilder,它基本上是一个数组,可以根据需要调整自身大小,方法是复制到一个新数组中。这是必要的,因为我们还不知道分配最终 String 的大小
  3. StringBuilder.toString() 将他的内部缓冲区复制到一个新的字符串中

如果此数据的最终目的地是流(例如呈现网页或写入文件),您可以将格式块直接组装到流中:

new PrintStream(outputStream, autoFlush, encoding).format("hello {0}", "world");

我推测优化器会优化掉格式字符串的处理。如果是这样,您将获得与手动展开 String.format 到 StringBuilder相当的摊销性能。

于 2011-08-20T03:36:46.470 回答
8

要扩展/纠正上面的第一个答案,实际上 String.format 不会帮助翻译。
当您打印日期/时间(或数字格式等)时,String.format 将有所帮助,其中存在本地化(l10n)差异(即,某些国家/地区将打印 04Feb2009,而其他国家/地区将打印 Feb042009)。
通过翻译,您只是在谈论将任何可外部化的字符串(例如错误消息和诸如此类的东西)移动到属性包中,以便您可以使用 ResourceBundle 和 MessageFormat 为正确的语言使用正确的包。

综观以上所有内容,我会说性能方面,String.format 与普通连接归结为您喜欢的内容。如果您更喜欢查看对 .format 的调用而不是串联,那么请务必使用它。
毕竟,阅读的代码比编写的要多得多。

于 2009-02-05T00:09:56.517 回答
7

在您的示例中,性能概率并没有太大差异,但还有其他问题需要考虑:即内存碎片。即使是连接操作也在创建一个新字符串,即使它是临时的(GC 它需要时间,而且工作量更大)。String.format() 更具可读性,并且涉及更少的碎片。

此外,如果您经常使用特定格式,请不要忘记您可以直接使用 Formatter() 类(所有 String.format() 所做的就是实例化一个一次性使用的 Formatter 实例)。

此外,您应该注意的其他事项:小心使用 substring()。例如:

String getSmallString() {
  String largeString = // load from file; say 2M in size
  return largeString.substring(100, 300);
}

那个大字符串仍在内存中,因为这正是 Java 子字符串的工作方式。更好的版本是:

  return new String(largeString.substring(100, 300));

或者

  return String.format("%s", largeString.substring(100, 300));

如果您同时做其他事情,第二种形式可能更有用。

于 2009-02-04T22:18:24.153 回答
5

通常,您应该使用 String.Format,因为它相对较快并且支持全球化(假设您实际上是在尝试编写用户可以阅读的内容)。如果您尝试翻译一个字符串而不是每个语句 3 个或更多(尤其是对于语法结构截然不同的语言),它还可以更容易地进行全球化。

现在,如果您从不打算翻译任何内容,那么要么依赖 Java 内置的 + 运算符转换为StringBuilder. StringBuilder或者明确地使用 Java 。

于 2009-02-04T22:14:41.587 回答
3

仅从 Logging 的角度来看的另一个角度。

我看到很多与登录此线程相关的讨论,所以想在回答中添加我的经验。可能有人会觉得它有用。

我猜想使用格式化程序进行日志记录的动机来自于避免字符串连接。基本上,如果您不打算记录它,您不希望有字符串 concat 的开销。

除非您想记录,否则您实际上并不需要连接/格式化。假设我定义了这样的方法

public void logDebug(String... args, Throwable t) {
    if(debugOn) {
       // call concat methods for all args
       //log the final debug message
    }
}

在这种方法中,如果它是调试消息并且 debugOn = false,则根本不会真正调用 cancat/formatter

尽管在这里使用 StringBuilder 而不是格式化程序会更好。主要动机是避免任何这些。

同时我不喜欢为每个日志语句添加“if”块,因为

  • 影响可读性
  • 减少我的单元测试的覆盖率——当你想确保每一行都经过测试时,这会让人感到困惑。

因此,我更喜欢使用上述方法创建一个日志实用程序类,并在任何地方使用它,而不用担心性能损失和任何其他相关问题。

于 2015-05-21T20:05:28.817 回答
2

我刚刚修改了 hhafez 的测试以包含 StringBuilder。StringBuilder 在 XP 上使用 jdk 1.6.0_10 客户端比 String.format 快 33 倍。使用 -server 开关将系数降低到 20。

public class StringTest {

   public static void main( String[] args ) {
      test();
      test();
   }

   private static void test() {
      int i = 0;
      long prev_time = System.currentTimeMillis();
      long time;

      for ( i = 0; i < 1000000; i++ ) {
         String s = "Blah" + i + "Blah";
      }
      time = System.currentTimeMillis() - prev_time;

      System.out.println("Time after for loop " + time);

      prev_time = System.currentTimeMillis();
      for ( i = 0; i < 1000000; i++ ) {
         String s = String.format("Blah %d Blah", i);
      }
      time = System.currentTimeMillis() - prev_time;
      System.out.println("Time after for loop " + time);

      prev_time = System.currentTimeMillis();
      for ( i = 0; i < 1000000; i++ ) {
         new StringBuilder("Blah").append(i).append("Blah");
      }
      time = System.currentTimeMillis() - prev_time;
      System.out.println("Time after for loop " + time);
   }
}

虽然这可能听起来很激烈,但我认为它仅在极少数情况下相关,因为绝对数字非常低:100 万个简单的 String.format 调用需要 4 秒 - 只要我将它们用于日志记录或像。

更新:正如 sjbotha 在评论中指出的那样, StringBuilder 测试是无效的,因为它缺少一个 final .toString()

String.format(.)在我的机器上,正确的加速因子StringBuilder是 23(带-server开关的是 16)。

于 2009-02-04T23:00:08.560 回答
1

这是 hhafez 条目的修改版本。它包括一个字符串生成器选项。

public class BLA
{
public static final String BLAH = "Blah ";
public static final String BLAH2 = " Blah";
public static final String BLAH3 = "Blah %d Blah";


public static void main(String[] args) {
    int i = 0;
    long prev_time = System.currentTimeMillis();
    long time;
    int numLoops = 1000000;

    for( i = 0; i< numLoops; i++){
        String s = BLAH + i + BLAH2;
    }
    time = System.currentTimeMillis() - prev_time;

    System.out.println("Time after for loop " + time);

    prev_time = System.currentTimeMillis();
    for( i = 0; i<numLoops; i++){
        String s = String.format(BLAH3, i);
    }
    time = System.currentTimeMillis() - prev_time;
    System.out.println("Time after for loop " + time);

    prev_time = System.currentTimeMillis();
    for( i = 0; i<numLoops; i++){
        StringBuilder sb = new StringBuilder();
        sb.append(BLAH);
        sb.append(i);
        sb.append(BLAH2);
        String s = sb.toString();
    }
    time = System.currentTimeMillis() - prev_time;
    System.out.println("Time after for loop " + time);

}

}

for 循环 391 之后的时间 for 循环 4163 之后的时间 for 循环 227 之后的时间

于 2014-06-25T21:01:11.577 回答
0

这个问题的答案很大程度上取决于您的特定 Java 编译器如何优化它生成的字节码。字符串是不可变的,理论上,每个“+”操作都可以创建一个新的。但是,您的编译器几乎可以肯定地优化了构建长字符串的临时步骤。上面两行代码完全有可能生成完全相同的字节码。

唯一真正了解的方法是在当前环境中迭代地测试代码。编写一个以迭代方式连接字符串的 QD 应用程序,并查看它们如何相互超时。

于 2009-02-04T22:16:05.703 回答
0

考虑"hello".concat( "world!" )在连接中使用少量字符串。与其他方法相比,它的性能甚至可能更好。

如果您有超过 3 个字符串,则可以考虑使用 StringBuilder 或仅使用 String,具体取决于您使用的编译器。

于 2016-12-05T17:10:19.453 回答