如果性能很重要,我应该使用Java的String.format()吗?

我们必须始终为日志输出构buildstring等等。 在JDK版本中我们学习了什么时候使用StringBuffer (很多附加的,线程安全的)和StringBuilder (很多附加的,非线程安全的)。

什么是使用String.format()的build议? 它是有效率的,还是我们不得不坚持串联一个性能重要的单线?

丑陋的旧风格,

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

整洁的新风格(可能很慢)

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

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

我写了一个小class来testing哪一个有更好的performance,而且格式提前。 由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首先用正则expression式parsinginput,然后填充参数。 另一方面,使用加号连接,通过javac(而不是由JIT)进行优化,并直接使用StringBuilder.append

运行时比较

我采取了hhafez代码,并添加了一个内存testing

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

我分别为每个方法运行,“+”运算符,String.format和StringBuilder(调用toString()),所以使用的内存不会受到其他方法的影响。 我添加了更多的连接,使string为“Blah”+ I +“Blah”+ I +“Blah”+ I +“Blah”。

结果如下(每次平均5次):
接近时间(ms)分配的内存(长)
“+”号747 320,504
String.format 16484 373,312
StringBuilder 769 57,344

我们可以看到,String'+'和StringBuilder在时间上是几乎相同的,但是StringBuilder在内存使用上效率更高。 当我们在足够短的时间间隔内有很多日志调用(或任何其他涉及string的语句)时,这一点非常重要,所以垃圾收集器将无法清除由“+”运算符产生的许多string实例。

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

结论:

  1. 我将继续使用StringBuilder。
  2. 我有太多的时间或太less的生活。

您的旧丑样式是由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更重要,因为它会创build一个新的Formatter,分析input的格式string,创build一个StringBuilder,将所有内容附加到它并调用toString()。

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

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

如果这个数据的最终目的地是一个stream(例如渲染一个网页或者写入一个文件),那么你可以直接将这些格式块组装到你的stream中:

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

我推测优化器将优化格式string处理。 如果是这样,你剩下的分摊性能等同于手动展开你的String.format到一个StringBuilder。

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

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

结果:

 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虚拟机。

所以,旧式(使用+)要快得多。

为了扩展/更正上面的第一个答案,实际上并不是String.format会帮助的翻译。
当打印date/时间(或数字格式等)时,什么是String.format会有帮助,哪里有本地化(l10n)差异(即某些国家将打印04Feb2009,其他国家将打印Feb042009)。
有了翻译,你只是在谈论如何将任何可以外部化的string(如错误消息和什么)移动到属性包中,以便使用正确的语言包,使用ResourceBundle和MessageFormat。

纵观以上所有,我会说性能明智,String.format与纯串联归结为你喜欢的。 如果你更喜欢看电话。格式化连接,那么一定要用那个。
毕竟,代码的读取比写入的要多得多。

在你的例子中,性能probalby没有太大差别,但还有其他问题需要考虑:内存碎片。 即使连接操作正在创build一个新的string,即使它是临时的(GC需要时间,它更多的工作)。 String.format()更具可读性,并且涉及更less的碎片。

另外,如果你使用的格式很多,不要忘记你可以直接使用Formatter()类(所有String.format()都是实例化一个Formatter实例)。

看到这个相关的问题是String.Format与StringBuilder一样高效吗? 。

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

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

这个大string仍然在内存中,因为这只是Java子串的工作方式。 更好的版本是:

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

要么

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

如果你在同一时间做其他的事情,第二种forms可能更有用。

一般来说,你应该使用String.Format,因为它比较快,并且支持全球化(假设你正在尝试写一些用户阅读的东西)。 如果你想翻译一个string而不是每个语句3个或更多(特别是对于语法结构完全不同的语言),它也使全球化变得更容易。

现在,如果你从来没有计划翻译任何东西,那么要么依靠Java内build的+运算符转换成StringBuilder 。 或者明确地使用Java的StringBuilder

从Logging的angular度来看另一个angular度只有。

我看到很多关于login这个线程的讨论,所以想到join我的经验来回答。 可能有人会觉得有用。

我想使用格式化程序的动机来自避免string连接。 基本上,如果你不打算logging,你不希望有stringconcat的开销。

除非你想logging,否则你不需要concat / format。 比方说,如果我定义一个这样的方法

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

在这种方法中,如果cancat / formatter的debugging消息和debugOn = false,则根本不会调用cancat / formatter

虽然在这里使用StringBuilder而不是格式化器会更好。 主要动机是避免任何这一点。

同时,我不想为每个日志logging声明添加“if”块

  • 它影响可读性
  • 减less我unit testing的覆盖率 – 当你想要确保每条线路都经过testing时,这会让人困惑。

因此,我更喜欢用上面的方法创build一个日志实用程序类,并在任何地方使用它,而不必担心性能问题以及与此相关的任何其他问题。

我只是修改hhafez的testing,包括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); } } 

虽然这可能听起来很激烈,但我认为这仅仅在极less数情况下才是有意义的,因为绝对数量相当低:对于100万简单的String.format调用,4秒是可以的 – 只要我用它们进行日志logging或喜欢。

更新:正如sjbotha在注释中指出的那样,StringBuildertesting是无效的,因为它缺less一个最终的.toString()

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

这里是hhafez条目的修改版本。 它包含一个string生成器选项。

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

}

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

对此的回答很大程度上取决于您的特定Java编译器如何优化其生成的字节码。 string是不可改变的,理论上每个“+”操作都可以创build一个新的string。 但是,您的编译器几乎可以肯定,优化了构build长string的临时步骤。 上面的两行代码完全可能生成完全相同的字节码。

唯一真正的方法是在当前环境中迭代testing代码。 编写一个QD应用程序,它可以迭代地连接string,看看它们是如何超越对方的。

考虑对串联中的less量string使用"hello".concat( "world!" ) 。 性能可能比其他方法更好。

如果你有3个以上的string,比使用StringBuilder,或只是string,根据你使用的编译器。