Java字符串拼接效率分析及最佳实践

本文来源于问题 Java字符串连接最佳实践?

  1. java连接字符串有多种方式,比如+操作符,StringBuilder.append方法,这些方法各有什么优劣(可以适当说明各种方式的实现细节)?
  2. 按照高效的原则,那么java中字符串连接的最佳实践是什么?
  3. 有关字符串处理,都有哪些其他的最佳实践?

废话不多说,直接开始, 环境如下:

  • JDK版本: 1.8.0_65
  • CPU: i7 4790
  • 内存: 16G

直接使用+拼接

看下面的代码:


  1. @Test 
  2.     public void test() { 
  3.         String str1 = "abc"; 
  4.         String str2 = "def"; 
  5.         logger.debug(str1 + str2); 
  6.     }  

在上面的代码中,我们使用加号来连接四个字符串,这种字符串拼接的方式优点很明显:
代码简单直观,但是对比StringBuilder和StringBuffer在大部分情况下比后者都低,这里说是大部分情况下,我们用javap工具对上面代码生成的字节码进行反编译看看在编译器对这段代码做了什么。


  1. public void test(); 
  2.     Code: 
  3.        0: ldc           #5                  // String abc 
  4.        2: astore_1 
  5.        3: ldc           #6                  // String def 
  6.        5: astore_2 
  7.        6: aload_0 
  8.        7: getfield      #4                  // Field logger:Lorg/slf4j/Logger; 
  9.       10: new           #7                  // class java/lang/StringBuilder 
  10.       13: dup 
  11.       14: invokespecial #8                  // Method java/lang/StringBuilder."<init>":()V 
  12.       17: aload_1 
  13.       18: invokevirtual #9                  // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder; 
  14.       21: aload_2 
  15.       22: invokevirtual #9                  // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder; 
  16.       25: invokevirtual #10                 // Method java/lang/StringBuilder.toString:()Ljava/lang/String; 
  17.       28: invokeinterface #11,  2           // InterfaceMethod org/slf4j/Logger.debug:(Ljava/lang/String;)V 
  18.       33: return  

从反编译的结果来看,实际上对字符串使用+操作符进行拼接,编译器会在编译阶段把代码优化成使用StringBuilder类,并调用append方法进行字符串拼接,最后调用toString方法,这样看来是否可以认为在一般情况下其实直接使用+,反正编译器也会帮我优化为使用StringBuilder?

StringBuilder源码分析

答案自然是不可以的,原因就在于StringBuilder这个类它内部做了些什么时。

我们看一看StringBuilder类的构造器


  1. public StringBuilder() { 
  2.         super(16); 
  3.     } 
  4.  
  5.     public StringBuilder(int capacity) { 
  6.         super(capacity); 
  7.     } 
  8.  
  9.     public StringBuilder(String str) { 
  10.         super(str.length() + 16); 
  11.         append(str); 
  12.     } 
  13.  
  14.     public StringBuilder(CharSequence seq) { 
  15.         this(seq.length() + 16); 
  16.         append(seq); 
  17.     }  

StringBuilder提供了4个默认的构造器, 除了无参构造函数外,还提供了另外3个重载版本,而内部都调用父类的super(int capacity)构造方法,它的父类是AbstractStringBuilder,构造方法如下:


  1. AbstractStringBuilder(int capacity) { 
  2.         value = new char[capacity]; 
  3.     }  

可以看到实际上StringBuilder内部使用的是char数组来存储数据(String、StringBuffer也是),这里capacity的值指定了数组的大小。结合StringBuilder的无参构造函数,可以知道默认的大小是16个字符。

也就是说如果待拼接的字符串总长度不小于16的字符的话,那么其实直接拼接和我们手动写StringBuilder区别不大,但是我们自己构造StringBuilder类可以指定数组的大小,避免分配过多的内存。

现在我们再看看StringBuilder.append方法内部做了什么事:


  1. @Override 
  2.    public StringBuilder append(String str) { 
  3.        super.append(str); 
  4.        return this; 
  5.    }  

直接调用的父类的append方法:


  1. public AbstractStringBuilder append(String str) { 
  2.         if (str == null) 
  3.             return appendNull(); 
  4.         int len = str.length(); 
  5.         ensureCapacityInternal(count + len); 
  6.         str.getChars(0, len, value, count); 
  7.         count += len; 
  8.         return this; 
  9.     }  

在这个方法内部调用了ensureCapacityInternal方法,当拼接后的字符串总大小大于内部数组value的大小时,就必须先扩容才能拼接,扩容的代码如下:


  1. void expandCapacity(int minimumCapacity) { 
  2.         int newCapacity = value.length * 2 + 2; 
  3.         if (newCapacity - minimumCapacity < 0) 
  4.             newCapacity = minimumCapacity; 
  5.         if (newCapacity < 0) { 
  6.             if (minimumCapacity < 0) // overflow 
  7.                 throw new OutOfMemoryError(); 
  8.             newCapacity = Integer.MAX_VALUE; 
  9.         } 
  10.         value = Arrays.copyOf(value, newCapacity); 
  11.     }  

StringBuilder在扩容时把容量增大到当前容量的两倍+2,这是很可怕的,如果在构造的时候没有指定容量,那么很有可能在扩容之后占用了浪费大量的内存空间。其次扩容后还调用了Arrays.copyOf方法,这个方法把扩容前的数据复制到扩容后的空间内,这样做的原因是:StringBuilder内部使用char数组存放数据,java的数组是不可扩容的,所以只能重新申请一片内存空间,并把已有的数据复制到新的空间去,这里它最终调用了System.arraycopy方法来复制,这是一个native方法,底层直接操作内存,所以比我们用循环来复制要块的多,即便如此,大量申请内存空间和复制数据带来的影响也不可忽视。

使用+拼接和使用StringBuilder比较


  1. @Test 
  2. public void test() { 
  3.     String str = ""; 
  4.     for (int i = 0; i < 10000; i++) { 
  5.         str += "asjdkla"; 
  6.     } 
  7. }  

上面这段代码经过优化后相当于:


  1. @Test 
  2.    public void test() { 
  3.        String str = null; 
  4.        for (int i = 0; i < 10000; i++) { 
  5.            str = new StringBuilder().append(str).append("asjdkla").toString(); 
  6.        } 
  7.    } 

一眼就能看出创建了太多的StringBuilder对象,而且在每次循环过后str越来越大,导致每次申请的内存空间越来越大,并且当str长度大于16时,每次都要扩容两次!而实际上toString方法在创建String对象时,调用了Arrays.copyOfRange方法来复制数据,此时相当于每执行一次,扩容了两次,复制了3次数据,这样的代价是相当高的。


  1. public void test() { 
  2.         StringBuilder sb = new StringBuilder("asjdkla".length() * 10000); 
  3.         for (int i = 0; i < 10000; i++) { 
  4.             sb.append("asjdkla"); 
  5.         } 
  6.         String str = sb.toString(); 
  7.     }  

这段代码的执行时间在我的机器上都是0ms(小于1ms)和1ms,而上面那段代码则大约在380ms!效率的差距相当明显。

同样是上面的代码,将循环次数调整为1000000时,在我的机器上,有指定capacity时耗时大约20ms,没有指定capacity时耗时大约29ms,这个差距虽然和直接使用+操作符有了很大的提升(且循环次数增大了100倍),但是它依旧会触发多次扩容和复制。

将上面的代码改成使用StringBuffer,在我的机器上,耗时大约为33ms,这是因为StringBuffer在大部分方法上都加上了synchronized关键字来保证线程安全,执行效率有一定程度上的降低。

使用String.concat拼接

现在再看这段代码:


  1. @Test 
  2.    public void test() { 
  3.        String str = ""; 
  4.        for (int i = 0; i < 10000; i++) { 
  5.            str.concat("asjdkla"); 
  6.        } 
  7.    }  

这段代码使用了String.concat方法,在我的机器上,执行时间大约为130ms,虽然直接相加要好的多,但是比起使用StringBuilder还要太多了,似乎没什么用。其实并不是,在很多时候,我们只需要连接两个字符串,而不是多个字符串的拼接,这个时候使用String.concat方法比StringBuilder要简洁且效率要高。


  1. public String concat(String str) { 
  2.         int otherLen = str.length(); 
  3.         if (otherLen == 0) { 
  4.             return this; 
  5.         } 
  6.         int len = value.length; 
  7.         char buf[] = Arrays.copyOf(value, len + otherLen); 
  8.         str.getChars(buf, len); 
  9.         return new String(buf, true); 
  10.     } 

上面这段是String.concat的源码,在这个方法中,调用了一次Arrays.copyOf,并且指定了len +
otherLen,相当于分配了一次内存空间,并分别从str1和str2各复制一次数据。而如果使用StringBuilder并指定capacity,相当于分配一次内存空间,并分别从str1和str2各复制一次数据,最后因为调用了toString方法,又复制了一次数据。

结论

现在根据上面的分析和测试可以知道:

  1. Java中字符串拼接不要直接使用+拼接。
  2. 使用StringBuilder或者StringBuffer时,尽可能准确地估算capacity,并在构造时指定,避免内存浪费和频繁的扩容及复制。
  3. 在没有线程安全问题时使用StringBuilder, 否则使用StringBuffer。
  4. 两个字符串拼接直接调用String.concat性能最好。

关于String的其他最佳实践

  1. 用equals时总是把能确定不为空的变量写在左边,如使用"".equals(str)判断空串,避免空指针异常。
  2. 第二点是用来排挤第一点的.. 使用str != null && str.length() != 0来判断空串,效率比第一点高。
  3. 在需要把其他对象转换为字符串对象时,使用String.valueOf(obj)而不是直接调用obj.toString()方法,因为前者已经对空值进行检测了,不会抛出空指针异常。
  4. 使用String.format()方法对字符串进行格式化输出。
  5. 在JDK 7及以上版本,可以在switch结构中使用字符串了,所以对于较多的比较,使用switch代替if-else。

我暂时想的起来的就这么几个了.. 请大家帮忙补充补充...

作者:疯狂的爱因斯坦

来源:51CTO

时间: 2024-08-06 23:44:23

Java字符串拼接效率分析及最佳实践的相关文章

深入理解Java虚拟机:JVM高级特性与最佳实践

目 录 [ - ] <深入理解Java虚拟机:JVM高级特性与最佳实践>前言 <深入理解Java虚拟机:JVM高级特性与最佳实践>内容特色 <深入理解Java虚拟机:JVM高级特性与最佳实践>目录 第1章 走近Java 1.1 概述 1.2 Java技术体系 1.3 Java发展史 1.4 展望Java技术的未来 1.4.1 模块化 1.4.2 混合语言 1.4.3 多核并行 1.4.4 进一步丰富语法 1.4.5 64位虚拟机 1.5 实战:自己编译JDK 1.5.1

SqlParameter居然不如字符串拼接效率高,很怪的问题。

问题描述 直接上代码,别一个贴子上有详细说明http://topic.csdn.net/u/20120116/14/4cbcea4c-4b2a-47dd-989c-bb5ebf57ad2a.html?seed=525050463&r=77321509#r_77321509publicDataSetGetRegInfoGameList(DateTimeBeginTime,DateTimeEndTime){//#region用字符串拼接测试结果,这个正常//StringBuilderstrSql=n

WebSphere DataPower 的资源管理和分析的最佳实践

简介 IBM WebSphere DataPower Appliances (以下简称 DataPower)的构建目标是快速部署系统集成和安全政策.让固件和硬件组件相匹配,以便在硬化和易于管理的平台中优化策略执行.DataPower 加快了实现价值的速度,降低了这些复杂的基础体系结构的总拥有成本. DataPower 配置往往通过与其他服务的集成来实现解决方案.例如,通过访问一个集中式目录(LDAP),可以制定安全策略决策.通过注册表和存储库系统,可以获得企业策略.通过访问 SYSLOG 资源,

JavaScript 字符串拼接性能分析小记

JavaScript 中,我们经常会因为各种各样的原因去拼接字符串,可能是为了Html的呈现,可能是为了属性的设置,也可能仅仅是为了输出调试日志. 每次拼接字符串的时候,我们很自然地去考虑它的性能,是不是str=str+a是不是和str+=a性能一样,使用数组Join是不是会更快一些,哪一种才是最好的实现. 的确,我们有诸多方式去拼接字符串,下面本文将会将主流字符串拼接方逐一进行性能评测. 公平起见,我们的测试都是跑在各浏览器的最后一个正式发布版本上,即IE9 ,Chrome19,FF13 及O

Java 字符串的拼接详解_java

工作日忙于项目的逻辑实现,周六有点时间,从书柜里拿出厚厚的英文版Thinking In Java,读到了字符串对象的拼接.参考着这本书做个翻译,加上自己思考的东西,写上这篇文章记录一下. 不可变的String对象 在Java中,String对象是不可变的(Immutable).在代码中,可以创建多个某一个String对象的别名.但是这些别名都是的引用是相同的. 比如s1和s2都是"droidyue.com"对象的别名,别名保存着到真实对象的引用.所以s1 = s2 String s1

Java中字符串拼接的一些细节分析_java

工作日忙于项目的逻辑实现,周六有点时间,从书柜里拿出厚厚的英文版Thinking In Java,读到了字符串对象的拼接.参考着这本书做个翻译,加上自己思考的东西,写上这篇文章记录一下. 不可变的String对象 在Java中,String对象是不可变的(Immutable).在代码中,可以创建多个某一个String对象的别名.但是这些别名都是的引用是相同的. 比如s1和s2都是"droidyue.com"对象的别名,别名保存着到真实对象的引用.所以s1 = s2 复制代码 代码如下:

Java程序优化的一些最佳实践

摘要:本文介绍了Java代码优化的过程,总结了优化Java程序的一些最佳实践,分析了进行优化的方法并解释了性能提升的原因.多角度分析导致性能低的原因并逐个进行优化使得程序性能得到极大提升,代码可读性.可扩展性更强. 作者通过经历的一个项目实例,介绍Java代码优化的过程,总结了优化Java程序的一些最佳实践,分析了进行优化的方法,并解释了性能提升的原因.作者从多个角度分析导致性能低的原因,并逐个进行优化,最终使得程序的性能得到极大提升,增强了代码的可读性.可扩展性. 一.衡量程序的标准衡量一个程

优云软件数据专家最佳实践:数据挖掘与运维分析

这份研究报告,作者是优云软件数据专家陈是维,在耗时1年时间制作的一份最佳实践,今天和大家分享下,关于<数据采矿和运维分析>,共同探讨~ 数据挖掘(Data Mining)是从大量数据中提取或"挖掘"知识. 广义数据挖掘:数据挖掘是从存放在数据库.数据仓库或其它信息库中的大量数据挖掘有趣知识的过程. 数据挖掘技术侧重:1)概率与数理统计 2)数据库技术 3)人工智能技术 4)机器学习. 1. 数据清理:消除噪音或不一致数据 2. 数据集成:多种数据源可以组合在一起 3. 数据

从源代码的角度聊聊java中StringBuffer、StringBuilder、String中的字符串拼接

长久以来,我们被教导字符串的连接最好用StringBuffer.StringBuilder,但是我们却不知道这两者之间的区别.跟字符串相关的一些方法中总是有CharSequence.StringBuffer.StringBuilder.String,他们之间到底有什么联系呢? 1.从类的定义看CharSequence.StringBuffer.StringBuilder.String的关系 下面先贴上这四者的定义(来自JDK1.6) CharSequence是一个定义字符串操作的接口,Strin