下雨天读源码——String、StringBuilder、StringBuffer
String源码
类定义
public final class String implements java.io.Serializable, Comparable<String>, CharSequence
这是string源码的类定义,首先是一个final类型的不可变类型,其次string实现了可比较、可序列化接口,主要说一下为什么是final的吧。这是顶级大佬设计string时的智慧啊,我们知道java中有一个string的常量池,在这个池中的对象可以被多个引用指向,如果string是可变的话,当一个引用修改了string,那么其他的引用的值也会发生变化,后果将是灾难性的,这是一个方面的考虑。
内置属性有四个
private final char value[];
/** Cache the hash code for the string */
private int hash; // Default to 0
/** use serialVersionUID from JDK 1.0.2 for interoperability */
private static final long serialVersionUID = -6849794470754667710L;
/**
* Class String is special cased within the Serialization Stream Protocol.
*
* A String instance is written into an ObjectOutputStream according to
* <a href="{@docRoot}/../platform/serialization/spec/output.html">
* Object Serialization Specification, Section 6.2, "Stream Elements"</a>
*/
private static final ObjectStreamField[] serialPersistentFields =
new ObjectStreamField[0];
String的大部分方法都是围绕着对value[]进行操作的。
构造方法
无参构造方法
public String() {
this.value = "".value;
}
可以看出string的无参构造是一个空字符串。
String str = new String();
if (str != null){
System.out.println("str is not null!");
}
if (str.isEmpty()){
System.out.println("str is empty");
}
------------------------------------------------------------
str is not null!
str is empty
有参构造
string的有参构造就太多了!拿几个典型的例子来看。
//用一个字符串生成一个字符串
public String(String original) {
this.value = original.value;
this.hash = original.hash;
}
//将一个字符数组生成为一个字符串
public String(char value[]) {
this.value = Arrays.copyOf(value, value.length);
}
//将一个字符数组中的一段生成为字符串
public String(char value[], int offset, int count)
public String(int[] codePoints, int offset, int count)
常见方法
常见方法我们一个一个来看。(部分没有坚持读下去,朋友叫打游戏去了)
//定义一个字符串,我们可以点出来以下这些方法,眼熟的一些方法已罗列在下。
String str = new String();
str.length();
str.getBytes(StandardCharsets.UTF_8);
str.equals();
str.isEmpty();
str.getBytes();
str.toUpperCase(Locale.ROOT);
str.toLowerCase(Locale.ROOT);
str.trim();
str.intern();
str.contains();
str.split();
str.toCharArray();
str.charAt();
str.substring();
str.replace();
str.codePointAt();
str.compareTo();
str.concat();
str.contentEquals();
str.endsWith();
str.indexOf();
str.lastIndexOf();
str.matches();
str.replaceAll();
str.replaceFirst();
str.length();
返回string对象里面的char数组长度
public int length() {
return value.length;
}
str.getBytes(StandardCharsets.UTF_8);
按照编码返回一个字节数组。
public byte[] getBytes(Charset charset) {
if (charset == null) throw new NullPointerException();
return StringCoding.encode(charset, value, 0, value.length);
}
str.equals();
public boolean equals(Object anObject) {
if (this == anObject) {
return true;
}
if (anObject instanceof String) {
String anotherString = (String)anObject;
int n = value.length;
if (n == anotherString.value.length) {
char v1[] = value;
char v2[] = anotherString.value;
int i = 0;
while (n-- != 0) {
if (v1[i] != v2[i])
return false;
i++;
}
return true;
}
}
return false;
}
str.isEmpty()
判断string内置char数组的长度是否为0来判断是否为空
public boolean isEmpty() {
return value.length == 0;
}
str.toUpperCase(Locale.ROOT)
将字母转为大写
str.toLowerCase(Locale.ROOT)
将字母转为小写
str.trim()
去除字符首位的空格,准确来说,是自动截断ASCII码<=32位的所有字符,然后头尾截断整个字符串。
public String trim() {
int len = value.length;
int st = 0;
char[] val = value; /* avoid getfield opcode */
//可显示字符的ASCII码是从32-126的,第32正好是空格。
while ((st < len) && (val[st] <= ' ')) {
st++;
}
while ((st < len) && (val[len - 1] <= ' ')) {
len--;
}
return ((st > 0) || (len < value.length)) ? substring(st, len) : this;
}
str.substring()
从[beginIndex,endIndex]这个区间截断字符串,如果截取的字符串跟原字符串一样,没有发生改变,则返回原字符串;如果发生了改变,则通过调用构造方法
public String(char value[], int offset, int count),生成一个新字符串。
public String substring(int beginIndex, int endIndex) {
if (beginIndex < 0) {
throw new StringIndexOutOfBoundsException(beginIndex);
}
if (endIndex > value.length) {
throw new StringIndexOutOfBoundsException(endIndex);
}
int subLen = endIndex - beginIndex;
if (subLen < 0) {
throw new StringIndexOutOfBoundsException(subLen);
}
return ((beginIndex == 0) && (endIndex == value.length)) ? this
: new String(value, beginIndex, subLen);
}
str.contains();
public boolean contains(CharSequence s) {
return indexOf(s.toString()) > -1;
}
str.indexOf();
static int indexOf(char[] source, int sourceOffset, int sourceCount,
char[] target, int targetOffset, int targetCount,
int fromIndex) {
//先做异常处理
if (fromIndex >= sourceCount) {
return (targetCount == 0 ? sourceCount : -1);
}
if (fromIndex < 0) {
fromIndex = 0;
}
//如果空字符串出现了,就返回空字符串出现在当前字符串的第0位
// String str = new String();
// String source = "abc";
// System.out.println(source.indexOf(str)); 输出0
if (targetCount == 0) {
return fromIndex;
}
char first = target[targetOffset];
int max = sourceOffset + (sourceCount - targetCount);
for (int i = sourceOffset + fromIndex; i <= max; i++) {
/* Look for first character. */
if (source[i] != first) {
while (++i <= max && source[i] != first);
}
/* Found first character, now look at the rest of v2 */
if (i <= max) {
int j = i + 1;
int end = j + targetCount - 1;
for (int k = targetOffset + 1; j < end && source[j]
== target[k]; j++, k++);
if (j == end) {
/* Found whole string. */
return i - sourceOffset;
}
}
}
return -1;
}
//妙啊,刚开始理解先不要管偏移量的事情,当做sourceoffset=0,targetoffset=0;
str.split();
这个源码有点复杂,暂时没时间整明白。
public String[] split(String regex, int limit) {
/* fastpath if the regex is a
(1)one-char String and this character is not one of the
RegEx's meta characters ".$|()[{^?*+\\", or
(2)two-char String and the first char is the backslash and
the second is not the ascii digit or ascii letter.
*/
char ch = 0;
if (((regex.value.length == 1 &&
".$|()[{^?*+\\".indexOf(ch = regex.charAt(0)) == -1) ||
(regex.length() == 2 &&
regex.charAt(0) == '\\' &&
(((ch = regex.charAt(1))-'0')|('9'-ch)) < 0 &&
((ch-'a')|('z'-ch)) < 0 &&
((ch-'A')|('Z'-ch)) < 0)) &&
(ch < Character.MIN_HIGH_SURROGATE ||
ch > Character.MAX_LOW_SURROGATE))
{
int off = 0;
int next = 0;
boolean limited = limit > 0;
ArrayList<String> list = new ArrayList<>();
//indexof找到当前字符串中匹配上的字符后,将next指针指向该位置
while ((next = indexOf(ch, off)) != -1) {
if (!limited || list.size() < limit - 1) {
list.add(substring(off, next));
off = next + 1;
} else { // last one
//assert (list.size() == limit - 1);
list.add(substring(off, value.length));
off = value.length;
break;
}
}
// If no match was found, return this
if (off == 0)
return new String[]{this};
// Add remaining segment
if (!limited || list.size() < limit)
list.add(substring(off, value.length));
// Construct result
int resultSize = list.size();
if (limit == 0) {
while (resultSize > 0 && list.get(resultSize - 1).length() == 0) {
resultSize--;
}
}
String[] result = new String[resultSize];
return list.subList(0, resultSize).toArray(result);
}
return Pattern.compile(regex).split(this, limit);
}
str.toCharArray();
底层调用的是native方法
public char[] toCharArray() {
// Cannot use Arrays.copyOf because of class initialization order issues
char result[] = new char[value.length];
System.arraycopy(value, 0, result, 0, value.length);
return result;
}
str.charAt();
返回value数组中的第index元素
public char charAt(int index) {
if ((index < 0) || (index >= value.length)) {
throw new StringIndexOutOfBoundsException(index);
}
return value[index];
}
str.getChars()
将此字符串中的字符复制到目标字符数组中。
要复制的第一个字符位于索引srcbegin处;要复制的最后一个字符位于索引srcEnd-1处(因此要复制的字符总数为srcEnd-srcebeng)。将字符复制到dst的子阵列中,从索引dst开始,到索引dst结束:
DSTBENG+(srcEnd SRCBENG)-1
参数:
srcbeagin–要复制的字符串中第一个字符的索引。
srcEnd–要复制的字符串中最后一个字符后的索引。
dst–目标阵列。
DSTBENG–目标阵列中的起始偏移量。
public void getChars(int srcBegin, int srcEnd, char dst[], int dstBegin) {
if (srcBegin < 0) {
throw new StringIndexOutOfBoundsException(srcBegin);
}
if (srcEnd > value.length) {
throw new StringIndexOutOfBoundsException(srcEnd);
}
if (srcBegin > srcEnd) {
throw new StringIndexOutOfBoundsException(srcEnd - srcBegin);
}
System.arraycopy(value, srcBegin, dst, dstBegin, srcEnd - srcBegin);
}
str.substring();
如果不是按原字符串截断,那么会重新生成一个新的string对象
public String substring(int beginIndex, int endIndex) {
if (beginIndex < 0) {
throw new StringIndexOutOfBoundsException(beginIndex);
}
if (endIndex > value.length) {
throw new StringIndexOutOfBoundsException(endIndex);
}
int subLen = endIndex - beginIndex;
if (subLen < 0) {
throw new StringIndexOutOfBoundsException(subLen);
}
return ((beginIndex == 0) && (endIndex == value.length)) ? this
: new String(value, beginIndex, subLen);
}
str.replace();
用指定的文字替换序列替换与文字目标序列匹配的此字符串的每个子字符串。替换从字符串的开头一直进行到结尾,例如,将字符串“aaa”中的“aa”替换为“b”将导致“ba”而不是“ab”。
参数:
target–要替换的字符值序列
替换–字符值的替换顺序
返回:
结果字符串
str.intern();
具体intern用法其实很简单,就是字符串调用intern方法后,就会将字符串注册到常量池中,jdk1.7之前是直接拷贝内容,jdk1.7之后是在常量池中建立一个引用指向字符串在堆内的地址。
返回字符串对象的规范表示形式。
最初为空的字符串池由类字符串私下维护。
调用intern方法时,如果池中已经包含一个字符串,该字符串等于equals(object)方法确定的该字符串对象,则返回池中的字符串。否则,此字符串对象将添加到池中,并返回对此字符串对象的引用。
因此,对于任意两个字符串s和t,s.intern()==t.intern()为真当且仅当s.equals(t)为真时。
所有文字字符串和字符串值常量表达式都被插入。字符串文本在Java的第3.10.5节中定义™ 语言规范。
返回:
与此字符串具有相同内容,但保证来自唯一字符串池的字符串
str.concat();
主要是用来拼接两个字符串。
public String concat(String str){
int otherLen = str.length();
if(otherLen == 0){
return this;
}
int len = value.length;
char buf[] = Arrays.copyOf(value,len+otherLen);
//将传入字符串从0的位置开始复制到扩展后的字符串,从原字符串长度的位置开始复制,复制的元素个数为传入字符穿的长度
str.getChars(buf,len);
return new String(buf,true);
}
string源码一共3000行(带注释),同时也是java程序里面比较简单的源码,阅读源码有助于提高我们对java整个面向对象、面向接口程序设计思想的理解。
StringBuilder源码
StringBuilder源码带注释一共439行,工作量比较小,读起来也稍微快一点。
StringBuilder类的说明
可变的字符序列。此类提供与StringBuffer兼容的API,但不保证同步。此类被设计为在单个线程使用字符串缓冲区的地方(通常情况下)作为StringBuffer的插入式替换。在可能的情况下,建议优先使用该类而不是StringBuffer,因为在大多数实现中它会更快。
StringBuilder上的主要操作是append和insert方法,它们重载以接受任何类型的数据。每个都有效地将给定的数据转换为字符串,然后将该字符串的字符追加或插入到字符串生成器中。append方法总是在生成器的末尾添加这些字符;insert方法在指定点添加字符。
例如,如果z引用当前内容为“start”的字符串生成器对象,那么方法调用z.append(“le”)会导致字符串生成器包含“startle”,而z.insert(4,“le”)会将字符串生成器更改为包含“starlet”。
通常,如果sb引用StringBuilder的实例,那么sb.append(x)与sb.insert(sb.length(),x)具有相同的效果。
每个字符串生成器都有一个容量。只要字符串生成器中包含的字符序列的长度不超过容量,就不需要分配新的内部缓冲区。如果内部缓冲区溢出,它会自动变大。
多线程使用StringBuilder实例不安全。如果需要这种同步,建议使用StringBuffer。
除非另有说明,否则将null参数传递给此类中的构造函数或方法将导致引发NullPointerException。
总结如下:
1.线程不安全
2.用来做string类型的数据的插入与拼接
3.性能上最快
public final class StringBuilder
extends AbstractStringBuilder
implements java.io.Serializable, CharSequence
属性
继承自抽象父类的属性有两个
//The value is used for character storage.
char[] value;
/**
* The count is the number of characters used.
*/
int count;
构造方法
无参构造
public StringBuilder() {
super(16);
}
//默认构造方法是创建一个容量为16的字符数组大小。
//父类的构造方法
AbstractStringBuilder(int capacity) {
value = new char[capacity];
}
有参构造
//将一个字符串作为参数传给构造器,会构建一个str.length() + 16空字符数组,然后将str的值赋给数组中去。
public StringBuilder(String str) {
super(str.length() + 16);
append(str);
}
主要方法
append
@Override
public StringBuilder append(Object obj) {
return append(String.valueOf(obj));
}
@Override
public StringBuilder append(String str) {
super.append(str);
return this;
}
public StringBuilder append(StringBuffer sb) {
super.append(sb);
return this;
}
@Override
public StringBuilder append(CharSequence s) {
super.append(s);
return this;
}
@Override
public StringBuilder append(CharSequence s, int start, int end) {
super.append(s, start, end);
return this;
}
@Override
public StringBuilder append(char[] str) {
super.append(str);
return this;
}
@Override
public StringBuilder append(char[] str, int offset, int len) {
super.append(str, offset, len);
return this;
}
@Override
public StringBuilder append(boolean b) {
super.append(b);
return this;
}
@Override
public StringBuilder append(char c) {
super.append(c);
return this;
}
@Override
public StringBuilder append(int i) {
super.append(i);
return this;
}
@Override
public StringBuilder append(long lng) {
super.append(lng);
return this;
}
@Override
public StringBuilder append(float f) {
super.append(f);
return this;
}
@Override
public StringBuilder append(double d) {
super.append(d);
return this;
}
可以看出append方法由13种重构,万物皆可append。
选择其中最尝试用的字符串拼接为例,继续往下点,到父类的append方法。
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;
}
//判断拼接以后的数组长度是否会超过value的长度,如果超过,会调用自己的扩容方法来动态的扩容。
private void ensureCapacityInternal(int minimumCapacity) {
// overflow-conscious code
if (minimumCapacity - value.length > 0) {
value = Arrays.copyOf(value,
newCapacity(minimumCapacity));
}
}
//扩容方法,先将当前value的数组长度*2+2,再与传进来的长度进行比较,如果仍然小于要求的长度,那干脆直接使用传进来的长度,再判断是否超出整数的最大长度,如果超出则溢出为负数,那么就抛出异常,否则就扩容成功。
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;
}
private int hugeCapacity(int minCapacity) {
if (Integer.MAX_VALUE - minCapacity < 0) { // overflow
throw new OutOfMemoryError();
}
return (minCapacity > MAX_ARRAY_SIZE)
? minCapacity : MAX_ARRAY_SIZE;
}
就到了AbstractStringBuilder的append方法中,在这个方法中又调用了str.getChars()方法,上面提到过,底层还是调用的copyof方法。
insert方法
实现方式跟append一样,只不过在细节上有所区别,它需要一个精确的位置来决定将字符串插入到哪一个位置。
public AbstractStringBuilder insert(int index, char[] str, int offset,
int len)
{
if ((index < 0) || (index > length()))
throw new StringIndexOutOfBoundsException(index);
if ((offset < 0) || (len < 0) || (offset > str.length - len))
throw new StringIndexOutOfBoundsException(
"offset " + offset + ", len " + len + ", str.length "
+ str.length);
ensureCapacityInternal(count + len);
System.arraycopy(value, index, value, index + len, count - index);
System.arraycopy(str, offset, value, index, len);
count += len;
return this;
}
StringBuffer源码
首先是类声明,StringBuffer与StringBuilder是继承自同一个父类,本是同根生,性能小差距!
public final class StringBuffer
extends AbstractStringBuilder
implements java.io.Serializable, CharSequence
stringbuffer支持多线程
区别在于stringbuffer的方法都加了锁(构造方法除外),属实是为了多线程而设计的啊。
@Override
public synchronized StringBuffer append(Object obj) {
toStringCache = null;
super.append(String.valueOf(obj));
return this;
}
@Override
public synchronized StringBuffer append(String str) {
toStringCache = null;
super.append(str);
return this;
}
public synchronized StringBuffer append(StringBuffer sb) {
toStringCache = null;
super.append(sb);
return this;
}
/**
* @since 1.8
*/
@Override
synchronized StringBuffer append(AbstractStringBuilder asb) {
toStringCache = null;
super.append(asb);
return this;
}
@Override
public synchronized StringBuffer append(CharSequence s) {
toStringCache = null;
super.append(s);
return this;
}
/**
* @throws IndexOutOfBoundsException {@inheritDoc}
* @since 1.5
*/
@Override
public synchronized StringBuffer append(CharSequence s, int start, int end)
{
toStringCache = null;
super.append(s, start, end);
return this;
}
@Override
public synchronized StringBuffer append(char[] str) {
toStringCache = null;
super.append(str);
return this;
}
/**
* @throws IndexOutOfBoundsException {@inheritDoc}
*/
@Override
public synchronized StringBuffer append(char[] str, int offset, int len) {
toStringCache = null;
super.append(str, offset, len);
return this;
}
@Override
public synchronized StringBuffer append(boolean b) {
toStringCache = null;
super.append(b);
return this;
}
@Override
public synchronized StringBuffer append(char c) {
toStringCache = null;
super.append(c);
return this;
}
@Override
public synchronized StringBuffer append(int i) {
toStringCache = null;
super.append(i);
return this;
}
具体方法实现呢,参看它父类的实现,跟stringbuilder一样一样儿的。
Stringbuffer整了点花活
在属性上比其兄弟多了一条toStringCache,用来存储tostring的返回值,这样在返回的时候会快上一些嘛?还有就是当调用append方法时,stringbuffer对象发生改变,就会将这个缓存清空,这也许也是stringbuffer不如stringbuilder的原因吧。
private transient char[] toStringCache;
toString方法
@Override
public synchronized String toString() {
if (toStringCache == null) {
toStringCache = Arrays.copyOfRange(value, 0, count);
}
return new String(toStringCache, true);
}
三者之间的比较
字符串拼接时的性能比较
三者是string < stringbuffer < stringbuilder,实际上stringbuffer与stringbuilder的性能十分接近(单线程下),请看测试用例
/**
* @Description:
* @ClassName: Test
* @Author: yokna
* @Date: 2021/8/9 14:17
* @Version: 1.0
*/
public class Test {
public static void main(String[] args) {
//string原生的concat方法
long beginTime = System.currentTimeMillis();
String str1 = "hello";
String str2 = "nihao";
for (int i = 0; i < 100000; i++) {
str1 = str1+str2;
}
long endTime = System.currentTimeMillis();
System.out.println("String:"+(endTime-beginTime));
long beginTime1 = System.currentTimeMillis();
StringBuffer sb1 = new StringBuffer("hello");
StringBuffer sb2 = new StringBuffer();
for (int i = 0; i < 100000; i++) {
sb1.append(sb2);
}
long endTime1 = System.currentTimeMillis();
System.out.println("StringBuffer:"+(endTime1-beginTime1));
long beginTime2 = System.currentTimeMillis();
StringBuilder stringBuilder1 = new StringBuilder("hello");
StringBuilder stringBuilder2 = new StringBuilder("nihao");
for (int i = 0; i < 100000; i++) {
stringBuilder1.append(stringBuilder2);
}
long endTime2 = System.currentTimeMillis();
System.out.println("StringBuilder:"+(endTime2-beginTime2));
}
}
--------------------------------------------------
String:19207
StringBuffer:3
StringBuilder:4
在这场小范围的火拼这种,StringBuffer取巧获得了胜利,看上去似乎其性能还优于StringBuilder,然而是不是一直如此呢?
下面开始StringBuffer与StringBuilder的皇城pk,当我们将量级从10w改为1000w时,差距就显现出来了!
/**
* @Description:
* @ClassName: Test
* @Author: yokna
* @Date: 2021/8/9 14:17
* @Version: 1.0
*/
public class Test {
public static void main(String[] args) {
long beginTime1 = System.currentTimeMillis();
StringBuffer sb1 = new StringBuffer("hello");
StringBuffer sb2 = new StringBuffer();
for (int i = 0; i < 10000000; i++) {
sb1.append(sb2);
}
long endTime1 = System.currentTimeMillis();
System.out.println("StringBuffer:"+(endTime1-beginTime1));
long beginTime2 = System.currentTimeMillis();
StringBuilder stringBuilder1 = new StringBuilder("hello");
StringBuilder stringBuilder2 = new StringBuilder("nihao");
for (int i = 0; i < 10000000; i++) {
stringBuilder1.append(stringBuilder2);
}
long endTime2 = System.currentTimeMillis();
System.out.println("StringBuilder:"+(endTime2-beginTime2));
}
}
StringBuffer:640
StringBuilder:206
总结
三者之间的差距确实有,可以看出String原生的字符串相加不仅费时间还费空间(没有看空间耗费情况,仅凭直觉,这句话虽然正确但是不太严谨),但StringBuffer与StringBuilder之间性能差不多。
StringBuffer第一次获胜的原因可能是他在程序执行过程中,刚好一个时间片内就执行完了,不涉及到线程之间上下文的切换,而它兄弟可能就比较惨,在执行过程中被打断的次数比较多吧。
第二次StringBuffer败北的原因在于其append方法加了synchronized关键字修饰,锁住了整个方法,在单线程下仍然会有获取锁的一个过程,且StringBuffer每一次append时都会对toStringCache清空一次,也会有额外的开销。但是这些代价是值得的,它的这些设计,让它能胜任其兄弟无法胜任的工作,那就是肩负多线程环境下的string操作。
总而言之,言而总之,遇事不决,StringBuffer!