内部实现
String
类代表字符串。
它是 final
修饰的类,不可继承。
1 | public final class String |
也就是说,Java 的设计者,不允许任何人定义 String
的子类。
因此,如果有一个 String
的引用,那么它引用的一定是 String
对象。
String
对象是常量,是不可变的,所以可以共享。
1 | private final char value[]; |
反过来想,正是因为字符串的使用频率非常高,所以需要通过一种共享机制来保证性能
String
的 API 比较稳定(1.8 新增了 join()
方法),大部分方法都需要熟练掌握。
但是有一些方法确实容易让人混淆,例如:split()
、 substring()
。
具体用法,可以参考我编写的 StringApiTest
。
另外,第三方类库 org.apache.commons.lang3.StringUtils
是 String
很好的补充,建议使用。
如果要比较字符串的内容是否相等,不用想,直接使用 equals()
或者 equalsIgnoreCase()
就对了。==
只能用来确定两个字符串是否存放在同一个位置。
创建和存储
关于字符串的创建和存储,我的理解是这样子的:
第 1 行,在编译期,编译器首先会去字符串池中检查是否存在 "string"
,如果存在,则把它的内存地址赋给 s1
,如果不存,则创建后,再返回新的内存地址。
第 2 行,网上很多文章说,会产生 3 个字符串对象,但是,实际上,常量的拼接,编译器是会进行优化的。
第 3 行,因为字符串池中已经存在 "string"
,所以不会创建,但是会在堆中创建一个新的字符串对象 "string"
。
第 4 行,因为字符串池中不存在 "str"
,所以会创建它,同时,也会在堆中创建一个新的字符串对象 "str"
。
因此,为了环保,能不 new
就不 new
吧。
字符串拼接
测试结果
结论:
少量字符串拼接,可以直接 +
;
如果大量的字符串频繁拼接,并且需要保证线程安全,使用 StringBuffer
,否则使用 StringBuilder
。
解析
首先,假设我们声明了2个字符串变量:
1 | String s1= "str"; |
再使用 +
直接把 s1
、s2
拼接成一个新的字符串 s3
:
1 | String s3= s1+ s2; |
实际上,真正执行的代码,大致如下:
1 | StringBuilder bulider = new StringBuilder(); |
不难看出,这里额外产生 3 个对象。这就是它低效的原因。
而使用 concat()
方法:
1 | String s3= s1.concat(s2); |
实际上,会把先把字符串 s1
拷贝到一个足够大的(可以容纳 s1
和 s2
)字符数组 buf
,然后再把拼接的字符串 s2
也拷贝到 buf
,再 new
一个新的字符串对象:
1 | String s3= new String(buf); |
这里的字符串数组拷贝操作需要一定开销,最后还要额外创建新的字符串对象,所以这种方法的效率也不高。
从测试的情况来看,当拼接的字符串较少时,甚至比直接使用 +
拼接还有低效。因此不建议使用。
如果使用 StringBuffer
的 append()
方法:
实际上,会先计算 StringBuffer
的存储空间,如果不足则放大一倍(这也是一个性能优化的点,可以预先分配足够的空间,减少复制开销),再进行字符数组的拷贝。
这种方式,避免了额外的对象创建。这也是它高效的真正原因。
StringBuilder
和 StringBuffer
的实现都是一样的,只不过 StringBuffer
的方法都是 synchronized
的。
测试代码
1 | public class StringPerformanceTest { |