一、字符串连接的效率问题
使用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.确认是否需要进行扩容操作
- 最小所需容量minimumCapacity是否比原数组长度要长,即当原数组长度不能满足所需最小容量时进行扩容操作。
- 计算扩容之后的容量newCapacity,newCapacity = (value.length * 2) + 2。
- 扩容后是否还小于所需的最小容量,如果小于则直接设置新容量为最小所需容量minimumCapacity。
- 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<StringBuilderThread> 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<StringBufferThread> 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(); } } } }
三、结论
- String为固定长度的字符串,StringBuilder和StringBuffer为变长字符串。
- StringBuffer是线程安全的,StringBuilder是非线程安全的。
- StringBuilder和StringBuffer的默认初始容量是16,可以提前预估好字符串的长度,进一步减少扩容带来的额外开销。