您的位置:首页 > 产品设计 > UI/UE

Java的String、StringBuffer、StringBuilder

2017-09-18 16:05 281 查看

一、String的解析

1.1 问题的引入

相信许多人都遇到过这样的一道面试题

String s1 = "ab";
String s2 = "cd";
String s3 = "abcd";
String s4 = s1 + s2;
String s5 = "ab" + "cd";

System.out.println(s4 == s3);
System.out.println(s5 == s3);


由于==号比较的变量地址,因此这道题翻译一下就是s4所指向的地址与s3所指向的地址是否一致,s5所指向的地址与s3所指向的地址是否一致。

第一道题的答案是s4所指向的地址和s3是不一致的,具体为什么,我们可以借助smali看看在jvm中s4 = s1 + s2这段代码的真正执行逻辑

new-instance v5, Ljava/lang/StringBuilder;
invoke-direct {v5}, Ljava/lang/StringBuilder;-><init>()V
invoke-virtual {v5, v0}, Ljava/lang/StringBuilder;->append(Ljava/lang/String;)Ljava/lang/StringBuilder;
move-result-object v5
invoke-virtual {v5, v1}, Ljava/lang/StringBuilder;->append(Ljava/lang/String;)Ljava/lang/StringBuilder;
move-result-object v5
invoke-virtual {v5}, Ljava/lang/StringBuilder;->toString()Ljava/lang/String;
move-result-object v3
.line 12
.local v3, "s4":Ljava/lang/String;


在JVM中,s4 = s1 + s2这段代码并不只是简单的两个字符串相加,通过smali可以看出先是新建了一个StringBuilder对象,然后通过append方法将v0(“ab”),v1(“cd”)的值相加,然后再调用toString赋值给s4。因此s4等于是一个新建的对象,当然地址和s3也就不一致了。至于StringBuilder的概念我们下面再说。

第二道题的答案是s5所指向的地址和s3是一致的,这个结果首先要知道s5在jvm中是一个怎么样的解释方法,还是看smali代码:

const-string v4, "abcd"
.line 14
.local v4, "s5":Ljava/lang/String;


可以看到s5=”ab”+”cd”被直接翻译成了s5=”abcd”,这样就和s3拥有了一样的常量字符,这两个对象就指向了常量池中一样的”abcd”地址。至于常量池的概念,我们下面再说。

1.2 常量池的概念

通过上面第二道题的结果,我们引出了常量池这个概念。在Java编译好的Class文件中,有一个称之为Constant Pool的区域,它是一个数组构成的表,用来存储程序中的各种常量,包括Class、String、Integer等等各种基本的Java类型。String Pool是Constant Pool中存储String常量的区域。在运行过程中,常量池中的String会在堆中创建一个新的对象,当一个变量被创建时,会优先查找常量池中是否有相同的String,如果有,怎直接返回常量池中String指向的地址。这样就能解释上面的s5 == s3了。

1.3 intern方法

既然引出了常量池的概念,我们也说一说intern这个方法,先看一个例子,我们在上面那题的基础上,给出一题

String s6 = s4.intern();
System.out.println(s6 == s3);


这个的输出是s6和s3是一样的对象,也就是等号成立。看intern这个方法的官方doc注释:

* When the intern method is invoked, if the pool already contains a
* string equal to this {@code String} object as determined by
* the {@link #equals(Object)} method, then the string from the pool is
* returned. Otherwise, this {@code String} object is added to the
* pool and a reference to this {@code String} object is returned.


当这个方法调用时,会先优先查找String pool中是否有相等的字符串,如果有,则直接返回String pool中的地址,否则,则新建一个字符串到String pool中。

1.4 String是一个不可变类型

通过查看源码可知String类中用来存储字符串的数组是一个final类型

/** The value is used for character storage. */
private final char value[];


因此只能被一次复制,之后不能再修改。字符串的扩展操作必定是新建了一个对象,所以String是一个不可变的类型。至于String不可变的原因,是由于String在Java中被当做了类似基本类型来使用,使用频率特别高如果可以任意改变它指向的内存的值,很容易导致其源数据改变造成一些意料之外的运行结果,比如在网络传输时,在发送某段字符串的过程中,String之前指向的内存值被修改了,导致数据出现了改变这一就会造成意想不到的结果。因此每次对String的修改都创建了一个新的对象,开辟了一块新的内存地址,以避免这种情况。

二、StringBuffer

2.1 StringBuffer是什么

StringBuffer其实就是对String的可变化对象,上面已经说过String是一个不可变的对象,但是总有一些场景会出现不可变对象会造成效率的降低,因此就需要一个StringBuffer对象用来弥补String的不足,可以看到StringBuffer的成员变量中,用来存储字符串(声明在AbstractStringBuilder中)的变量已经不是final了,意味着它可以进行扩展,删除。

/**
* The value is used for character storage.
*/
char[] value;


除此之外,StringBuffer是线程安全的,所有的公开方法都加上了
synchronized
字段,这样就可以避免多线程操作造成的不可预知的结果。

2.2 为什么要StringBuffer

看下面一段代码

String s1 = "";
for (int i = 0; i < 1000; i++) {
s1 += "ab";
}


这样一个循环增加字符串的长度由于String的不可变性,会在每次调用+=的时候创建一个新的对象,效率会非常的低。在第四节会测试看看这样的效率影响会有多大。如果改写成

StringBuffer stringBuffer = new StringBuffer(2000);
for (int i = 0; i < 1000; i++) {
stringBuffer.append("ab");
}


只需要创建一次对象,并且直接通过扩容StringBuilder就可以实现+=的效果。说到扩容,这里再介绍一下StringBuilder的append方法,最核心应该就是StringBuilder默认会创建一个长度为16的字符数组,如果在调用append的时候发现数组不过大,则会增加原长度的2倍+2的长度,如下:

/**
* This implements the expansion semantics of ensureCapacity with no
* size check or synchronization.
*/
void expandCapacity(int minimumCapacity) {
int newCapacity = value.length * 2 + 2;
if (newCapacity - minimumCapacity < 0)
newCapacity = minimumCapacity;
if (newCapacity < 0) {
if (minimumCapacity < 0) // overflow
throw new OutOfMemoryError();
newCapacity = Integer.MAX_VALUE;
}
value = Arrays.copyOf(value, newCapacity);
}


因此,如果能提前估算出StringBuilder的大小,就能减少扩容带来的性能损耗。就像
StringBuffer stringBuffer = new StringBuffer(2000);
这样声明即可。

三、StringBuilder

3.1 StringBuilder是什么

StringBuilder可以说是StringBuffer的无同步锁版本,和StringBuffer唯一的区别就是所有的公开方法都没有加synchronized。

3.2 为什么要用StringBuilder

加锁会造成性能损耗,所以如果确定不会进行多线程操作时,使用StringBuilder会比使用StringBuffer带来更好的效率

四、String、StringBuffer、StringBuilder

4.1 效率的差别

上面分别说明了String、StringBuffer和StringBuilder,现在我们就来看看这三个类的效率差别有多大吧。下面是对三者分别进行扩展1000个字符 10000个字符 100000个字符所需时间的测试结果

测试对象10001000010000010000000
String7ms173ms5033ms
StringBuffer0ms1ms1ms395ms
StringBuilder0ms1ms1ms146ms
可以看到StringBuffer和StringBuilder比String的效率高了多少,在10000000的循环之后才看出StringBuffer和StringBuilder的差别,大概是差了2倍左右。

所以对于字符串扩充这个操作效率是StringBuilder > StringBuffer > String

4.2 总结

在使用的过程中如果没有多线程的操作,尽量使用StringBuilder来进行字符串的操作,StringBuffer一般用于多线程操作同一字符的情况。而在进行字符串操作的时候尽量避免使用”+=”,”=”等操作,可以大大提升程序的运行效率。
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息