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。而重用,还解决了前面的长度设置问题,因为即使一开始估算不准,多扩容几次之后也够了。

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