我们必须一直构建字符串用于日志输出等。通过各个 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() 具体来说。

有帮助吗?

解决方案

我写了一个小的类来测试它有两个更好的性能和+谈到未来的格式。通过的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);

    }
}

运行以上为不同氮说明两者表现线性,但String.format较慢5-30倍。

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

“运行时的比较”

其他提示

我拿了 哈菲兹 代码并添加了一个 记忆测试:

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
字符串格式  16484       373,312
StringBuilder  769           57,344

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

还有一点,顺便说一句,不要忘记检查日志记录 等级 在构造消息之前。

结论:

  1. 我将继续使用 StringBuilder。
  2. 我的时间太多,或者生命太少。

所有这里给出的基准有一些缺陷,因此结果是不可靠的。

我很惊讶,没有人使用 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虚拟机。

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

您老难看样式自动通过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是多了很多重量级的,因为它创建了一个新的格式化,解析您的输入格式字符串,创建一个StringBuilder,附加的一切,并调用的toString()。

Java 的 String.format 的工作原理如下:

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

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

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

我推测优化器将优化格式字符串处理。如果是这样,你就剩下等价的 摊销的 手动将 String.format 展开到 StringBuilder 中的性能。

要展开/正确的第一个答案上面,这不是翻译说的String.format将有帮助,其实。点击 什么的String.format将帮助是,当你打印的日期/时间(或数字格式等),那里有本地化(L10N)的差异(即,一些国家将打印04Feb2009和其他人将打印Feb042009)。点击 通过翻译,你只是在谈论移动任何外部化的字符串(如错误消息和什么,而不是)到属性包,这样就可以使用右束为正确的语言,使用资源包和MessageFormat中。结果 点击 综观上述所有,我想说的是性能的角度来看,主场迎战的String.format普通级联归结到你喜欢什么。如果你喜欢看来电.format在串联,然后通过各种手段,去这一点。点击 毕竟,代码读取多很多,比它的写入。

在你的榜样,表现probalby也差不太多,但也有其他的问题需要考虑:即内存碎片。即使连击操作创建一个新的字符串,即使它暂时的(它需要时间来GC它和它的更多的工作)。的String.format()仅仅是更好的可读性和它涉及较少的碎片。

此外,如果您使用的是特定的格式很多,不要忘了,你可以直接使用格式化()类(所有的String.format()确实是实例化一个使用格式化程序实例)。

此外,其他的东西你应该知道:小心使用子的()。例如:

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

第二种形式可能是更有益的,如果你在同一时间做其他的东西。

一般来说,你应该,因为它是比较快的,它支持全球化(假设你实际上是试图写一些东西,被用户读取)使用的String.Format。这也使得它更容易全球化,如果你想给一个字符串与3个或更多翻译每条语句(尤其是对于具有完全不同的语法结构的语言)。

现在,如果你在翻译什么从不打算,那么要么依赖于Java内置的转换+运营商的进入StringBuilder。或者使用Java的StringBuilder明确。

仅从日志记录的角度来看另一个角度。

我看到很多与登录此线程相关的讨论,因此考虑在答案中添加我的经验。也许有人会发现它有用。

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

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

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”块,因为

  • 影响可读性
  • 减少单元测试的覆盖范围 - 当您想确保每一行都经过测试时,这会令人困惑。

因此,我更喜欢使用上面的方法创建一个日志记录实用程序类,并在任何地方使用它,而不用担心性能影响和与之相关的任何其他问题。

我刚刚修改了 hhafez 的测试以包含 StringBuilder。在 XP 上使用 jdk 1.6.0_10 客户端,StringBuilder 比 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);
   }
}

虽然这听起来可能很激烈,但我认为它只在极少数情况下才有意义,因为绝对数字非常低:4 秒进行 100 万次简单的 String.format 调用是可以的 - 只要我将它们用于日志记录等。

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

正确的加速因子来自 String.format(.)StringBuilder 在我的机器上是 23(16 -server 转变)。

下面被修改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时代 时间循环227后

这个问题的答案取决于你的具体Java编译器如何优化它产生的字节码非常多。字符串是不可变,并且从理论上说,每个“+”操作可以建立一个新的。但是,你的编译器几乎可以肯定是优化掉在构建长串的中间步骤。这是完全可能的是,以上的代码两行生成完全相同的字节码。

要知道的唯一真正的办法是在当前的环境下反复测试代码。写QD的应用程序,重复连接字符串两种方式,看看他们是如何超时反目成仇。

考虑使用"hello".concat( "world!" )为少数串联串。这可能是连供的性能比其他方法更好。

如果您有超过3串,不是考虑使用StringBuilder的,或只是字符串,这取决于您使用的编译器。

许可以下: CC-BY-SA归因
不隶属于 StackOverflow
scroll top