在JAVA中,对字符串的操作可以通过String
、StringBuffer
、StringBuilder
来进行。三者虽然功能相似,但在实际开发中,不同场景下使用三者的性能会存在一定差异,三者各有其较适宜使用的场景。本文将通过分析三者的源代码,分析主要数据组织及功能实现上的区别、设计方式的影响,并分析三者分别适合使用的场景。
源代码分析
String类
主要结构
String类的结构如下:
1 | public final class String |
String类被final修饰,说明它不可被继承。
String的值由byte[] value
来存储,即String的本质是一个byte的数组。被final修饰,说明String是个不可变类,即存储的值不可改变。
构造函数
String类提供的构造函数有很多种,在下面进行一些简单的分析:
- 无参构造,创建空字符串
1 | public String() { |
- 提供一个String作为参数,进行深拷贝
1 | public String(String original) { |
- 提供一个字符数组作为参数,构造新的String
1 | public String(char value[]) { |
- 通过一个现有数组中截取一段子数组,构造新的String
数组可能是字符或者各种形式的编码
1 | public String(char value[], int offset, int count) { |
- 通过byte数组或者StringBuffer、StringBuilder来构造新的String
1 | public String(byte[] bytes) { |
常用方法
- 长度
1 | public int length() { |
- 判断是否为空
1 | public boolean isEmpty() { |
- 取某位置的字符
1 | public char charAt(int index) { |
- 判断两个字符串是否相等
1 | public boolean equals(Object anObject) { |
- 比较两个字符串
1 | public int compareTo(String anotherString) { |
- 判断起始/终结字符
1 | public boolean startsWith(String prefix, int toffset) { |
- 找某字符的下标
1 | public int indexOf(int ch) { |
- 取子串
1 | public String substring(int beginIndex) { |
- 将String中的指定内容进行替换,需要注意的是,由于String不可变,这种”替换”实际上是新建了一个字符串进行返回,而非改变原来的字符串。
1 | public String replace(char oldChar, char newChar) { |
AbstractStringBuilder类
分析源码发现,StringBuilder类和StringBuffer类都继承自AbstractStringBuilder父类,故先分析父类的结构和主要功能。
主要结构
与String类一样,使用byte数组来存储字符串内容。增加count变量记录字符串的实际长度。
另一个与String不同的点在于,AbstractStringBuilder是一个可变的类。
1 | abstract class AbstractStringBuilder implements Appendable, CharSequence { |
构造函数
有参构造通过传入字符数组的容量作为参数,构造指定容量的字符数组。
1 | AbstractStringBuilder() { |
常用方法
- 字符串间比较
1 | int compareTo(AbstractStringBuilder another) { |
- 获取字符串长度,由于count变量被用于记录字符串长度,因此只要返回count值即可
1 | public int length() { |
- 由于构造的字符串可变,源码中提供改变字符串所需的函数
1 | //用于保证容量至少与给定的最小值相等 |
- 返回某一下标处的字符
1 | public char charAt(int index) { |
- 改变某一下标处的字符
1 | public void setCharAt(int index, char ch) { |
- 在现有字符串后面添加新的字符串
1 | public AbstractStringBuilder append(String str) { |
- 替换,此处的替换与String类中的不同,并非返回新的字符串,而是在原有字符串基础上进行改变并返回
1 | public AbstractStringBuilder replace(int start, int end, String str) { |
- 插入/删除
1 | public AbstractStringBuilder insert(int index, char[] str, int offset, |
- 返回子串
1 | public String substring(int start) { |
StringBuilder类
主要结构
StringBuilder类继承于AbstractStringBuilder,不同于String,是一个可变类。其主要结构如下:
1 | public final class StringBuilder |
构造函数
StringBuilder的构造函数大多调用父类的接口完成。
1 | public StringBuilder() { |
常用方法
StringBuilder的方法(append、insert等)大多调用父类接口,因此代码很短,实现较为方便。这里不再赘述其代码实现方法。
StringBuffer类
主要结构
和StringBuilder一样,StringBuffer也继承于AbstractStringBuilder类,也是可变类。
1 | public final class StringBuffer |
构造函数
StringBuffer的构造函数形式和StringBuilder几乎完全一样,都是以继承父类为主
1 | public StringBuffer() { |
常用方法
StringBuffer与StringBuilder的主要方法结构都非常相似,不同点在于StringBuffer的方法前都有synchronized
关键字作为修饰。即,StringBuffer类的方法每次只有一个线程可以访问,实现了线程安全。
String、StringBuffer、StringBuilder的对比
在分析完源代码之后,可以得出三者的主要区别如下:
String为不可变类,即其值为一个常量,不可以被改变。其所在的内存区域为常量池。
StringBuilder为可变类,其值可以通过已定义的函数接口发生改变,分配的内存区域为堆。
StringBuffer为可变类,定义的方法每次只能由一个线程访问,实现线程安全,分配的内存区域为堆。
三者都不可以被继承。
由于三者结构和功能存在区别,它们适合的场景也有一定区别。
由于每次对String的值进行改变时(例如连接、替换等),JVM将会生成一个新的字符串,将原来String的名字链接到新的字符串上,并且回收原有字符串。而在对StringBuffer和StringBuilder进行操作时,只是简单改变其自身的值。这导致了改变字符串时,String的性能和效率显著低于另外两个类。
因此,String适合的场景为,对少量字符串进行操作,并且操作较少的情况。StringBuffer和StringBuilder比较适合对字符串进行复杂操作的场景。其中StringBuffer由于实现线程安全,比较适合在多线程的场景中使用,而StringBuilder在单线程场景中使用效率会比较高。
思考题
1 | String s1 = "Welcome to Java"; |
Q: 为什么s1\==s2 返回false,而s1==s3返回true?
A: 由于String的对象存储在常量池中,s1在常量池中对应”Welcome to Java”,创建s2时由于使用new,创建了一个新的常量,与s1所对应常量不为同一个,故返回false。而s3被直接对应到常量池中”Welcome to Java”,与s1对应常量相同,故返回true。