String-StringBuffer-StringBuilder详解

Lou.Chen
大约 12 分钟

String-StringBuffer-StringBuilder区别

一、String

public final class String
    implements java.io.Serializable, Comparable<String>, CharSequence {
    /** The value is used for character storage. */
    private final char value[];
    ......
}

通过以上代码,我们可以发现 String的内部是由一个被final的char数组实现的,即字符串不可修改

**注:**被final修饰的类不可继承,被final修饰的方法不能被重载,被final修饰的成员变量只能赋值一次,不能再次修改。

public String concat(String str) {
        int otherLen = str.length();
        if (otherLen == 0) {
            return this;
        }
        int len = value.length;
        char buf[] = Arrays.copyOf(value, len + otherLen);
        str.getChars(buf, len);
        return new String(buf, true);
    }
}

public String replace(char oldChar, char newChar) {
        if (oldChar != newChar) {
            int len = value.length;
            int i = -1;
            char[] val = value; /* avoid getfield opcode */

            while (++i < len) {
                if (val[i] == oldChar) {
                    break;
                }
            }
            if (i < len) {
                char buf[] = new char[len];
                for (int j = 0; j < i; j++) {
                    buf[j] = val[j];
                }
                while (i < len) {
                    char c = val[i];
                    buf[i] = (c == oldChar) ? newChar : c;
                    i++;
                }
                return new String(buf, true);
            }
        }
        return this;
}

我们从以上两个方法字符串的操作方法中可以看出,无论是concat 还是replace操作都会生成一个new一个新字符串。

结论:“对String对象的任何改变都不影响到原对象,相关的任何change操作都会生成新的对象”。

String的存储原理:
public class Main {
         
    public static void main(String[] args) {
        String str1 = "hello world";
        String str3 = "hello world";
        String str2 = new String("hello world");
        String str4 = new String("hello world");
         
        System.out.println(str1==str2); //false
        System.out.println(str1==str3); //true
        System.out.println(str2==str4); //false
    }
}

首先str1,str3直接创建的字符串常量都存储在堆中的字符串常量区,他们都指向同一内存空间。

str2,str4new出来的字符串对象分别在堆中开辟一块独立的空间,即他们的地址不相同。

但是,在new String("hello world")时,jvm会先去字符串常量区查看是否存在该创建的字符串hello world,若不存在,则在字符串常量池中创建该hello world字符串,并将该实例指向该字符串。若存在,该实例直接指向该字符串变量。

二、StringBuffer、StringBuilder

1、String字符串累加操作(原理)

public class Test{
    public static void main(String[] args) {
        String str = "";
        for (int i = 0; i < 1000; i++) {
            str += "hello";
        }
    }
}

这句 string += "hello";的过程相当于将原有的string变量指向的对象内容取出与"hello"作字符串相加操作再存进另一个新的String对象当中,再让str变量指向新生成的对象

通过反编译,我们发现,每次循环会new出一个StringBuilder对象,然后进行append操作,最后通过toString方法返回String对象。也就是说这个循环执行完毕new出了10000个对象,试想一下,如果这些对象没有被回收,会造成多大的内存资源浪费。从上面还可以看出:string+="hello"的操作事实上会自动被JVM优化成:

String+="hello"的操作事实上会自动被JVM优化成:

StringBuilder s = new StringBuilder(str);
s.append("hello");
String res=s.toString();

这就是为什么String字符串直接相加会造成空间的浪费,它每次回在字符串相加的时候new一个StringBuilder对象

2、StringBuilder和StringBuffer对比

public class Test{
    public static void main(String[] args) {
        StringBuilder sb=new StringBuilder();
        for (int i = 0; i < 1000; i++) {
            sb.append("hello");
        }
    }
}

new操作只进行了一次,也就是说只生成了一个对象,append操作是在原有对象的基础上进行的。因此在循环了10000次之后,这段代码所占的资源要比上面小得多。

通过我们比较源码发现,StringBuilderStringBuffer都继承自AbstractStringBuilder抽象类,所以StringBuilderStringBuffer类拥有的成员属性以及成员方法基本相同

不同的是StirngBuffer在所有的方法上加上了Synchronized关键字,进行同步操作,所以是线程安全的

Stringbufferappend方法

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

StringBuilderappend方法

@Override
public StringBuilder append(String str) {
        super.append(str);
        return this;
}
StringStringBufferStringBuilder
final修饰,不可继承final修饰,不可继承final修饰,不可继承
字符串常量,创建后不可变字符串变量,可动态修改字符串变量,可动态修改
不存在线程安全问题线程安全,所有public方法由synchronized修改线程不安全
大量字符串拼接效率最低大量字符串拼接效率非常高大量字符串拼接效率最高

3、StirngBuilder/StringBuffer实现原理

这里我们介绍StringBuilder:

内部可变数组,存在初始化StringBuilder对象中字符数组容量为16,存在扩容

  • AbstractStringBuilder
char[] value; //数组全部的容量
int count //已经使用的容量
一、StringBuilder
①空参数构造器

创建时不指定容量大小 默认容量为16

public StringBuilder() {
        super(16);
}
②自定义初始容量-构造函数

传入指定容量大小

public StringBuilder(int capacity) {
        super(capacity);
}
③以字符串String 作为参数的构造
public StringBuilder(String str) {
        super(str.length() + 16);
        append(str);
}
④append操作

我们看到还是调用父类的AbstractStringBuilderappend的方法

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

父类AbstractStringBuilder

public AbstractStringBuilder append(String str) {
    //若传入的为null
        if (str == null)
            //直接将null作为字符串存入
            return appendNull();
        int len = str.length();
    	//①扩容
        ensureCapacityInternal(count + len);
    	//②将目标字符串赋值到字符数组中
        str.getChars(0, len, value, count);
        count += len;
        return this;
}

ensureCapacityInternal扩容:

private void ensureCapacityInternal(int minimumCapacity) {
        // overflow-conscious code
       //如果当前要存入的 '字符串的长度+已经使用的长度>当前数组容量'  就扩容	
        if (minimumCapacity - value.length > 0) {
            value = Arrays.copyOf(value,
                    //扩容新数组的实际长度
                    newCapacity(minimumCapacity));
        }
}

扩容数量:

newCapacity

private int newCapacity(int minCapacity) {
        // 直接扩容 当前数组容量的2倍+2
        int newCapacity = (value.length << 1) + 2;
        //若扩容之后还不够
        if (newCapacity - minCapacity < 0) {
            //直接把当前的 传入的字符串长度+已经使用的容量 作为新扩容的容量长度
            newCapacity = minCapacity;
        }
        return (newCapacity <= 0 || MAX_ARRAY_SIZE - newCapacity < 0)
            ? hugeCapacity(minCapacity)
            : newCapacity;
}

调用Arrays.copyOf方法,直接将当前字符数组,加入到另一个扩容好的字符数组中

⑤字符数组扩容完毕之后,再将字符串加入扩容好的数组

str.getChars(0, len, value, count);

			// 要赋值字符串的起始位置;  结束位置(最后一个字符);目标数组;目标数组的起始位置
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);
}
⑥toString()方法
@Override
    public String toString() {
        // Create a copy, don't share the array
        return new String(value, 0, count);
    }

这里的toString方法直接new 一个String对象,将StringBuilder对象的value进行一个拷贝,重新生成一个对象,不共享之前StringBuilder的char[]

二、StringBuffer

StringBuffer这里的实现和StringBuilder一致,不同的是在stringBuffer中每个方法加上Synchronized同步方法

在StringBuffer中:

//用于保存toString()方法的缓存 更改是设置清除为null
private transient char[] toStringCache;
 @Override
    public synchronized StringBuffer append(String str) {
        //每次修改时 缓存设置为null
        toStringCache = null;
        super.append(str);
        return this;
    }
@Override
    public synchronized String toString() {
        if (toStringCache == null) {
            //如果被修改即为null,然后重新赋值给缓存
            toStringCache = Arrays.copyOfRange(value, 0, count);
        }
        //没有修改,直接返回缓存中的值
        return new String(toStringCache, true);
}

​ 这里的作用就是如果StringBuffer对象此时存在toStringCache,在多次调用其toString方法时,其new出来的String对象是会共享同一个char[] 内存的,达到共享的目的。但是StringBuffer只要做了修改,其toStringCache属性值都会置null处理。这也是StringBuffer和StringBuilder的一个区别点。

4、总结

String 类不可变,内部维护的char[] 数组长度不可变,为final修饰,String类也是final修饰,不存在扩容。字符串拼接,截取,都会生成一个新的对象。频繁操作字符串效率低下,因为每次都会生成新的对象。

​ StringBuilder 类内部维护可变长度char[] , 初始化数组容量为16,存在扩容, 其append拼接字符串方法内部调用System的native方法,进行数组的拷贝,不会重新生成新的StringBuilder对象。非线程安全的字符串操作类, 其每次调用 toString方法而重新生成的String对象,不会共享StringBuilder对象内部的char[],会进行一次char[]的copy操作。

​ StringBuffer 类内部维护可变长度char[], 基本上与StringBuilder一致,但其为线程安全的字符串操作类,大部分方法都采用了Synchronized关键字修改,以此来实现在多线程下的操作字符串的安全性。其toString方法而重新生成的String对象,会共享StringBuffer对象中的toStringCache属性(char[]),但是每次的StringBuffer对象修改,都会置null该属性值。

三、不同场景下的性能测试

public class Test1 {
    public static final Integer TIME=100000;
    public static void main(String[] args) {
            testString();
            testString01();
            testString02();
            testStringBuffer();
            testStringBuilder();
    }

    public static void testString() {
        String s = "";
        long begin=System.currentTimeMillis();
        for (int i = 0; i <TIME; i++) {
            s += "java";
        }
        long end=System.currentTimeMillis();
        System.out.println(s.getClass().getName()+"执行的时间为:"+(end-begin)+"毫秒");
    }

    public static void testString01() {
        long begin=System.currentTimeMillis();
        for (int i = 0; i <TIME; i++) {
            String s = "i"+"like"+"you";
        }
        long end=System.currentTimeMillis();
        System.out.println("字符串直接相加"+"执行的时间为:"+(end-begin)+"毫秒");
    }

    public static void testString02() {
        String s1 = "i";
        String s2 = "like";
        String s3 = "you";
        long begin=System.currentTimeMillis();
        for (int i = 0; i <TIME; i++) {
            String s = s1+s2+s3;
        }
        long end=System.currentTimeMillis();
        System.out.println("字符串间接相加"+"执行的时间为:"+(end-begin)+"毫秒");
    }

    public static void testStringBuffer() {
        StringBuffer sb=new StringBuffer();
        long begin=System.currentTimeMillis();
        for (int i = 0; i <TIME; i++) {
            sb.append("java");
        }
        long end=System.currentTimeMillis();
        System.out.println(sb.getClass().getName()+"执行的时间为:"+(end-begin)+"毫秒");
    }

    public static void testStringBuilder() {
        StringBuilder sb=new StringBuilder();
        long begin=System.currentTimeMillis();
        for (int i = 0; i <TIME; i++) {
            sb.append("java");
        }
        long end=System.currentTimeMillis();
        System.out.println(sb.getClass().getName()+"执行的时间为:"+(end-begin)+"毫秒");
    }


}

结果==>

java.lang.String执行的时间为:13141毫秒
字符串直接相加执行的时间为:2毫秒
字符串间接相加执行的时间为:6毫秒
java.lang.StringBuffer执行的时间为:3毫秒
java.lang.StringBuilder执行的时间为:3毫秒

我们从上面可以知道,String直接进行s += "java"的操作,jvm帮我们编译成:

StringBuilder sb = new StringBuilder(s);
sb.append("java");
s=sb.toString();

所以时间会更慢。

结论:

从上面的结果我们可以看出:

①对于直接相加字符串,效率很高,因为在编译器便确定了它的值,也就是说形如"I"+"like"+"you"; 的字符串相加,在编译期间便被优化成了"ilikeyou"。这个可以用javap -c命令反编译生成的class文件进行验证。

对于间接相加(即包含字符串引用),形如s1+s2+s3; 效率要比直接相加低,因为在编译器不会对引用变量进行优化。

②String、StringBuilder、StringBuffer三者的执行效率:

StringBuilder > StringBuffer > String

当然这个是相对的,不一定在所有情况下都是这样。

比如String str = "hello"+ "world"的效率就比 StringBuilder st = new StringBuilder().append("hello").append("world")要高。

因此,这三个类是各有利弊,应当根据不同的情况来进行选择使用:

  • 当字符串相加操作或者改动较少的情况下,建议使用 String str="hello"这种形式;

  • 当字符串相加操作较多的情况下,建议使用StringBuilder,如果采用了多线程,则使用StringBuffer。

四、常见String面试题踩坑

1、输出代码结果是什么?

String a = "hello2";  
String b = "hello" + 2;
System.out.println((a == b)); //true

输出结果为:true

hello+"2"在编译期间就被优化成hello2,因此在运行期间,变量a和变量b指向的是同一个对象。

2、下面这段代码的输出结果是什么?

String a = "hello2";  
String b = "hello";       
String c = b + 2;       
System.out.println((a == c));

输出结果:false

变量c的结果,我们在之前的叙述可以知道,String c=b+2 被jvm编译为:

StringBuilder s = new StringBuilder(b);
s.append(2);
s.toString(); //重新覆给变量c

相当于重新在堆中创建一块区域。不会在编译期间被优化,不会把b+2当做字面常量来处理的,因此这种方式生成的对象事实上是保存在堆上的。因此a和c指向的并不是同一个对象。

3、下面这段代码的输出结果是什么?

String a = "hello2";
final String b = "hello";       
String c = b + 2;       
System.out.println((a == c));

输出结果:true

对final变量(该变量能够在编译期确定)的访问在编译期间都会直接被替代为真实的值。那么String c = b + 2;在编译期间就会被优化成:String c = "hello" + 2;

4、下面这段代码的输出结果是什么?

public class Main {
    public static void main(String[] args) {
        String a = "hello2";
        final String b = getHello();
        String c = b + 2;
        System.out.println((a == c));
    }
     
    public static String getHello() {
        return "hello";
    }
}

输出结果:false

虽然被b被final修饰,但是b的值不能在编译期被确定,由于其赋值是通过方法调用返回的,那么它的值只能在运行期间确定。所以a不等于c

5、下面这段代码的输出结果是什么?

public class Main {
    public static void main(String[] args) {
        String a = "hello";
        String b =  new String("hello");
        String c =  new String("hello");
        String d = b.intern();
         
        System.out.println(a==b);
        System.out.println(b==c);
        System.out.println(b==d);
        System.out.println(a==d);
    }
}

输出结果:

false
false
false
true

这里面涉及到的是String.intern方法的使用。在String类中,intern方法是一个本地方法intern方法会在字符串常量池中查找是否存在内容相同的字符串,如果存在则返回指向该字符串的引用,如果不存在,则会将该字符串入池,并返回一个指向该字符串的引用。因此,a和d指向的是同一个对象。

6、String str = new String("abc")创建/涉及了多少个对象?

该段代码执行过程和类的加载过程是有区别的。在类加载的过程中,确实在运行时常量池中创建了一个"abc"对象,而在代码执行过程中确实只创建了一个String对象。

  • 如果原字符串常量池中没有“abc”,则涉及到了两个String对象 ,通过反编译我们发现只new了一次,创建了一个对象

  • 如果原字符串常量池中有“abc",则创建了一个对象,new了一次

①首先在堆中(不是常量池)创建一个指定的对象"abc",并让str引用指向该对象 ②在字符串常量池中查看,是否存在内容为"abc"字符串对象 ③若存在,则将new出来的字符串对象与字符串常量池中的对象联系起来 ④若不存在,则在字符串常量池中创建一个内容为"abc"的字符串对象,并将堆中的对象与之联系起来