前言:之前对着三者关系有点模糊,今天做个笔记总结一下
简介
运行速度快慢为:StringBuilder > StringBuffer > String
String为字符串常量,而StringBuilder和StringBuffer均为字符串变量
StringBuffer线程安全,支持同步锁(synchronized) ,而StringBuilder线程不安全,
由于 StringBuilder 相较于 StringBuffer 有速度优势,所以多数情况下建议使用 StringBuilder 类。然而在应用程序要求线程安全的情况下,则必须使用 StringBuffer 类。
String
String 类图
String为什么不可变,查看源码,因为String底层是final char数组,所以不可变
1 | public final class String implements java.io.Serializable, Comparable<String>, CharSequence { |
String 常用方法
1 | // 返回字符串长度 |
String比较
1 | String s1 = "abc"; |
JVM方法区(线程共享数据区)中包含的都是在整个程序中永远唯一的元素,如class,static变量等
我以上的步骤,首先会在jvm方法区的常量池中创建”abc”并赋给s1,s2发现常量池中存在”abc”,所以直接也把常量池中的”abc”赋给s2,s3在堆(线程共享数据区)中创建对象,因为常量池中检索发现存在”abc”,所以将”abc”赋给该值,s4同理,注意s3、s4是对象引用位于栈(线程私有数据区)中,它们指向位于堆中的对象,所以s3、s4比较的是堆中的地址
为什么字符串拼接不应该用String?
1 | String s = "abc" + "123"; |
java String的拼接原理就是创建StringBuilder对象,那为什么拼接不可以运用String?
1 | String s = "abc" + "123"; |
每次拼接都要创建StringBuilder对象,尤其在循环中,创建大量对象耗时耗资源,而且StringBuilder拼接并不优(如同ArrayList,涉及到扩容问题,尽可能减少扩容次数,才是最优)
StringBuilder、StringBuffer
StringBuffer (StringBuilder)常用方法
1 | public StringBuffer append(String s) 将指定的字符串追加到此字符序列 |
StringBuilder 线程不安全,性能好,StringBuffer 线程安全。两者最大的区别在于StringBuffer的方法加了synchronized,如下面代码所示
1 |
|
接下来以源码来稍稍深入探究StringBuilder,StringBuffer源码基本同StringBuilder,只是加了同步
AbstractStringBuilder
1 | abstract class AbstractStringBuilder implements Appendable, CharSequence { |
StringBuilder可变是因为其char数组没有final修饰
构造函数
1 | public StringBuilder() { |
初始容量16,容量不足会进行扩容
append
StringBuilder append(Object obj)调用append(String str),然后调用父类的append(String str)方法
1 |
|
1 | public AbstractStringBuilder append(String str) { |
AbstractStringBuilder的append(String str)
- 参数是否为空
- 获取长度,判断当前容量是否能存放(不能就会扩容)
- 调用String的getChars拼接
- 增加长度
1 | public void getChars(int srcBegin, int srcEnd, char dst[], int dstBegin) { |
String.getChars(int srcBegin, int srcEnd, char dst[], int dstBegin)
- 判断各参数合法性
- 调用native方法 System.arraycopy
1 | public static native void arraycopy(Object src, int srcPos, Object dest, int destPos, int length); |
StringBuilder拼接问题
1. 初始长度好重要
StringBuilder的内部有一个char[], 不断的append()就是不断的往char[]里填东西的过程。
new StringBuilder() 时char[]的默认长度是16,然后,如果要append第17个字符,怎么办?
用System.arraycopy成倍复制扩容!!!!
这样一来有数组拷贝的成本,二来原来的char[]也白白浪费了要被GC掉。可以想见,一个129字符长度的字符串,经过了16,32,64, 128四次的复制和丢弃,合共申请了496字符的数组,在高性能场景下,这几乎不能忍。
所以,合理设置一个初始值多重要。
但如果我实在估算不好呢?多估一点点好了,只要字符串最后大于16,就算浪费一点点,也比成倍的扩容好。
2. Liferay的StringBundler类
Liferay的StringBundler类提供了另一个长度设置的思路,它在append()的时候,不急着往char[]里塞东西,而是先拿一个String[]把它们都存起来,到了最后才把所有String的length加起来,构造一个合理长度的StringBuilder。
3. 但还是浪费了一倍的char[]
浪费发生在最后一步,StringBuilder.toString()
//创建拷贝, 不共享数组
return new String(value, 0, count);
String的构造函数会用 System.arraycopy()复制一把传入的char[]来保证安全性不可变性,如果故事就这样结束,StringBuilder里的char[]还是被白白牺牲了。
为了不浪费这些char[],一种方法是用Unsafe之类的各种黑科技,绕过构造函数直接给String的char[]属性赋值,但很少人这样做。
另一个靠谱一些的办法就是重用StringBuilder。而重用,还解决了前面的长度设置问题,因为即使一开始估算不准,多扩容几次之后也够了。