一、字符串连接的效率问题
使用String连接字符串时为什么慢?
小知识点
java中对数组进行初始化后,该数组所占的内存空间、数组长度都是不可变的。
创建一个字符串,为字符串对象分配内存空间,会耗费掉一定的时间(CPU)与空间(内存)代价,作为最基础的数据类型,大量频繁的创建字符串,极大程度地影响程序的性能。
过多无用的中间对象
每次连接字符串时都会创建一个新的String对象,随着拼接次数的增多,这个对象会越来越大。
如,进行100次拼接需要创建100个String对象才能够达到目的。
StringBuilder在连接时为什么效率更高?
字符数组的扩容机制:
private void ensureCapacityInternal(int minimumCapacity) {
// 最小所需容量minimumCapacity是否比原数组长度要长
// overflow-conscious code
if (minimumCapacity - value.length > 0) {
value = Arrays.copyOf(value,
newCapacity(minimumCapacity));
}
}
private int newCapacity(int minCapacity) {
// 计算扩容之后的容量newCapacity
// overflow-conscious code
int newCapacity = (value.length << 1) + 2;
// 扩容后还小于所需的最小容量
if (newCapacity - minCapacity < 0) {
// 设置新容量为最小所需容量minimumCapacity
newCapacity = minCapacity;
}
// newCapacity是否溢出,newCapacity是否比数组所能分配的最大容量 MAX_ARRAY_SIZE 还要大。
return (newCapacity <= 0 || MAX_ARRAY_SIZE - newCapacity < 0)
? hugeCapacity(minCapacity)
: newCapacity;
}
private int hugeCapacity(int minCapacity) {
// 最小所需容量minCapacity大于Integer.MAX_VALUE时抛出内存溢出异常
if (Integer.MAX_VALUE - minCapacity < 0) { // overflow
throw new OutOfMemoryError();
}
// 如果minCapacity介于MAX_ARRAY_SIZE和Integer.MAX_VALUE之间,则新的容量为minCapacity,否则直接使用MAX_ARRAY_SIZE作为新的容量。
return (minCapacity > MAX_ARRAY_SIZE)
? minCapacity : MAX_ARRAY_SIZE;
}
向原StringBuilder对象中追加字符串时:
1.追加对象str为null时追加'null'字符
2.确认是否需要进行扩容操作
2.1 最小所需容量minimumCapacity是否比原数组长度要长,即当原数组长度不能满足所需最小容量时进行扩容操作。
2.2 计算扩容之后的容量newCapacity,newCapacity = (value.length * 2) + 2。
2.3 扩容后是否还小于所需的最小容量,如果小于则直接设置新容量为最小所需容量minimumCapacity。
2.4 newCapacity是否溢出,newCapacity是否比数组所能分配的最大容量 MAX_ARRAY_SIZE 还要大。如果是的话则判断,最小所需容量minCapacity大于Integer.MAX_VALUE时抛出内存溢出异常,如果minCapacity介于MAX_ARRAY_SIZE和Integer.MAX_VALUE之间,则新的容量为minCapacity,否则直接使用MAX_ARRAY_SIZE作为新的容量。
3.str.getChars()将str追加到value的末尾
效率高的原因
- 扩容机制保证了,只有在满足扩容条件
minimumCapacity - value.length > 0
时才会进行扩容生成新的数组,所以大部分情况都是在对原数组进行操作,避免了产生过多的无用char[]对象,节省了系统资源的开销。
代码
/**
* 比较字符串连接速度
*
* @Author: lingyejun
* @Date: 2019/8/17
* @Describe:
* @Modified By:
*/
public class LinkCompare {
/**
* 原始字符串连接
*
* @param times
*/
public static void linkByString(int times) {
Long startTime = System.currentTimeMillis();
String initStr = "";
for (int i = 0; i < times; i++) {
initStr = initStr + i;
}
Long endTime = System.currentTimeMillis();
System.out.println("String 连接 " + times + " 次 消耗:" + (endTime - startTime) + "ms");
}
/**
* 使用StringBuilder连接字符串
*
* @param times
*/
public static void linkByStringBuilder(int times) {
Long startTime = System.currentTimeMillis();
StringBuilder initStr = new StringBuilder();
for (int i = 0; i < times; i++) {
initStr.append(i);
}
Long endTime = System.currentTimeMillis();
System.out.println("StringBuilder 连接 " + times + " 次 消耗:" + (endTime - startTime) + "ms");
}
/**
* 使用StringBuffer连接字符串
*
* @param times
*/
public static void linkByStringBuffer(int times) {
Long startTime = System.currentTimeMillis();
StringBuffer initStr = new StringBuffer();
for (int i = 0; i < times; i++) {
initStr.append(i);
}
Long endTime = System.currentTimeMillis();
System.out.println("StringBuffer 连接 " + times + " 次 消耗:" + (endTime - startTime) + "ms");
}
public static void main(String[] args) {
// 100000000
linkByStringBuilder(40000);
//-XX:+PrintGCDetails
//linkByString(40000);
}
}
二、StringBuilder和String Buffer的线程安全比较
验证StringBuffer的线程安全性
线程不安全的原因
public StringBuilder append(String str) {
super.append(str);
return this;
}
public synchronized StringBuffer append(String str) {
toStringCache = null;
super.append(str);
return this;
}
测试代码
import java.util.ArrayList;
import java.util.List;
/**
* StringBuilder和StringBuffer的并发测验
*
* @Author: lingyejun
* @Date: 2019/8/17
* @Describe:
* @Modified By:
*/
public class SecurityCompare {
public void stringBuilderTest() {
// 初始化StringBuilder
StringBuilder stringBuilder = new StringBuilder();
// joinList
List joinList = new ArrayList<>();
// 模拟并发场景
for (int i = 0; i < 1000; i++) {
StringBuilderThread sbt = new StringBuilderThread(stringBuilder);
sbt.start();
joinList.add(sbt);
}
// 等待append线程执行完毕后再执行主线程
for (StringBuilderThread thread : joinList) {
try {
thread.join();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
// 打印最终的结果
System.out.println("StringBuilder 并发append的结果: " + stringBuilder.length());
}
public void stringBufferTest() {
// 初始化StringBuffer
StringBuffer stringBuffer = new StringBuffer();
// joinList
List joinList = new ArrayList<>();
// 模拟并发场景
for (int i = 0; i < 1000; i++) {
StringBufferThread sbf = new StringBufferThread(stringBuffer);
sbf.start();
joinList.add(sbf);
}
// 等待append线程执行完毕后再执行主线程
for (StringBufferThread thread : joinList) {
try {
thread.join();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
// 打印最终的结果
System.out.println("StringBuffer 并发append的结果: " + stringBuffer.length());
}
public static void main(String[] args) {
SecurityCompare securityCompare = new SecurityCompare();
securityCompare.stringBuilderTest();
securityCompare.stringBufferTest();
}
public static class StringBuilderThread extends Thread {
private StringBuilder stringBuilder;
public StringBuilderThread(StringBuilder stringBuilder) {
this.stringBuilder = stringBuilder;
}
@Override
public void run() {
try {
Thread.sleep(10);
} catch (InterruptedException e) {
e.printStackTrace();
}
stringBuilder.append("a");
try {
Thread.sleep(10);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
private static class StringBufferThread extends Thread {
private StringBuffer stringBuffer;
public StringBufferThread(StringBuffer stringBuffer) {
this.stringBuffer = stringBuffer;
}
@Override
public void run() {
try {
Thread.sleep(10);
} catch (InterruptedException e) {
e.printStackTrace();
}
stringBuffer.append("a");
try {
Thread.sleep(10);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
三、结论
1.String为固定长度的字符串,StringBuilder和StringBuffer为变长字符串。
2.StringBuffer是线程安全的,StringBuilder是非线程安全的。
3.StringBuilder和StringBuffer的默认初始容量是16,可以提前预估好字符串的长度,进一步减少扩容带来的额外开销。