String、StringBuffer、StringBuilder有什么区别?

** ********************** String、StringBuffer、StringBuilder有什么区别?**

今天我要问你的问题是,理解 Java 的字符串,String、StringBuffer、
StringBuilder 有什么区别?

典型回答
String 是 Java 语⾔⾮常基础和重要的类,提供了构造和管理字符串的各种基本
逻辑。它是典型的 Immutable 类,被声明成为 final class,所有属性也都是
final 的。也由于它的不可变性,类似拼接、裁剪字符串等动作,都会产⽣新的
String 对象。由于字符串操作的普遍性,所以相关操作的效率往往对应⽤性能有
明显影响。
StringBuffer 是为解决上⾯提到拼接产⽣太多中间对象的问题⽽提供的⼀个类,
它是 Java 1.5 中新增的,我们可以⽤ append 或者 add ⽅法,把字符串添加到
已有序列的末尾或者指定位置。StringBuffer 本质是⼀个线程安全的可修改字符
序列,它保证了线程安全,也随之带来了额外的性能开销,所以除⾮有线程安全
的需要,不然还是推荐使⽤它的后继者,也就是 StringBuilder。
StringBuilder 在能⼒上和 StringBuffer 没有本质区别,但是它去掉了线程安全
的部分,有效减⼩了开销,是绝⼤部分情况下进⾏字符串拼接的⾸选。

⼏乎所有的应⽤开发都离不开操作字符串,理解字符串的设计和实现以及相关⼯
具如拼接类的使⽤,对写出⾼质量代码是⾮常有帮助的。关于这个问题,我前⾯
的回答是⼀个通常的概要性回答,⾄少你要知道 String 是 Immutable 的,字符
串操作不当可能会产⽣⼤量临时字符串,以及线程安全⽅⾯的区别。
如果继续深⼊,⾯试官可以从各种不同的⻆度考察,⽐如可以:
通过 String 和相关类,考察基本的线程安全设计与实现,各种基础编程实
践。
考察 JVM 对象缓存机制的理解以及如何良好地使⽤。
考察 JVM 优化 Java 代码的⼀些技巧。
String 相关类的演进,⽐如 Java 9 中实现的巨⼤变化。

针对上⾯这⼏⽅⾯,我会在知识扩展部分与你详细聊聊。
知识扩展

1. 字符串设计和实现考量
我在前⾯介绍过,String 是 Immutable 类的典型实现,原⽣的保证了基础线程
安全,因为你⽆法对它内部数据进⾏任何修改,这种便利甚⾄体现在拷⻉构造函
数中,由于不可变,Immutable 对象在拷⻉时不需要额外复制数据。
我们再来看看 StringBuffer 实现的⼀些细节,它的线程安全是通过把各种修改
数据的⽅法都加上 synchronized 关键字实现的,⾮常直⽩。其实,这种简单粗
暴的实现⽅式,⾮常适合我们常⻅的线程安全类实现,不必纠结于
synchronized 性能之类的,有⼈说“过早优化是万恶之源”,考虑可靠性、正确
性和代码可读性才是⼤多数应⽤开发最重要的因素。
为了实现修改字符序列的⽬的,StringBuffer 和 StringBuilder 底层都是利⽤可
修改的(char,JDK 9 以后是 byte)数组,⼆者都继承了
AbstractStringBuilder,⾥⾯包含了基本操作,区别仅在于最终的⽅法是否加了
synchronized。
另外,这个内部数组应该创建成多⼤的呢?如果太⼩,拼接的时候可能要重新创
建⾜够⼤的数组;如果太⼤,⼜会浪费空间。⽬前的实现是,构建时初始字符串
⻓度加 16(这意味着,如果没有构建对象时输⼊最初的字符串,那么初始值就
是 16)。我们如果确定拼接会发⽣⾮常多次,⽽且⼤概是可预计的,那么就可
以指定合适的⼤⼩,避免很多次扩容的开销。扩容会产⽣多重开销,因为要抛弃
原有数组,创建新的(可以简单认为是倍数)数组,还要进⾏ arraycopy。
前⾯我讲的这些内容,在具体的代码书写中,应该如何选择呢?
在没有线程安全问题的情况下,全部拼接操作是应该都⽤ StringBuider 实现
吗?毕竟这样书写的代码,还是要多敲很多字的,可读性也不理想,下⾯的对⽐
⾮常明显。
String strByBuilder = new
StringBuilder().append(“aa”).append(“bb”).append(“cc”).append
(“dd”).toString();
String strByConcat = “aa” + “bb” + “cc” + “dd”;
其实,在通常情况下,没有必要过于担⼼,要相信 Java 还是⾮常智能的。
我们来做个实验,把下⾯⼀段代码,利⽤不同版本的 JDK 编译,然后再反编
译,例如:
public class StringConcat {
public static void main(String[] args) {
String myStr = “aa” + “bb” + “cc” + “dd”;
System.out.println(“My String:” + myStr);
}
}
先编译再反编译,⽐如使⽤ JDK 9:
${JAVA9_HOME}/bin/javac StringConcat.java
${JAVA9_HOME}/bin/javap -v StringConcat.class
JDK 8 的输出⽚段是:
6: new #4 // class java/lang/StringBuilder
9: dup
10: invokespecial #5 // Method java/lang/StringBuilder.""😦)V
13: ldc #6 // String My String:
15: invokevirtual #7 // Method java/lang/StringBuilder.append:
(Ljava/lang/String;)Ljava/lang/StringBuilder;
18: aload_1
19: invokevirtual #7 // Method java/lang/StringBuilder.append:
(Ljava/lang/String;)Ljava/lang/StringBuilder;
22: invokevirtual #8 // Method java/lang/StringBuilder.toString:
()Ljava/lang/String;
⽽在 JDK 9 中,反编译的结果就⾮常简单了,⽚段是:
7: invokedynamic #4, 0 // InvokeDynamic #0:makeConcatWithConstants:
(Ljava/lang/String;)Ljava/lang/String;
你可以看到,在 JDK 8 中,字符串拼接操作会⾃动被 javac 转换为
StringBuilder 操作,⽽在 JDK 9 ⾥⾯则是因为 Java 9 为了更加统⼀字符串操
作优化,提供了 StringConcatFactory,作为⼀个统⼀的⼊⼝。javac ⾃动⽣成
的代码,虽然未必是最优化的,但普通场景也⾜够了,你可以酌情选择。

2. 字符串缓存
我们粗略统计过,把常⻅应⽤进⾏堆转储(Dump Heap),然后分析对象组
成,会发现平均 25% 的对象是字符串,并且其中约半数是重复的。如果能避免
创建重复字符串,可以有效降低内存消耗和对象创建开销。
String 在 Java 6 以后提供了 intern() ⽅法,⽬的是提示 JVM 把相应字符串缓
存起来,以备重复使⽤。在我们创建字符串对象并调⽤ intern() ⽅法的时候,如
果已经有缓存的字符串,就会返回缓存⾥的实例,否则将其缓存起来。⼀般来
说,JVM 会将所有的类似“abc”这样的⽂本字符串,或者字符串常量之类缓存起
来。
看起来很不错是吧?但实际情况估计会让你⼤跌眼镜。⼀般使⽤ Java 6 这种历
史版本,并不推荐⼤量使⽤ intern,为什么呢?魔⻤存在于细节中,被缓存的字
符串是存在所谓 PermGen ⾥的,也就是臭名昭著的“永久代”,这个空间是很有
限的,也基本不会被 FullGC 之外的垃圾收集照顾到。所以,如果使⽤不当,
OOM 就会光顾。
在后续版本中,这个缓存被放置在堆中,这样就极⼤避免了永久代占满的问题,
甚⾄永久代在 JDK 8 中被 MetaSpace(元数据区)替代了。⽽且,默认缓存⼤
⼩也在不断地扩⼤中,从最初的 1009,到 7u40 以后被修改为 60013。你可以
使⽤下⾯的参数直接打印具体数字,可以拿⾃⼰的 JDK ⽴刻试验⼀下。
-XX:+PrintStringTableStatistics
你也可以使⽤下⾯的 JVM 参数⼿动调整⼤⼩,但是绝⼤部分情况下并不需要调
整,除⾮你确定它的⼤⼩已经影响了操作效率。
-XX:StringTableSize=N
Intern 是⼀种显式地排重机制,但是它也有⼀定的副作⽤,因为需要开发者写代
码时明确调⽤,⼀是不⽅便,每⼀个都显式调⽤是⾮常麻烦的;另外就是我们很
难保证效率,应⽤开发阶段很难清楚地预计字符串的重复情况,有⼈认为这是⼀
种污染代码的实践。
幸好在 Oracle JDK 8u20 之后,推出了⼀个新的特性,也就是 G1 GC 下的字
符串排重。它是通过将相同数据的字符串指向同⼀份数据来做到的,是 JVM 底
层的改变,并不需要 Java 类库做什么修改。
注意这个功能⽬前是默认关闭的,你需要使⽤下⾯参数开启,并且记得指定使⽤
G1 GC:
-XX:+UseStringDeduplication
前⾯说到的⼏个⽅⾯,只是 Java 底层对字符串各种优化的⼀⻆,在运⾏时,字
符串的⼀些基础操作会直接利⽤ JVM 内部的 Intrinsic 机制,往往运⾏的就是特
殊优化的本地代码,⽽根本就不是 Java 代码⽣成的字节码。Intrinsic 可以简单
理解为,是⼀种利⽤ native ⽅式 hard-coded 的逻辑,算是⼀种特别的内联,
很多优化还是需要直接使⽤特定的 CPU 指令,具体可以看相关源码,搜
索“string”以查找相关 Intrinsic 定义。当然,你也可以在启动实验应⽤时,使⽤
下⾯参数,了解 intrinsic 发⽣的状态。
-XX:+PrintCompilation -XX:+UnlockDiagnosticVMOptions -XX:+PrintInlining
// 样例输出⽚段
180 3 3 java.lang.String::charAt (25 bytes)
@ 1 java.lang.String::isLatin1 (19 bytes)

@ 7 java.lang.StringUTF16::getChar (60 bytes) intrinsic
可以看出,仅仅是字符串⼀个实现,就需要 Java 平台⼯程师和科学家付出如此
⼤且默默⽆闻的努⼒,我们得到的很多便利都是来源于此。
我会在专栏后⾯的 JVM 和性能等主题,详细介绍 JVM 内部优化的⼀些⽅法,
如果你有兴趣可以再深⼊学习。即使你不做 JVM 开发或者暂时还没有使⽤到特
别的性能优化,这些知识也能帮助你增加技术深度。
3.String ⾃身的演化
如果你仔细观察过 Java 的字符串,在历史版本中,它是使⽤ char 数组来存数
据的,这样⾮常直接。但是 Java 中的 char 是两个 bytes ⼤⼩,拉丁语系语⾔
的字符,根本就不需要太宽的 char,这样⽆区别的实现就造成了⼀定的浪费。
密度是编程语⾔平台永恒的话题,因为归根结底绝⼤部分任务是要来操作数据
的。
其实在 Java 6 的时候,Oracle JDK 就提供了压缩字符串的特性,但是这个特
性的实现并不是开源的,⽽且在实践中也暴露出了⼀些问题,所以在最新的
JDK 版本中已经将它移除了。
在 Java 9 中,我们引⼊了 Compact Strings 的设计,对字符串进⾏了⼤⼑阔
斧的改进。将数据存储⽅式从 char 数组,改变为⼀个 byte 数组加上⼀个标识
编码的所谓 coder,并且将相关字符串操作类都进⾏了修改。另外,所有相关的
Intrinsic 之类也都进⾏了重写,以保证没有任何性能损失。
虽然底层实现发⽣了这么⼤的改变,但是 Java 字符串的⾏为并没有任何⼤的变
化,所以这个特性对于绝⼤部分应⽤来说是透明的,绝⼤部分情况不需要修改已
有代码。
当然,在极端情况下,字符串也出现了⼀些能⼒退化,⽐如最⼤字符串的⼤⼩。
你可以思考下,原来 char 数组的实现,字符串的最⼤⻓度就是数组本身的⻓度
限制,但是替换成 byte 数组,同样数组⻓度下,存储能⼒是退化了⼀倍的!还
好这是存在于理论中的极限,还没有发现现实应⽤受此影响。
在通⽤的性能测试和产品实验中,我们能⾮常明显地看到紧凑字符串带来的优
势,即更⼩的内存占⽤、更快的操作速度。
今天我从 String、StringBuffer 和 StringBuilder 的主要设计和实现特点开始,
分析了字符串缓存的 intern 机制、⾮代码侵⼊性的虚拟机层⾯排重、Java 9 中
紧凑字符的改进,并且初步接触了 JVM 的底层优化机制 intrinsic。从实践的⻆
度,不管是 Compact Strings 还是底层 intrinsic 优化,都说明了使⽤ Java 基
础类库的优势,它们往往能够得到最⼤程度、最⾼质量的优化,⽽且只要升级
JDK 版本,就能零成本地享受这些益处。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值