String-StringBuffer-StringBuilder详解
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
,str4
所new
出来的字符串对象分别在堆中开辟一块独立的空间,即他们的地址不相同。
但是,在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次之后,这段代码所占的资源要比上面小得多。
通过我们比较源码发现,StringBuilder
和StringBuffer
都继承自AbstractStringBuilder
抽象类,所以StringBuilder
和StringBuffer
类拥有的成员属性以及成员方法基本相同
不同的是:StirngBuffer
在所有的方法上加上了Synchronized
关键字,进行同步操作,所以是线程安全的
①Stringbuffer
的append
方法
@Override
public synchronized StringBuffer append(String str) {
toStringCache = null;
super.append(str);
return this;
}
②StringBuilder
的append
方法
@Override
public StringBuilder append(String str) {
super.append(str);
return this;
}
String | StringBuffer | StringBuilder |
---|---|---|
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操作
我们看到还是调用父类的AbstractStringBuilder
的append
的方法
@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"的字符串对象,并将堆中的对象与之联系起来