阿里云面试:理解 Java 中 String 不可变性的重要性及其背后的机制:深入分析 String、StringBuffer 和 StringBuilder 的区别

本文将探讨在阿里云面试中出现的一道 Java 基础面试题:“StringStringBufferStringBuilder 三者之间的区别,以及为什么 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 不可变的原因有以下几点:

  1. 保存字符串的字符数组被 final 修饰且为私有属性,同时 String 类没有提供修改该字符串的方法。
  2. String 类被 final 修饰,无法被继承,从而避免了子类可能造成的不可变性破坏。

相关阅读:对于如何理解 String 类型值的不可变性,可以参考知乎的相关讨论[1]。

补充信息(来自 issue 675[2]):在 Java 9 之后,StringStringBuilderStringBuffer 的实现都采用了 byte 数组来存储字符串。

Java 9 中 String 底层实现为何由 char[] 改为 byte[]

新版的 String 支持两种编码方案:Latin-1 和 UTF-16。如果字符串中包含的字符在 Latin-1 的表示范围内,则会使用 Latin-1 编码。Latin-1 编码下,byte 仅占用一个字节(8 位),而 char 占用两个字节(16 位),因此在内存利用上,bytechar 节省了一半的空间。

如果字符串中包含的字符超出了 Latin-1 的表示范围,则在存储空间上 bytechar 是等量的。这是官方的介绍:Java 9 JEP 254

StringBuilderStringBuffer 皆继承自 AbstractStringBuilder 类,该类同样使用字符数组保存字符串,但不使用 finalprivate 修饰,且提供了多种修改字符串的方法,例如 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 中的对象是不可变的,因此可以视为常量,因此它是线程安全的。而 AbstractStringBuilderStringBuilderStringBuffer 的父类,定义了一些基本的字符串操作方法如 expandCapacityappendinsertindexOf 等。StringBuffer 对方法加了同步锁,因此是线程安全的;相比之下,StringBuilder 则未添加同步锁,因此不是线程安全的。

性能对比

每次对 String 类型进行修改时,都会生成一个新的 String 对象,同时将指针指向新的对象。而 StringBuffer 则是对自身对象进行操作,而不生成新对象,改变对象引用。在相同情境下,使用 StringBuilder 相较于 StringBuffer 仅能提高大约 10%~15% 的性能,但也带来了多线程不安全的风险。

三者使用的总结

  1. 适用于操作少量数据时:使用 String
  2. 单线程下对字符串缓冲区进行大量数据操作时:使用 StringBuilder
  3. 多线程下对字符串缓冲区进行大量数据操作时:使用 StringBuffer

参考资料

[1] 如何理解 String 类型值的不可变性? - 知乎提问: https://www.zhihu.com/question/20618891/answer/114125846

[2] issue 675: https://github.com/Snailclimb/JavaGuide/issues/675