Skip to content

String类的源码如下,内容使用char数组存储value[]且使用final修饰,且类使用final修饰表示不可被继承。

java
public final class String implements java.io.Serializable, Comparable<String>, CharSequence {
    private final char value[];
    private int hash;
}

我们在代码中经常修改String类型的数据:

java
String s = "abcd";
s = s.concat("ef");

虽然字符串内容看上去从"abcd"变成了"abcdef",但是实际上,我们得到的已经是一个新的字符串了。如下图,在堆中重新创建了一个"abcdef"字符串,和"abcd"并不是同一个对象。

image-20230831203840683

String的"+"是如何实现的

还是这样一段代码。我们把他生成的字节码进行反编译,看看结果。

java
String wechat = "Hollis";
String introduce = "Chuang";
String hollis = wechat + "," + introduce;

反编译后的内容如下,通过查看反编译以后的代码,我们可以发现,原来字符串常量在拼接过程中,是将String转成了StringBuilder后,使用其append方法进行处理的。

java
String wechat = "Hollis";
String introduce = "Chuang";
String hollis = (new StringBuilder()).append(wechat).append(",").append(introduce).toString();

那么也就是说,Java中的+对字符串的拼接,其实现原理是使用StringBuilder.append。

StringBuffer和StringBuilder

原理和String类类似,StringBuilder类也封装了一个字符数组;与String不同的是,它并不是final的;由于字符数组中不一定所有位置都已经被使用,它有一个实例变量,表示数组中已经使用的字符个数,定义如下:

java
char[] value;
int count;

其append源码如下:

java
public StringBuilder append(String str) {
    super.append(str);
    return this;
}

该类继承了AbstractStringBuilder类,看下其append方法:

java
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;
}

append会直接拷贝字符到内部的字符数组中,如果字符数组长度不够,会进行扩展。StringBuffer和StringBuilder类似,最大的区别就是StringBuffer是线程安全的,看一下StringBuffer的append方法。该方法使用synchronized进行声明,说明是一个线程安全的方法。而StringBuilder则不是线程安全的。

java
public synchronized StringBuffer append(String str) {
    toStringCache = null;
    super.append(str);
    return this;
}

不要在for循环中使用+拼接字符串

java
long t1 = System.currentTimeMillis();
String str = "hollis";
for (int i = 0; i < 50000; i++) {
    String s = String.valueOf(i);
    str += s;
}
long t2 = System.currentTimeMillis();
System.out.println("+ cost:" + (t2 - t1));

将上述代码反编译,我们可以看到,反编译后的代码,在for循环中,每次都是new了一个StringBuilder,然后再把String转成StringBuilder,再进行append。

java
long t1 = System.currentTimeMillis();
String str = "hollis";
for(int i = 0; i < 50000; i++) {
    String s = String.valueOf(i);
    str = (new StringBuilder()).append(str).append(s).toString();
}

long t2 = System.currentTimeMillis();
System.out.println((new StringBuilder()).append("+ cost:").append(t2 - t1).toString());

频繁的新建对象要耗费很多时间,不仅仅会耗费时间,频繁的创建对象还会造成内存资源的浪费。

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

  1. 缓存:字符串是使用最广泛的数据结构,大量字符串的创建非常耗费资源,JVM中专门开辟了一部分空间来存储Java字符串,那就是字符串常量池。通过字符串池,两个内容相同的字符串变量,可以从池中指向同一个字符串对象,从而节省了关键的内存资源。之所以可以这么做,主要是因为字符串的不变性。试想一下,如果字符串是可变的,我们一旦修改了s的内容,那必然导致s2的内容也被动的改变了,这显然不是我们想看到的。
  2. 安全性:当我们在程序中传递一个字符串的时候,如果这个字符串的内容是不可变的,那么我们就可以相信这个字符串中的内容。如果是可变的,那么这个字符串内容就可能随时都被修改。那么这个字符串内容就完全不可信了。这样整个系统就没有安全性可言了。
  3. 线程安全:不可变会自动使字符串成为线程安全的,因为当从多个线程访问它们时,它们不会被更改。因为如果线程更改了值,那么将在字符串池中创建一个新的字符串,而不是修改相同的值。因此,字符串对于多线程来说是安全的。
  4. hashcode缓存:由于字符串对象被广泛地用作数据结构,它们也被广泛地用于哈希实现,在对这些散列实现进行操作时,经常调用hashCode()方法。不可变性保证了字符串的值不会改变。因此,hashCode()方法在String类中被重写,以方便缓存,这样在第一次hashCode()调用期间计算和缓存散列,并从那时起返回相同的值。
  5. 性能:因为字符串不可变,所以可以用字符串池缓存,可以大大节省堆内存。由于字符串是应用最广泛的数据结构,提高字符串的性能对提高整个应用程序的总体性能有相当大的影响。