文章目录
前言
此文主要针对StringBuffer和StringBuilder做一些解读。
一、String
1、String不可变的原理
提到字符串 String,大家第一形象就是不可变,但是当我们直接再次修改字符串时,会变成新建一个字符串,而不是在原先基础上修改
为什么会新建一个对象?
- 1、可以从两个方面理解:语法方面和内存方面
- 2、语法方面:String 内部存储字符串其实是由一个字符数组存储,其修饰符为 private final char value[];,因为字符数组被 private final 修饰意味着不可更改,所以定义在其内的字符串也不可更改
public final class String
implements java.io.Serializable, Comparable<String>, CharSequence {
private final char value[];
....
}
- 3、内存方面:String定义的字符串,会存储至堆中的字符串常量池中,当字符串常量池中,有该字符串时,可以直接引用,但是没有时,会创建一个字符串存入字符串常量池中
如下:name存储在栈中,只是一个引用,现在把字符串"晚归的生活"存入常量池中,但是当再次给 name 赋值时,字符串常量池中并没有该字符串"晚归的生活很帅“,所以会创建一个新的字符串存储入常量池中。
public static void main(String[] args) {
String name = "晚归的生活";
name = "晚归的生活很帅";
System.out.println(name);
}
2、final 扩展
final 表示最终的,不可更改的。可能一部分初学者看到String 、StringBuffer、StringBuilder源码会发现,三者都被 final 修饰,为什么String是不可变的,另外两者是可变的呢,这其实是对 final 不够深入了解导致的,所以在这扩展一下。
- 1、修饰类时:表示该类不可被继承
- 2、修饰方法时:表示该方法不可被重写
- 3、修饰变量时:有两种情况,修饰基础数据类型、引用类型,但数据不可改
- 4、修饰对象时:对象的指向地址不可更改,但值可以更改
public static void main(String[] args) {
final int[] arr ={1,2,3};
arr[1] = 10; //修改数值
System.out.println(Arrays.toString(arr)); 输出[1, 10, 3]
}
所以我们知道,修饰类时,只是表示该类不能被继承,重要的是第四条:修饰对象时,指向地址不可更改,但是数据可以更改。数组是一个对象,可以理解为数组在堆中的地址没有改变,但是里面存储的数据可以改变,不影响数组的地址即可。
final 修饰数组对象,发现数组内数据可以更改,但是因为 private 修饰符的原因,是私有的,外部不可更改,两个修饰符的限定下,char[]不可更改。
private final char value[];
而 StringBuilder和StringBuffer 并无限定修饰,所以可变的
char[] value;
二、StringBuffer
1、StringBuffer定论
StringBuffer 是线程安全的、可变的字符序列、不会增加新的对象
- 线程安全:StringBuffer使用了关键字 synchronized(这个以后讲,你只要记住它是线程安全的即可) ,该关键字表示同步的,确保在同一个时刻,只有一个线程可以执行某个方法,表现出串行顺序执行似的,保护数据的安全
- 可变的字符序列:在StringBuffer定义了每个字符串缓冲区都有一个容量。只要字符串缓冲区中包含的字符序列的长度不超过容量,就没有必要分配新的内部缓冲区数组。如果内部缓冲区溢出,它会自动变大
- 不会增加新的对象:每次对 StringBuffer 对象本身进行操作,而不是生成新的对象
2、StringBuffer构造方法
对于String 既可以使用构造器,也可以直接赋值,但是 StringBuffer只能使用构造器,所以我们先从构造器开始了解
- 使用默认构造函数,StringBuffer会默认分配一个16大小的字符缓存区
public StringBuffer() {
super(16);
}
- 指定大小,根据参数大小分配
public StringBuffer(int capacity) {
super(capacity);
}
- 初始化内容,字符串缓冲区的初始容量加上 16 大小长度。
public StringBuffer(String str) {
super(str.length() + 16);
append(str);
}
从上面,大家应该有个概念了,StringBuffer这个类必须创建实例才可使用,不能直接赋值,并且使用默认构造器时,StringBuffer内部定义了一个默认大小为 16 的字符串缓存区,当数据没有超过这个大小时,则不会增加长度,下面直接用常用方法来解析
3、StringBuffer常用方法示例和原理
3.1、常用方法示例
- StringBuffer常用方法有两类:append() 和 insert()。
- 方法 append() 表示可以拼接字符串和各类数据类型,而insert() 可以插入字符。 两者重载了不同参数类型,使其绝大部分数据类型都可以用
append()方法示例:
- append():表示可以拼接各类数据类型
代码示例:
代码显示 s1 拼接 s2 使用了 append() 方法,可以拼接String 定义的字符串
public static void main(String[] args) {
String s0 = "喜欢美食";
StringBuffer s1 = new StringBuffer("晚归的生活");
StringBuffer s2 = s1.append(s0);
System.out.println(s2); 输出晚归的生活喜欢美食
}
当然也可以直接拼接
public static void main(String[] args) {
StringBuffer s1 = new StringBuffer(10);
StringBuffer append = s1.append(1);
System.out.println(append); 输出 1
}
所以从上面演示可知,append() 重载实现了大部分数据类型,可以拼接不同类型数据,甚至有时候可以用于插入
insert() 方法示例
- insert():表示在指定的位置插入数据,和append()一样,支持所有的基本数据类型
代码示例:
public static void main(String[] args) {
StringBuffer s1 = new StringBuffer("abcdefg");
s1.insert(0,1); 索引0处插入1
System.out.println(s1); 输出1abcdefg
}
下面我们看一下内部代码的实现
3.2、方法内部实现原理
以方法append()为示例
1、现在定义一个需要初始化的字符串
public static void main(String[] args) {
StringBuffer s1 = new StringBuffer("abcdefg");
s1.append("a");
System.out.println(s1);
}
2、调用append() 方法,查看其中的源码实现:发现内部是调用了父类 AbstractStringBuilder 的append()
public synchronized StringBuffer append(String str) {
toStringCache = null;
super.append(str); 调用父类的append()
return this;
}
3、跳转到父类的append() 方法实现时,它会先调用一个 ensureCapacityInternal() 方法
public AbstractStringBuilder append(String str) {
if (str == null) 当字符串为空,则在后面加上null
return appendNull();
int len = str.length(); 获取字符长度
ensureCapacityInternal(count + len);调用方法,增加长度扩容数组,然后赋值给原变量value
str.getChars(0, len, value, count); 把字符串复制到扩容后的char[]
count += len;
return this;
}
4、我们继续往下走, 查看 ensureCapacityInternal 内部实现,根据源码说明:minimumCapacity表示最小容量
当所需最小容量大于原本字符组容量时,则调用方法 Arrays.copyOf(value,newCapacity(minimumCapacity));
如果最小容量没有大于原本字符组容量,则什么都不做。
- 主要是判断添加数据后的数组大小是否超过原本数组大小
private void ensureCapacityInternal(int minimumCapacity) {
if (minimumCapacity - value.length > 0) {
value = Arrays.copyOf(value, newCapacity(minimumCapacity));
}
}
5、假如我们插入的数据已经超过了原本所需容量大小,现在需要扩容,调用 Arrays.copyOf(value, newCapacity(minimumCapacity))
- 主要作用是返回一个符合容量要求的数组
扩展
Arrays.copyOf(char[] original, int newLength) 表示返回一个newLength长度的数组,并拷贝originl内容
original:第一个参数为要拷贝的数组对象
newLength:第二个参数为拷贝的新数组长度
6、我们直接看 newCapacity(minimumCapacity) 方法的内部实现
- 表明作用是创建一个新的容量大小,注意看返回值
- 如果新容量小于指定的最小容量,则新容量为指定的最小容量(避免内存浪费)
private int newCapacity(int minCapacity) { 创建一个新的容量大小
int newCapacity = (value.length << 1) + 2; 新容量为旧容量的两倍加上 2
if (newCapacity - minCapacity < 0) { 如果新容量小于指定的最小容量,则新容量为指定的最小容量(避免内存浪费)
newCapacity = minCapacity;
}
MAX_ARRAY_SIZE等于Integer.MAX_VALUE - 8,判断新容量是否在0<x<MAX_ARRAY_SIZE之间,是则返回,否则判断一下是否溢出
return (newCapacity <= 0 || MAX_ARRAY_SIZE - newCapacity < 0)
? hugeCapacity(minCapacity)
: newCapacity;
}
7、好了,此时创建了一个符合要求的数组了,再返回 ensureCapacityInternal(),后续调用 getChars() 方法
- 主要作用将字符串中的字符复制到目标字符数组中,并返回 this 这个对象
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;
}
扩展
返回值 | 方法 | 作用 |
---|---|---|
void | getChars(int srcBegin, int srcEnd, char[] dst, int dstBegin) | 将字符从此序列复制到目标字符数组 dst |
srcBegin – 要复制的字符串中第一个字符的索引。
srcEnd – 要复制的字符串中最后一个字符之后的索引。
dst – 目标数组。
dstBegin – 目标数组中的起始偏移量。
上面大致说明了 StringBuffer中拼接方法append() 的整体流程,其他方法也差不多如此,有兴趣的查看源码即可
3.3、总结
当需要拼接字符串时,需要调用 append() 方法,其内部实现了 ensureCapacityInternal(int minimumCapacity) 方法,判断该操作是否需要扩容,有两种情况,扩容、不扩容
- 扩容:如果指定的最小容量大于当前容量,则旧容量需要扩容 2 倍加2
- 不扩容: 如果指定的最小容量小于等于当前容量,则不会进行扩容。
最后调用 getChars() 方法实现数组的复制
三、 StringBuilder
- StringBuilder 是可变字符序列
- 适合单线程情况下,属于线程不安全的
- StringBuilder 内部实现和StringBuffer差不多,所以这不多讲了
扩展一下:
当我们遇见加号拼接时:字符串 + 字符串,在编译期间,字符串会被优化为
new StringBuilder().append("字符串1").append("字符串2").toString();
查看一下 toString() 方法,返回的还是一个字符串
public String toString() {
return new String(value, 0, count);
}
四、 String和StringBuffer、StringBuilder差异
- String 是不可变的,而StringBuffer、StringBuilder属于可变序列字符类,两者只需要扩容底层数组大小即可
- String 可直接赋值和使用构造函数,而 StringBuffer、StringBuilder 只能使用构造函数
- StringBuffer 适合多线程下,线程安全的,但是效率低些,因为加了 synchronized 关键字
- StringBuilder适合单线程,线程不安全,但是速度快