我们必须一直为日志输出构建字符串等等。在JDK版本中,我们已经学习了什么时候使用StringBuffer(许多追加,线程安全)和StringBuilder(许多追加,非线程安全)。
使用String.format()有什么建议?它是有效的,还是我们被迫坚持在性能很重要的一行程序中使用连接?
例如,丑陋的老式风格,
String s = "What do you get if you multiply " + varSix + " by " + varNine + "?";
vs.整洁的新样式(字符串。格式,可能更慢),
String s = String.format("What do you get if you multiply %d by %d?", varSix, varNine);
注意:我的特定用例是代码中的数百个“一行”日志字符串。它们不涉及循环,所以StringBuilder太重量级了。我对String.format()特别感兴趣。
仅从日志的角度看另一个角度。
我在这个帖子上看到了很多关于登录的讨论,所以我想在回答中加入我的经验。也许有人会觉得有用。
我猜使用格式化程序进行日志记录的动机来自于避免字符串连接。基本上,如果你不打算记录它,你不希望有字符串连接的开销。
实际上不需要concat/format,除非您想记录日志。假设我这样定义一个方法
public void logDebug(String... args, Throwable t) {
if(debugOn) {
// call concat methods for all args
//log the final debug message
}
}
在这种方法中,如果它是一个调试消息并且debugOn = false,则根本不会真正调用canat /formatter
尽管在这里使用StringBuilder而不是formatter会更好。我们的主要动机就是避免这些。
同时,我不喜欢为每个日志语句添加“if”块
它会影响可读性
减少单元测试的覆盖率——当你想要确保每一行都经过测试时,这会让人感到困惑。
因此,我更喜欢用上面的方法创建一个日志实用程序类,并在任何地方使用它,而不用担心性能损失和其他与之相关的问题。
下面是修改后的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);
}
}
循环391之后的时间
循环4163之后的时间
循环227之后的时间
我使用了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次):
Approach |
Time(ms) |
Memory allocated (long) |
+ operator |
747 |
320,504 |
String.format |
16484 |
373,312 |
StringBuilder |
769 |
57,344 |
我们可以看到String +和StringBuilder在时间上实际上是相同的,但是StringBuilder在内存使用上要高效得多。
当我们在足够短的时间间隔内有许多日志调用(或任何其他涉及字符串的语句)时,这是非常重要的,因此垃圾收集器将无法清理+操作符导致的许多字符串实例。
顺便说一句,在构造消息之前,不要忘记检查日志级别。
结论:
我将继续使用StringBuilder。
我有的是时间,有的是生活。
我写了一个小类来测试两者中哪个具有更好的性能,并且+优先于格式。以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。格式要慢5-30倍。
原因是在当前的String实现中。Format首先用正则表达式解析输入,然后填充参数。另一方面,使用plus的连接由javac(而不是JIT)优化,并使用StringBuilder。直接添加。
在您的示例中,性能可能没有太大不同,但还有其他问题需要考虑:即内存碎片。连接操作也在创建一个新字符串,即使它是临时的(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));
or
return String.format("%s", largeString.substring(100, 300));
如果你同时做其他事情,第二种形式可能更有用。