天天看点

String是如何保证不可变的

问题:

  1. String是如何保证不可变的?
  2. String为什么要设计为不可变的?
  3. StringBuffer为什么可变?底层逻辑是什么?
  4. StringBuffer是如何进行扩容的?

String是如何保证不可变的?

String类被final修饰,不能被继承。底层采用一个 private final char value[] 数组进行存储,并且使用final修饰,所以String是不可变的。

  1. final修饰的不能被继承;
  2. final修饰的变量赋值后不能改变;
  3. final修饰的方法不能重写;
String是如何保证不可变的

但是这里要明确一点,字符数组被 final 修饰,不能改变的是变量的引用地址,并不是字符数组的中的元素。可以对字符数组中的元素进行改变。

public static void main(String[] args) throws Exception {
    final char[] chars = new char[]{'A', 'B', 'C'};
    chars[0] = 'D';
    System.out.println(chars);
}           

既然字符数组是可变的,那为什么String又是不可变的呢?其实更多的是在底层实现的。通过源码可以发现 char 数组是 private 修饰的,并且没有对外部提供任何修改 char 数组的方法。String被 final 修饰不可被继承,也无法通过继承重写方法的方式进行破坏。通过这三种手段保证了 String 的不可变性。

String 为什么设计为不可变的?

更容易实现字符串池

字符串池是用来存储Java常量的一块空间。主要用来缓存和重用字符串对象,从而提高性能和节省内存。当我们声明一个字符串常量时,会先检查字符串池是否存在相同内容的字符串常量。如果存在,则直接返回字符串池中的对象引用;如果不存在,则在字符串池中创建一个新的字符串对象,并返回对象的引用。

保证多线程安全

并发场景下,多个线程读取同一个资源并不会引发线程问题的。但是多个线程对同一资源进行写操作是不安全的,所以 String 通过它的不可变性,从而保证了多线程的安全问题。

避免安全问题

在网络连接和数据库连接中经常使用字符串作为参数,例如,网络连接地址URL,反射机制所需要的 String 参数,其不变性可以保证连接的安全性。如果字符串是可变的,那么可以通过修改字符串指向对象的值进行破坏,可能会引起严重的安全问题。

加快字符处理速度

因为 String 不可变,保证了 hashcode 的唯一性,那么在创建对象时就可以将 hashcode 缓存,不需要重新参与计算。这也是 Map 喜欢使用 String 作为 Key 的原因,处理速度比其他的键对象要快。所以 HashMap 经常选用 String 作为键。

StringBuilder:

非线程安全,StringBuilder底层采用一个可变的 char[] 数组。也就是说StringBuilder底层这个 char 数组是可以进行扩容的。

改变字符串的底层逻辑:

StringBuilder 使用了 append() 进行了字符串的拼接,底层是使用了数组拷贝的方式进行数据的赋值,将要拼接的字符数组 copy 到原字符数组中去,并且会在 copy 之前校验原数组是否需要扩容。

public AbstractStringBuilder append(String str) {
    if (str == null)
        return appendNull();
    int len = str.length();
  // 检验是否需要扩容
    ensureCapacityInternal(count + len);
  // 数组copy
    str.getChars(0, len, value, count);
    count += len;
    return this;
}

public void getChars(int srcBegin, int srcEnd, char dst[], int dstBegin) {
    if (srcBegin < 0) {
        throw new StringIndexOutOfBoundsException(srcBegin);
    }
    if (srcEnd > value.length) {
        throw new StringIndexOutOfBoundsException(srcEnd);
    }
    if (srcBegin > srcEnd) {
        throw new StringIndexOutOfBoundsException(srcEnd - srcBegin);
    }
    System.arraycopy(value, srcBegin, dst, dstBegin, srcEnd - srcBegin);
}           

StringBuilder扩容:

StringBuilder在初始化的时候,默认是char数组长度是16,每次扩容是原char数组长度的是 2n+2。

StringBuffer:

线程安全,相比 StringBuilder 来说在原来的方法上使用了 synchronized 来保证线程安全。

性能比较:

StringBuilder > StringBuffer > String

总结:

如果遇到大量的字符串拼接的时候,优先选择使用StringBuilder或StringBuffer。 String是字符串常量,不可变,使用String进行字符串操作的时候,每次都会创建一个新的对象,原来的对象就会变成垃圾被GC回收掉,很影响效率。 StringBuilder和StringBuffer它们是字符串变量,是可变的。当对字符串进行操作时,实际上实在对象上操作,并不会像String一样每次创建新的对象。

继续阅读