字符串不可变性以及StringBuilder和StringBuffer在字符串拼接中的作用和扩容机制

发布时间:2024年01月18日

java字符串的不可变性

在jdk1.8及以前,字符串底层存储用的是一个字符(char)类型的数组,jdk1.9之后用的是整型中的字节型(byte)数组来存储字符串。以下下主要以jdk1.8为例子展开。

 private final char value[];
/*
底层的字符类型的数组声明时用private和final来修饰
private修饰代表在类之外不能直接操作,只能通过该对象的公共方法来操作,而公共方法最后都返回的是新字符串
final来修饰代表引用数据类型的地址不能变,也就是说只能修改里面的元素内容,但是在外部无法直接操作,只能借助类中的方法,但是方法都返回一个新的字符串
从这两个角度,字符串无法变,这就是字符串的不可变性
*/


对于一个方法的解析
public String substring(int beginIndex, int endIndex) {
        if (beginIndex < 0) {
            throw new StringIndexOutOfBoundsException(beginIndex);
        }
        if (endIndex > value.length) {
            throw new StringIndexOutOfBoundsException(endIndex);
        }
        int subLen = endIndex - beginIndex;
        if (subLen < 0) {
            throw new StringIndexOutOfBoundsException(subLen);
        }
        return ((beginIndex == 0) && (endIndex == value.length)) ? this
                : new String(value, beginIndex, subLen);
    }
 substring:字符串的截取, 当截取值就是整个字符串时,返回自己,没改变。如果不是new了一个新的String对象,原字符串没有发生改变。
通过这个方法可以了解到,/*无法在不改变内部代码的情况下,修改字符串的内容,那么字符串不可变*/

大量字符串拼接中StringBuilder和StringBuffer的作用

因为字符串的不可变性,字符串的拼接最终都是产生一个新的字符串,在大量拼接操作过程中就会产生大量的新字符串(临时字符串),将会短时间占用大量空间,导致空间的浪费。

StringBuilder、StringBuffer非相关拓展介绍:

StringBuilder:jdk1.5,非线程安全,稍慢

StringBuffer:jdk1.0,带线程锁(synchronized),线程安全,稍慢

下面以StringBuilder为例分析其在大量字符串拼接中的作用

StringBuilder类:
public final class StringBuilder
    extends AbstractStringBuilder
    implements java.io.Serializable, CharSequence

AbstractStringBuilder抽象类:
abstract class AbstractStringBuilder implements Appendable, CharSequence    
   
    
public StringBuilder(int capacity) {
        super(capacity);
}
AbstractStringBuilder(int capacity) {
        value = new char[capacity];
    }

/*
StringBuilder这个类继承AbstractStringBuilder抽象类,StringBuilder这个类中构造方法调用了抽象类的构造方法,创建了一个字符(char)类型的数组。
*/

至此我们可以创建一个类似于字符串的一个StringBuilder对象,并可以创建一个初始值,如果我在创建对象时使用的无参构造方法,底层会给我创建一个长度为16的字符类型的数组,作为成员会有默认值就是空字符
下面开始拼接操作,看字符串的拼接和StringBuilder对象来做拼接有什么不同
StringBuilder对象的拼接需要用到append方法
StringBuilder sc = new StringBuilder("123");
sc.append(",");

public StringBuilder append(String str) {
        super.append(str);
        return this;
    }
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;
    }
private AbstractStringBuilder appendNull() {
        int c = count;
        ensureCapacityInternal(c + 4);
        final char[] value = this.value;
        value[c++] = 'n';
        value[c++] = 'u';
        value[c++] = 'l';
        value[c++] = 'l';
        count = c;
        return this;
    }
/*
sc这个对象在调用时用了父类的append方法,将自己返回
父类中的append方法,首先判断了一下传入变量是否为null,是执行appendNull()方法将结果返回
appendNull()方法:首先判断是否需要扩容,用另一个变量指向对象的字符数组,将null拆分成字符添加到字符数组中,直接改变对象底层的字符数组。
接着如果添加的元素不是null,就执行字符串的getChars实际是System.arraycopy(value, srcBegin, dst, dstBegin, srcEnd - srcBegin);将一个数组中的元素复制到另一个数组,前面的数组是源数组,后一个是目标数组,将元素组中的元素,复制到目标数组。此操作仍是在对象的底层字符数组上直接操作。
所以整个一次拼接过程不产生临时额外数组,完美的解决了原始字符串拼接,每次拼接操作都会产生临时数据浪费资源的问题
*/

StringBuffer与StringBuilder拼接过程基本一致,只是它们一个线程安全效率稍低,一个非线程安全效率比较而言稍高。

StringBuffer与StringBuilder中的扩容机制

仍然以StringBuilder为例

StringBuilder有多种构造方法生成对象,无参的,有参传数值的,有参传字符串的等
以无参、有参传数值、有参传字符串这三种常见的创建对象的形式,了解其中的扩容机制
StringBuilder sc = new StringBuilder();
public StringBuilder() {
        super(16);
    }
AbstractStringBuilder(int capacity) {
        value = new char[capacity];
    }
/*
无参,调用抽象类的构造方法,创建一个长度为16的字符数组,初始值空字符
*/
StringBuilder sc1 = new StringBuilder(1024);
public StringBuilder(int capacity) {
        super(capacity);
    }
AbstractStringBuilder(int capacity) {
        value = new char[capacity];
    }
/*
传数值的这种,自己定义最初字符数组长度
*/

StringBuilder sc2 = new StringBuilder("123");
public StringBuilder(String str) {
        super(str.length() + 16);
        append(str);
    }
AbstractStringBuilder(int capacity) {
        value = new char[capacity];
    }
/*
传字符串这种,先调用抽象类的构造方法,得到一个字符串长度再加16的字符数组
只有调用append(str)方法,将字符串底层字符数组复制到对象底层维护的数组
*/
当以上三种方法创建出对象后做拼接操纵,会通过ensureCapacityInternal(count + len)方法来判断需不需要扩容
minimumCapacity=count + len,拼接后字符数组数据元素长度=原本的字符数组实际长度+要拼接字符串长度
当minimumCapacity - value.length > 0时,也就是拼接后字符数组元素长度>对象维护的数组长度时我们需要扩容value = Arrays.copyOf(value,newCapacity(minimumCapacity));通过newCapacity方法得到最终扩容后数组长度,通过Arrays.copyOf()复制对象维护数组值的同时开辟相应的空间,最终数组复制给对象维护字符数组。
newCapacity方法,首先在原对象维护数组长度基础上*2+2;此时看扩容后的长度是否满足最低要求,满足,扩容长度为原对象维护数组长度基础上*2+2,不满足则扩充最低要求长度,最终如果这个长度不小于等于0或者超过定义的最大长度,那么最终长度确定,否则和Integer.MAX_VALUE值作比较,比这个值都大,报异常内存溢出,否则和数组最大长度(MAX_ARRAY_SIZE)比较,如果小,就返回当前值,否则就把数组最大值返回。
文章来源:https://blog.csdn.net/qq_43717997/article/details/135685615
本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。