阿里云面试:理解 Java 中 String 不可变性的重要性及其背后的机制:深入分析 String、StringBuffer 和 StringBuilder 的区别
阿里云面试:理解 Java 中 String 不可变性的重要性及其背后的机制:深入分析 String、StringBuffer 和 StringBuilder 的区别
本文将探讨在阿里云面试中出现的一道 Java 基础面试题:“String
、StringBuffer
、StringBuilder
三者之间的区别,以及为什么 String
是不可变的。”许多文章对此的解释往往不够准确,因此希望本篇文章能提供清晰的理解。此外,我们还将讨论 Java 9 为什么将 String
的底层实现从 char[]
改为 byte[]
。
下面开始正文内容。
可变性解析
String
类使用 final
关键字修饰的字符数组保存字符串,因而 String
对象是不可变的。其相关代码如下:
public final class String implements java.io.Serializable, Comparable<String>, CharSequence {
private final char value[];
//...
}
🐛 修正说明:
final
关键字限制了类的继承、方法的重写以及基本数据类型的值不可改变。对于引用类型的变量,final
仅防止其指向其他对象。因此,final
修饰的字符数组并不是String
不可变的根本原因,因为该数组的内容是可以被修改的。真正导致String
不可变的原因有以下几点:
- 保存字符串的字符数组被
final
修饰且为私有属性,同时String
类没有提供修改该字符串的方法。String
类被final
修饰,无法被继承,从而避免了子类可能造成的不可变性破坏。
相关阅读:对于如何理解 String
类型值的不可变性,可以参考知乎的相关讨论[1]。
补充信息(来自 issue 675[2]):在 Java 9 之后,String
、StringBuilder
和 StringBuffer
的实现都采用了 byte
数组来存储字符串。
Java 9 中 String
底层实现为何由 char[]
改为 byte[]
?
新版的 String
支持两种编码方案:Latin-1 和 UTF-16。如果字符串中包含的字符在 Latin-1 的表示范围内,则会使用 Latin-1 编码。Latin-1 编码下,byte
仅占用一个字节(8 位),而 char
占用两个字节(16 位),因此在内存利用上,byte
比 char
节省了一半的空间。
如果字符串中包含的字符超出了 Latin-1 的表示范围,则在存储空间上 byte
和 char
是等量的。这是官方的介绍:Java 9 JEP 254。
StringBuilder
和 StringBuffer
皆继承自 AbstractStringBuilder
类,该类同样使用字符数组保存字符串,但不使用 final
和 private
修饰,且提供了多种修改字符串的方法,例如 append
方法。
abstract class AbstractStringBuilder implements Appendable, CharSequence {
char[] value;
public AbstractStringBuilder append(String str) {
if (str == null)
return appendNull();
int len = str.length();
ensureCapacityInternal(count + len);
str.getChars(0, len, value, count);
count += len;
return this;
}
//...
}
线程安全性分析
由于 String
中的对象是不可变的,因此可以视为常量,因此它是线程安全的。而 AbstractStringBuilder
是 StringBuilder
和 StringBuffer
的父类,定义了一些基本的字符串操作方法如 expandCapacity
、append
、insert
和 indexOf
等。StringBuffer
对方法加了同步锁,因此是线程安全的;相比之下,StringBuilder
则未添加同步锁,因此不是线程安全的。
性能对比
每次对 String
类型进行修改时,都会生成一个新的 String
对象,同时将指针指向新的对象。而 StringBuffer
则是对自身对象进行操作,而不生成新对象,改变对象引用。在相同情境下,使用 StringBuilder
相较于 StringBuffer
仅能提高大约 10%~15% 的性能,但也带来了多线程不安全的风险。
三者使用的总结
- 适用于操作少量数据时:使用
String
- 单线程下对字符串缓冲区进行大量数据操作时:使用
StringBuilder
- 多线程下对字符串缓冲区进行大量数据操作时:使用
StringBuffer
参考资料
[1] 如何理解 String
类型值的不可变性? - 知乎提问: https://www.zhihu.com/question/20618891/answer/114125846
[2] issue 675: https://github.com/Snailclimb/JavaGuide/issues/675