0%

String、StringBuffer、StringBuilder

前言:之前对着三者关系有点模糊,今天做个笔记总结一下

简介

运行速度快慢为:StringBuilder > StringBuffer > String

String为字符串常量,而StringBuilder和StringBuffer均为字符串变量

StringBuffer线程安全,支持同步锁(synchronized) ,而StringBuilder线程不安全,
由于 StringBuilder 相较于 StringBuffer 有速度优势,所以多数情况下建议使用 StringBuilder 类。然而在应用程序要求线程安全的情况下,则必须使用 StringBuffer 类。

String

String 类图

image-20191226004020565

String为什么不可变,查看源码,因为String底层是final char数组,所以不可变

1
2
3
4
5
public final class String implements java.io.Serializable, Comparable<String>, CharSequence {
/** The value is used for character storage. */
private final char value[];
......
}

String 常用方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
// 返回字符串长度
int length()

// 返回指定子字符串在此字符串中第一次出现处的索引
int indexOf(String str)

// 将此字符串与指定的对象比较
boolean equals(Object anObject)

// 返回指定索引处的 char 值
char charAt(int index)

// 按字典顺序比较两个字符串(注意是字段顺序,不是数字大小)
int compareTo(String anotherString)

// 测试此字符串是否以指定的后缀结束
boolean endsWith(String suffix)

// 测试此字符串是否以指定的前缀开始
boolean startsWith(String prefix)

// 截取字符串,返回一个新的字符串,它是此字符串的一个子字符串
String substring(int beginIndex)
// 截取字符串,返回一个新字符串,它是此字符串的一个子字符串
String substring(int beginIndex, int endIndex)


// 将此字符串转换为一个新的字符数组。
char[] toCharArray()

// 返回此对象本身(它已经是一个字符串!)
String toString()

// 返回字符串的副本,忽略前导空白和尾部空白
String trim()

String比较

1
2
3
4
5
6
7
8
9
10
11
12
String s1 = "abc";
String s2 = "abc";
String s3 = new String("abc");
String s4 = new String("abc");
// true
System.out.println(s1 == s2);
// false
System.out.println(s2 == s3);
// false
System.out.println(s3 == s4);
// true
System.out.println(s3.equal(s4));

JVM方法区(线程共享数据区)中包含的都是在整个程序中永远唯一的元素,如class,static变量等

我以上的步骤,首先会在jvm方法区的常量池中创建”abc”并赋给s1,s2发现常量池中存在”abc”,所以直接也把常量池中的”abc”赋给s2,s3在堆(线程共享数据区)中创建对象,因为常量池中检索发现存在”abc”,所以将”abc”赋给该值,s4同理,注意s3、s4是对象引用位于栈(线程私有数据区)中,它们指向位于堆中的对象,所以s3、s4比较的是堆中的地址

image-20191226004621182

为什么字符串拼接不应该用String?

1
2
3
String s = "abc" + "123";
等价于
String s = new StringBuilder().append("abc").append("123").toString();

java String的拼接原理就是创建StringBuilder对象,那为什么拼接不可以运用String?

1
2
3
4
5
6
7
8
9
10
11
String s = "abc" + "123";
s = s + "4";
s = s + "5";
s = s + "6";

等价于

String s = new StringBuilder().append("abc").append("123").toString();
String s = new StringBuilder(s).append("4").toString();
String s = new StringBuilder(s).append("5").toString();
String s = new StringBuilder(s).append("6").toString();

每次拼接都要创建StringBuilder对象,尤其在循环中,创建大量对象耗时耗资源,而且StringBuilder拼接并不优(如同ArrayList,涉及到扩容问题,尽可能减少扩容次数,才是最优)

StringBuilder、StringBuffer

StringBuffer (StringBuilder)常用方法

1
2
3
4
5
6
7
8
9
public StringBuffer append(String s) 将指定的字符串追加到此字符序列

public StringBuffer reverse() 将此字符序列用其反转形式取代

public delete(int start, int end) 移除此序列的子字符串中的字符

public insert(int offset, int i) 将 int 参数的字符串表示形式插入此序列中

replace(int start, int end, String str) 使用给定 String 中的字符替换此序列的子字符串中的字符

StringBuilder 线程不安全,性能好,StringBuffer 线程安全。两者最大的区别在于StringBuffer的方法加了synchronized,如下面代码所示

1
2
3
4
5
6
7
8
9
10
@Override
public synchronized int length() {
return count;
}

@Override
public synchronized int capacity() {
return value.length;
}
......

接下来以源码来稍稍深入探究StringBuilder,StringBuffer源码基本同StringBuilder,只是加了同步

AbstractStringBuilder

1
2
3
4
5
6
7
8
9
10
11
12
13
abstract class AbstractStringBuilder implements Appendable, CharSequence {
/**
* The value is used for character storage.
*/
char[] value;

/**
* The count is the number of characters used.
*/
int count;

......
}

StringBuilder可变是因为其char数组没有final修饰

构造函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
public StringBuilder() {
super(16);
}
public StringBuilder(int capacity) {
super(capacity);
}
public StringBuilder(String str) {
super(str.length() + 16);
append(str);
}
public StringBuilder(CharSequence seq) {
this(seq.length() + 16);
append(seq);
}

初始容量16,容量不足会进行扩容

append

StringBuilder append(Object obj)调用append(String str),然后调用父类的append(String str)方法

1
2
3
4
5
6
7
8
9
10
@Override
public StringBuilder append(Object obj) {
return append(String.valueOf(obj));
}

@Override
public StringBuilder append(String str) {
super.append(str);
return this;
}
1
2
3
4
5
6
7
8
9
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;
}

AbstractStringBuilder的append(String str)

  1. 参数是否为空
  2. 获取长度,判断当前容量是否能存放(不能就会扩容)
  3. 调用String的getChars拼接
  4. 增加长度
1
2
3
4
5
6
7
8
9
10
11
12
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);
}

String.getChars(int srcBegin, int srcEnd, char dst[], int dstBegin)

  1. 判断各参数合法性
  2. 调用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。而重用,还解决了前面的长度设置问题,因为即使一开始估算不准,多扩容几次之后也够了。

-------------本文结束感谢您的阅读-------------