剖析java的字符串拼接机制

来源:互联网 发布:it cosmetics 编辑:程序博客网 时间:2024/05/17 03:08
 说在前面。本文只是为了发表我的观点,表述中如果有问题请谅解;另外出现错别字不要惊慌,对我来说是一件正常的事;再者,转载注明出处,PBY。    我们普遍听到的说法是,利用java提供对加号运算符的重载机制,进行字符串拼接是一件低效且资源消耗大的事情,而其理由是,由于String对象的不可变性,导致每次的拼接运算会产生一个冗余的字符串对象,也有说法说该对象会被置于字符串常量池,造成巨大的资源消耗。就此,和大家讨论几点问题:

1、这种说法是否正确,如果错误,这种说法是如何流传的

2java究竟如何实现在字符串拼接时对加号运算符的重载

3、是不是所有的字符串对象都会进入字符串常量池

就第一个问题来说,已经在前文隐晦的给出了我的观点,本文的目的其实是为了反驳开头的这种观点,事实上更加精确的说法是,这种提法从JDK1.5开始不成立了。

关于这个问题,jdk1.5以前的版本我没有兴趣去探究,可以猜测到的是,在1.5之前,java的确使用了低效的手段重载加号运算符,也使得当时的工程师普遍放弃使用这种拼接机制,这种影响一直延续到了1.5以后的版本。即使在普遍使用1.6或更高版本的今天,大多数的人依然认为java对字符串拼接的处理是笨拙的,主要原因可能是java团队似乎在1.5版本的修改说明中并没有提及关于String拼接处理的修改,或者说java团队公示了这个修改,但是没有人去关注,当然另有原因是一部分人知道了这个修改但是出于一些考虑,仍然不使用java基于加号运算符重载机制的字符串拼接,这个原因我将在后文介绍。

目前普遍的做法是使用StringBufferStringBuilder来拼接字符串,事实上对这两个类的选择比较自由,通常由习惯来定,由于两者的区别只体现在线程安全性上,又由于在做字符串拼接时这两者通常通过局部变量的方式出现,所有事实上两者在大多数时候没有区别,因此在后文的陈述中,这两个类将会互相指代。

那么在JDK1.5以后java如何实现对+运算符的重载呢?

首先探究字符串常量加号运算,通过图1中的示例来看,图右侧是一段简单的java代码,其中出现了字符串常量的加运算,左侧是其编译后的class文件,从左侧的文件中,可以清楚的看到,class并没有出现”abc””def”的拼接痕迹,而是直接以”abcdef“的形式呈现。这个示例可以清楚的说明,java中字符串常量的拼接实际是在编译其完成。实时上除了String其他常量的运算都是编译期完成的,图2的示例使用int类型数据做了演示。


1


2

需要说明的是,这种机制不是在JDK1.5才有的,具体什么版本没有详细探究,但是我想java团队,应该在最早的时候就有了这样的处理。

下面探究字符串变量的拼接处理,方法依然是观察class文件,如图3class文件清楚的说明了java如何重载加号运算符,请看我划线的3行代码,我们惊讶的发现,其中竟然出现了StringBuilder这个不速之客,也就是说,java对加号运算符的重载是基于StringBuilder实现的,由于StringBuilder是在1.5版本出现的,所以自然的猜想到这种机制也是在1.5开始的,但究竟1.5之前使用什么在这里就不做探究了。


3

基于以上的表述,文章的观点逐渐滑向了鼓励使用加号运算来拼接字符串,但事实上,我将继续陈述下一个问题,这个问题可能不是鲜为人知的,对于大多数工程师或者java使用者来说,应该是知道的,但是为了文章的完整性我依然要陈述。

StringBufferString的底层实现都是利用数组,而String的数组是final(关于String为什么使用final修饰在本文中将不做讨论,可以另发文来说明),因此对于一个生产中的字符串,更加适合用一个可变的对象来处理,而大多数时候我们都选择StringBuffer来进行,实时上前文也说了,java的拼接也是用StringBuilder实现的,那么两者之间会有什么区别呢,我们是否应该放弃使用StringBuffer呢?

第一个浅显的理由不支持我们放弃对StringBuffer的使用,如图4,这种拼接形式是我们常会用到的,这种情景下,我们可能应当继续使用StringBuffer去做拼接工作。


4

第二个理由更为重要,我提到StringBuffer使用数组实现,那么数组多大是我们应该关心的问题,图3可以看到,java调用 new StringBuilder() 构造方法来做字符串拼接的“工厂”,通过翻看java源码(在此不截取了),这个构造方法,初始化了一个16位的数组,作为缓存区,这对于大多数的需求来说是不够的,我们继续关注append方法的实现,我截取了调用栈的最后一段,就是如果缓存区不足以装下待添加的字符串时,如何扩容:注意图5划线的两行代码,第二行说明,扩容的方法是将原有的缓存区内容放到一个更大的数组中,这段代码我不做详解了,事实上我们可以猜测到StringBuffer使用这种手段进行扩容处理,从代码中也可以看到,java对容量扩大多少也是做了比较精心的计算的,但事实上,如果我们使用它做Sql的拼接,java会扩容多少次,会产生多少冗余的引用,这两个数据恐怕都非常可观。


5

本文的主旨终于引导到了,正确的使用StringBuffer,简单说就是根据需求指明缓存区大小,直接上图6,请使用这个方法构造StringBuffer


6

结合以上全文,我们得到了一种更加健康的使用StringBuffer的方式,直接上图7

 

7

7的示例,引发了对开头问题3的思考,一个简单的思考是图中的String c是否会进入字符串常量池,这个问题其实在很多资料中已经有了答案,但是我仍然尝试探索这个问题,将在下一回分享我的探索过程。

0 0
原创粉丝点击