【Java基础】✅String、StringBuilder和StringBuffer的深度分析

首先一句话

String是不可变的,StringBuilder和StringBuffer是可变的,StringBuffer是线程安全的,而StringBuilder是非线程安全的。


String的不可变性

存储结构

String对象在内部使用一个 char 数组来存储字符串数据。从Java 9开始,String 类使用 byte 数组加上一个编码标记(coder)来存储字符串,以更有效地处理不同的字符集。但不管是哪种存储方式,这个数组被声明为 final,这意味着一旦数组被初始化,它的引用就不能指向另一个数组。

构造过程

当你创建一个新的 String 对象时,例如通过字符串字面量(如 “hello”)或通过构造函数,Java 会在内存中创建一个新的 String 对象,并初始化这个内部数组。因为内部数组是 final 的,所以一旦字符串被赋值,就无法更改这个数组的内容。

操作行为

当你对字符串进行任何修改的操作,如拼接、替换字符等,Java 实际上并不是修改原有的字符串,而是创建了一个新的 String 对象。例如,使用 + 操作符连接两个字符串时,Java 实际上会在内存中创建一个新的 String 对象来保存结果,原来的字符串对象不会被改变。

String s1 = "hello";
String s2 = s1 + " world";

在这个例子中,s1 指向 “hello” 字符串的内存位置。当执行连接操作 s1 + " world" 时,实际上会在内存中创建一个新的字符串 “hello world”,而 s1 仍然指向原来的 “hello”。这就保证了 s1 的不变性。

为什么要设计成不可变的?

主要有以下方面考虑:

  • 安全性:因为字符串不可变,所以它们可以安全地用在多线程环境中,无需担心并发修改。
  • 缓存:不变性允许 String 对象被缓存,例如字符串字面量都是缓存并重用的。
  • 使用为哈希键:不变性保证了字符串的哈希码(hashCode)是常量,这使得字符串非常适合作为哈希表的键。
  • 性能:因为字符串不可变,所以可以用字符串池缓存,可以大大节省堆内存。而且还可以提前对哈希码(hashCode)进行缓存,更加高效,由于字符串是应用最广泛的数据结构,提高字符串的性能对提高整个应用程序的总体性能有相当大的影响。


StringBuilder的可变性、非线程安全性

StringBuilder简化源码如下:

public final class StringBuilder implements java.io.Serializable, CharSequence {
    // 字符数组,用于存储字符串内容
    private char[] value;
    
    // 当前StringBuilder中字符的数量
    private int count;
    
    // 构造方法,初始化StringBuilder对象
    public StringBuilder() {
        value = new char[16]; // 初始容量为16
    }

    // 向StringBuilder中追加字符
    public StringBuilder append(String str) {
        // 检查字符数组容量是否足够,如果不够则进行扩容
        int len = str.length();
        ensureCapacityInternal(count + len);
        str.getChars(0, len, value, count);
        count += len;
        return this;
    }

    // 扩容方法,保证字符数组容量足够
    private void ensureCapacityInternal(int minimumCapacity) {
        // 如果当前字符数组的长度不够,则进行扩容
        if (minimumCapacity - value.length > 0)
            expandCapacity(minimumCapacity);
    }

    // 扩容方法,将字符数组容量扩大一倍
    private void expandCapacity(int minimumCapacity) {
        int newCapacity = value.length * 2 + 2;
        if (newCapacity - minimumCapacity < 0)
            newCapacity = minimumCapacity;
        if (newCapacity < 0) {
            if (minimumCapacity < 0) // overflow
                throw new OutOfMemoryError();
            newCapacity = Integer.MAX_VALUE;
        }
        value = Arrays.copyOf(value, newCapacity);
    }
}

可变性:StringBuilder的可变性体现在其内部的字符数组(char[] value)上,且没有用final进行修饰。在初始化StringBuilder时,会创建一个指定初始容量的字符数组,并且随着字符的追加、插入或删除操作,该字符数组的长度可能会动态变化,但是StringBuilder对象本身并不会改变。这种设计使得可以在同一个StringBuilder对象上执行多个操作,而不会创建新的对象,从而提高了性能。

线程不安全性:StringBuilder的线程不安全性主要源自于其内部状态的修改没有进行同步控制。在StringBuilder的实现中,并没有对字符数组的修改进行同步操作(即没有synchronized修饰),因此当多个线程同时访问和修改同一个StringBuilder实例时,可能会导致竞态条件(race condition)的发生,进而产生不可预测的结果。这种情况下,如果一个线程正在执行修改操作,而另一个线程同时进行了修改或读取操作,可能会导致数据不一致或异常。


StringBuffer的可变性、线程安全性

同样上简化源码进行分析:

public final class StringBuffer implements java.io.Serializable, CharSequence {
    // 字符数组,用于存储字符串内容
    private char[] value;
    
    // 当前StringBuffer中字符的数量
    private int count;
    
    // 构造方法,初始化StringBuffer对象
    public StringBuffer() {
        synchronized(this) {
            value = new char[16]; // 初始容量为16
        }
    }

    // 向StringBuffer中追加字符,方法使用synchronized关键字进行同步
    public synchronized StringBuffer append(String str) {
        // 检查字符数组容量是否足够,如果不够则进行扩容
        int len = str.length();
        ensureCapacityInternal(count + len);
        str.getChars(0, len, value, count);
        count += len;
        return this;
    }

    // 扩容方法,保证字符数组容量足够
    private void ensureCapacityInternal(int minimumCapacity) {
        // 如果当前字符数组的长度不够,则进行扩容
        if (minimumCapacity - value.length > 0)
            expandCapacity(minimumCapacity);
    }

    // 扩容方法,将字符数组容量扩大一倍,方法使用synchronized关键字进行同步
    private synchronized void expandCapacity(int minimumCapacity) {
        int newCapacity = value.length * 2 + 2;
        if (newCapacity - minimumCapacity < 0)
            newCapacity = minimumCapacity;
        if (newCapacity < 0) {
            if (minimumCapacity < 0) // overflow
                throw new OutOfMemoryError();
            newCapacity = Integer.MAX_VALUE;
        }
        value = Arrays.copyOf(value, newCapacity);
    }
}

可变性:可以发现,StringBuffer与StringBuilder非常相似,因此可变性的原理是一样的。

线程安全性:StringBuffer的方法是同步的,因此它是线程安全的。这种线程安全性是通过在StringBuffer的方法中使用synchronized关键字来实现的,这样就可以确保在多线程环境中对StringBuffer的操作是同步的,不会发生竞态条件。

性能:虽然这种同步机制确保了线程安全性,但也降低了性能,因为在多线程环境下可能会出现线程竞争,导致一些线程需要等待其他线程完成操作。


选择策略

选择合适的类取决于项目的需求,如果不确定是否需要线程安全性,或者仅在单线程环境下操作字符串,可以优先选择StringBuilder,因为它具有更好的性能。如果需要保证线程安全性,或者在多线程环境下操作字符串,可以选择StringBuffer。而对于不需要修改的字符串内容,则可以直接使用String。

  • 28
    点赞
  • 18
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值