一.概述
字符串是个什么?
字符串是由字符组成的,而字符由字节根据编码规则而来。这里我们的问题停留在字符以及字符串上,不涉及字符和字节的转换,想了解字符和字节转换以及编码规则请看字符编码(ASCII,Unicode和UTF-8,GBK)
问题
平时开发中涉及到字符串拼接到底什么情况使用什么方法最合适呢?以及为什么选择这种方法呢?(或者说一下String,StringBuilder,StringBuffer异同等)相信大家面试的时候都遇到过。
结论
下面先给出结论,再将结论的得出过程仔细分析一下。(jdk版本为1.8.0-51)
先给出拼接方式如下:
方法一: 使用“+”;
方法二: String.concat方法
方法三: StringBuilder.append方法
方法四: StringBuffer.append方法
下面是选择原则:
- 简单拼接使用方法一“+”
- 百分百确定不涉及多线程,大量拼接使用方法三StringBuilder.append
- 不确定会涉及多线程,大量拼接使用方法四StringBuffer.append
下面看看如何得出的这些结论。
二.拼接实际过程
2.1先看各个类的定义
String:是一个常量,一旦赋值就不可以修改了。(内部实现为字符数组,其为final类型)
StringBuilder:字符串变量(非线程安全)。(内部实现也是字符数组)
StringBuffer:字符串变量(Synchronized,即线程安全)。(内部实现也是字符数组)
2.2各种方法拼接的实质
2.2.1 方法一: 使用“+”
String既然是final的,那么哪里来的拼接呢?其实,所有的所谓字符串拼接,都是重新生成了一个新的字符串。如下:
String s2 = "Hello";
String s3 = "Word";
String s4 = s2 + s3;
如果我们反编译会发现其内部在编译的时候其实是使用了StringBuilder。上面的拼接过程是:
String s4 = new StringBuilder().append(s2).append(s3).toString();
但是注意
如果如下
String s1 = "Hello" + "Word";
其内部的实现和上面的原理就不同了,上面的这种拼接方法就相当于:
String s1 = "HelloWord";
因为在编译时候Hello和Word两个字符串被保存入常量池中,编译的时候就已经将两个字符串拼接为一个。关于类加载和编译相关的知识这里不展开说明。
2.2.2 方法二: String.concat方法
下面是源码:
@FastNative
public native String concat(String str);
在jdk版本为1.8.0-51中已经成为了native方法,从注解@FastNative
来看它是将拼接过程放入了native层加快了拼接速度。
2.2.3 方法三: StringBuilder.append方法
实例代码如下,将两个字符串进行拼接
StringBuilder sb = new StringBuilder();
String s = sb.append("Hello").append("Word").toString();
StringBuilder类的基类是AbstractStringBuilder,并且append也是使用的AbstractStringBuilder中的,下面我们来看AbstractStringBuilder。其中重要的几点:
- 存储实现是字符数组
- 字符数组中不一定所有位置都被占用了
- “数组扩容”,构造新String
下面是AbstractStringBuilder源码,只取相关部分
-------省略代码-------
/**
* 字符数组作为存储容器
*/
char[] value;
/**
* 数组中被使用的数量
*/
int count;
-------省略代码-------
//拼接
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;
}
//确认是否要扩容
private void ensureCapacityInternal(int minimumCapacity) {
// overflow-conscious code
if (minimumCapacity - value.length > 0) {
value = Arrays.copyOf(value,
newCapacity(minimumCapacity));
}
}
//扩容操作
private int newCapacity(int minCapacity) {
// overflow-conscious code
int newCapacity = (value.length << 1) + 2;
if (newCapacity - minCapacity < 0) {
newCapacity = minCapacity;
}
return (newCapacity <= 0 || MAX_ARRAY_SIZE - newCapacity < 0)
? hugeCapacity(minCapacity)
: newCapacity;
}
StringBuilder源码,只取相关部分
@Override
public StringBuilder append(String str) {
super.append(str);
return this;
}
@Override
public String toString() {
if (count == 0) {
return "";
}
return StringFactory.newStringFromChars(0, count, value);
}
2.2.4 方法四: StringBuffer.append方法
StringBuffer也是继承的AbstractStringBuilder,并且相应的append,ensureCapacityInternal,newCapacity也是来自于其基类。不同的是StringBuffer中append方法是synchronized,于是保证了线程安全。
@Override
public synchronized StringBuffer append(String str) {
toStringCache = null;
super.append(str);
return this;
}
三 效率问题
这里通过拼接代码来验证,如下
方法一: 使用“+”;
private void concatStr1() {
long t1 = System.currentTimeMillis();
String s = "hello";
for(int i=0;i<60000;i++){
String str = String.valueOf(i);
s+=str;
}
long t2 = System.currentTimeMillis();
long dur = t2-t1;
Log.e("cost_time=",dur+"");
}
方法二: String.concat方法
private void concatStr2() {
long t1 = System.currentTimeMillis();
String s = "hello";
for(int i=0;i<60000;i++){
String str = String.valueOf(i);
s.concat(str);
}
long t2 = System.currentTimeMillis();
long dur = t2-t1;
Log.e("cost_time=",dur+"");
}
方法三: StringBuilder.append方法
private void concatStr3() {
long t1 = System.currentTimeMillis();
String s = "hello";
StringBuilder sb = new StringBuilder();
for(int i=0;i<60000;i++){
sb.append(i);
}
long t2 = System.currentTimeMillis();
long dur = t2-t1;
Log.e("cost_time=",dur+"");
}
方法四: StringBuffer.append方法
private void concatStr4() {
long t1 = System.currentTimeMillis();
String s = "hello";
StringBuffer sb = new StringBuffer();
for(int i=0;i<60000;i++){
sb.append(i);
}
long t2 = System.currentTimeMillis();
long dur = t2-t1;
Log.e("cost_time=",dur+"");
}
下面是依次的耗时时间
06-14 15:42:13.554 25934-26300/com.gong.io.demo E/cost_time=: 79443
06-14 16:04:48.623 31326-31505/com.gong.io.demo E/cost_time=: 37
06-14 15:47:45.329 27786-27911/com.gong.io.demo E/cost_time=: 5
06-14 16:06:26.622 31965-32103/com.gong.io.demo E/cost_time=: 6
从中我们了解到耗时排名如下:
StringBuilder < StringBuffer < concat < +
得出结论:
- 大量拼接操作时选用StringBuffer(耗时和StringBuilder几乎相同,且你不可能保证不会用在多线程的情况下,针对Android开发)。若后台开发并且这里明确可能成为速度瓶颈,那另当别论(百分百确定单线程就用StringBuilder);
- concat方法现改为native方法了,效率有了非常大的提升(相对之前使用字符数组);
- 方法一“+”:不光速度很低,由于内部创建大量StringBuilder实例(参看2.2.1),造成内存资源浪费。