文章目录


前言

      本篇博客使对​​String​​​、​​StringBuffer​​​及​​StringBuilder​​的相关整理,如有疑问,欢迎与我交流沟通。

      其他博客文章索引目录:博客目录索引(持续更新)




一、String字符串

1.1、认识String类

Java学习笔记 String、StringBuffer与StringBuilder_字符数组

​String​​​:表示为字符串,可以使用字符串字面值与类实例来给该类进行赋值,底层是使用​​fina char[] value​​​(常量字符数组),并且String类是​​final​​常量类,无法被继承,无参构造是创建空的字符串。

//常量类:无法被继承
public final class String
implements java.io.Serializable, Comparable<String>, CharSequence {
//底层使用char型数组保存字符串
private final char value[];
....
}

特性说明:​​String​​​代表不可变的字符序列,​​String​​​对象一旦创建了,对​​String​​对象的任何改变都不回影响到原对象,任何更改对象内容的操作都会生成新的对象。

看下​​String​​类中的几个更改操作方法:

//合并
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);
str.getChars(buf, len);
return new String(buf, true);
}

//替换
public String replace(char oldChar, char newChar) {
if (oldChar != newChar) {
int len = value.length;
int i = -1;
char[] val = value; /* avoid getfield opcode */

while (++i < len) {
if (val[i] == oldChar) {
break;
}
}
if (i < len) {
char buf[] = new char[len];
for (int j = 0; j < i; j++) {
buf[j] = val[j];
}
while (i < len) {
char c = val[i];
buf[i] = (c == oldChar) ? newChar : c;
i++;
}
return new String(buf, true);
}
}
return this;
}
  • 可以看到执行这些方法,最后都返回了新的​​String​​对象。

这也就是为什么​​String​​被称为不可变的字符序列。




1.2、String两种赋值方式(=、new)

首先看几种赋值方式

public static void main(String[] args) {
//两种创建字符串方式:
//方式一:通过在常量池中创建对象返回引用给s
String s = "changlu";
//方式二:通过new实例,将引用返回给s1
String s1 = new String("changlu");
//+来合并字符串(也是有区别的)
String s2 = "chang"+"lu";
String s3 = "" + s;
}

Java学习笔记 String、StringBuffer与StringBuilder_java_02

  • 方式一:对于直接赋值字面值的s来说,会在常量池中创建一个字符串并将其引用地址传给s。
  • 方式二:对于使用​​new​​​创建字符串的,会先判断常量池中是否有此字符串,①若没有,会创建两个对象(常量池及堆),先在常量池中创建一个字符串,将字符串引用传递给堆中对象中的​​value​​​,接着将堆中对象的引用地址传递给​​s1​​​;②若有,创建一个对象(堆),首先将常量池中找的该字符串将其引用传递给堆中对象的​​value​​​,接着再将堆中创建的对象引用传递给​​s1​​。
  • 例题情况:由于之前已在常量池中创建了一个字符串"changlu",所以只在堆中创建了一个对象,接着步骤与②一致。



认识下字符串常量池(Constant Pool Table)


字符串常量池出现原因:字符串的分配与其他对象相同,需要消耗高昂的时间与空间,对于字符串的使用是很频繁的,JVM为了提高性能和减少内存的开销引入了字符串常量池,目的是为了共享字符串,相当于给字符串开辟了一个空间,每当创建字符串时会先去常量池中找是否有已经创建相同的字符串,若有直接返回,从而省去了创建的过程(但是有一个遍历查找的过程)。

  • 一旦我们创建字符串时,首先会去检查字符串常量池中是否存在要创建的字符串,若是存在会直接返回该字符串的引用;若不存在才会去创建。

重复赋值区别

String s = "changlu";//首先在常量池中创建该字符串,再返回引用。(有创建过程)
String s1 = "changlu";//在常量池中发现了已创建的字符串返回引用。(无创建过程)

注意点:对于常量池中的字符串在创建之后无法更改,若是使用"+"字符串(在常量区找合并的字符串是否存在),若不存在,则会在字符串常量区中重新再创建一个字符串。

关于字符串常量区所处的位置

  • ​JDK1.6​​:字符串常量池在方法区中(具体实现:永久代)。
  • ​JDK1.7​​:字符串常量池在堆中。
  • ​JDK1.8​​:字符串常量池在方法区(具体实现:元空间)。



1.3、字符串赋值的各类情况

我们先看一道题

public static void main(String[] args) {
String s = "changlu";
String s1 = new String("changlu");
String s2 = "chang" + "lu";
String s3 = "" + s;
//判断是否相同
System.out.println(s == s1);//false
System.out.println(s == s2);//true
System.out.println(s == s3);//false
System.out.println(s1 == s2);//false
System.out.println(s1 == s3);//false
System.out.println(s2 == s3);//false
}

我们将所有字符串都进行了逐一比较,对于这种题很容易混淆不清,实际上我们只需要知道他们对应引用的地址是在哪里创建之后,对于这种问题就很轻松了。

首先记几个关键点

  1. 对于使用=直接赋值字符串的(非拼接),一定是使用的常量区的引用。
  2. 对于new出来的字符串对象,一定是在堆中的引用。
  3. 常量与常量的拼接结果在常量池。(注意常量池不会有相同内容的常量)
  4. 常量与变量的拼接结果在堆中。(在堆中新创建一个对象)

分析过程

String s = "changlu";//在常量区创建字符串对象"changlu",在常量区中地址为0x11,返回引用给s。==>s=0x11
String s1 = new String("changlu");//s1引用地址的对象中的底层数组value引用地址0x11,而s1存放的是堆中创建对象的引用地址为0x22。==>s1=0x22
String s2 = "chang" + "lu";//对于常量与常量拼接,在编译器完成,所以会直接去常量区找拼接后的字符串是否存在,这里是找到将地址0x11引用给s2。==>s2=0x11
String s3 = "" + s;//常量与变量(字符串对象实例)拼接,会在堆中创建新的对象地址为0x36,该对象中的value则会引用常量区的字符串,由于也已经存在了所以将0x11引用给value。==>s3=0x36

对于引用数据类型进行==判断,是比较它们的引用地址,根据上面分析就能够很快得出程序最后的执行结果了。

补充点

  • 若是常量与常量拼接,在编译期就能够确定了,jvm会直接在此期间就优化如​​"chang"+"lu"​​​为​​"changlu"​​​,然后直接拿​​"changlu"​​去常量池中找,若有直接返回引用地址,没有则在常量区创建并返回引用地址。
  • 若是常量与变量拼接,在编译期无法确定变量的引用地址,如​​""+s​​中的s无法确定,那么就不能在编译期优化字符串,只有在程序运行期间动态分配地址并将新地址赋值给栈中的变量。

对于补充点1中的实际验证可以去参考文章2的链接中查看。




1.4、认识intern()方法

​intern()​​​方法是​​String​​类中方法:

public native String intern();
  • 该方法是一个本地方法,底层调用​​c++​​的方法实现。

由于知识量有限,所以直接说结论不往深处探讨:使用该方法会返回指向常量池的地址

public static void main(String[] args) {
String str = new String("changlu");
String str1 = "changlu";
System.out.println(str == str1);//false
System.out.println(str1 == str.intern());//true
}
  • ​str​​指向堆中的开辟的地址;
  • ​str1​​指向常量区的地址;
  • ​str1.intern()​​指向堆中实例的value对应的地址(即常量区地址);



1.5、常用方法


字符串与基本数据类型及包装类转换方法


字符串 => 基本数据类型及包装类:​​Integer​​​包装的​​parseInt(String s);​​​,其他包装类类似​​parseXXX(String s);​

基本数据类型、包装类 => 字符串:调用不同的String类静态方法​​String valueOf(int i)​​​ 及其他的​​valueOf()​​方法

字符数组 => 字符串(String构造器):​​String(char[]) ​​​和​​ String(char[],int offset,int length)​​。

字符串 => 字符数组(String静态方法):​​public char[] toCharArray()​​​,​​public void getChars(int srcBegin, int srcEnd, char[] dst, int dstBegin)​

字节数组 => 字符串(String构造器):​​String(byte[])​​​、​​String(byte[],int offset,int length)​

字符串 => 字节数组(String静态方法):​​public byte[] getBytes()​​​ 、​​public byte[] getBytes(String charsetName)​




常见方法


引用的是尚硅谷的课件

Java学习笔记 String、StringBuffer与StringBuilder_常量区_03

Java学习笔记 String、StringBuffer与StringBuilder_java_04

Java学习笔记 String、StringBuffer与StringBuilder_字符数组_05

Java学习笔记 String、StringBuffer与StringBuilder_字符串_06

Java学习笔记 String、StringBuffer与StringBuilder_字符数组_07

Java学习笔记 String、StringBuffer与StringBuilder_java_08分类总结:长度、指定索引、为空、大小写转换、比较、连接、截取、测试前后缀、是否包含、替换(单个、所有)、正则匹配、拆分字符串。




相关面试题

例1、在使用​​new String("")​​创建了几个对象?

通过看博客得知一个比较好的答案,1个或2个。使用​​new​​来创建字符串对象时会有如下过程,首先会去检查要创建字符串在字符串常量区中是否存在?

  • ①如果存在,那么只创建一个对象在堆区,接着将在常量区中找到的字符串引用返回给堆中对象的value,再将堆中对象的引用地址返回给栈中的变量。
  • ②如果不存在,那么会创建2个对象(堆与常量区),首先在常量区中创建字符串并将引用传递给堆中对象的value,接着再将堆中对象的引用传递给栈中的常量。


例2:输出下面运行结果

public class StringTest {
String s = new String("changlu");
char[] ch = {'h','a','v','a'};

public void change(String s,char ch[]){
s = "liner";
ch[0] = 'j';
}

public static void main(String[] args) {
StringTest str = new StringTest();
str.change(str.s,str.ch);
System.out.println(str.s);
System.out.println(str.ch);
}
}

Java学习笔记 String、StringBuffer与StringBuilder_字符数组_09

说明:本题还是有些迷惑性的,结果是​​str.s​​​没有改变,​​str.ch​​改变了。为啥呢,我们看个图一下子就能懂了。

Java学习笔记 String、StringBuffer与StringBuilder_常量区_10

  • 为什么str.s没有更改?因为方法中只是传递了一个String的字符串,那么给它重新赋值字面值,过程是在常量区创建liner,之后将引用地址给s,并没有涉及到str中s的引用地址指向。
  • str.s更改了?方法中传递的是地址值0x34,做的操作是0x34地址部分的第一个字符赋值为j,对应str实例中的ch依旧是指向0x34,所以最后结果得出更改了。

那么怎样传递才能让str.s改变值呢

//方法中参数改为str的实例,这时候传过来更改其中的s变量值才是有效的,因为是对str实例中的属性进行更改操作
public void change1(StringTest str){
str.s = "liner";
str.ch[0] = 'j';
}




二、StringBuffer类

2.1、认识StringBuffer类

Java学习笔记 String、StringBuffer与StringBuilder_字符数组_11

​StringBuffer​​​:代表可变的字符序列,是线程安全,效率低,可对字符串内容进行增删(如append()方法)不会返回新的对象,大多方法与​​String​​​相同,底层使用的是其父类中的​​char[]​​数组存储数据。

//StringBuffer类
public final class StringBuffer
extends AbstractStringBuilder
implements java.io.Serializable, CharSequence
{
public StringBuffer() {//调用的是父类的有参构造器
super(16);
}
}

//AbstractStringBuilder类
abstract class AbstractStringBuilder implements Appendable, CharSequence {

char[] value;//这里相较于String底层的value,这里不为final,是可变的字符序列

AbstractStringBuilder(int capacity) {
value = new char[capacity];//使用value来存储数据
}
}

构造器介绍:​​StringBuffer​​对象必须要由构造器来生成

  • ​StringBuffer()​​:初始容量为16的字符串缓冲区,开辟了16位的字符数组。
  • ​StringBuffer(int size)​​:构造指定容量的字符串缓冲区。
  • ​StringBuffer(String str)​​:将内容初始化为指定字符串内容。
public StringBuffer() {
super(16);//父类有参构造器,创建大小为16位的字符数组
}

public StringBuffer(int capacity) {
super(capacity);//创建capacity位数的字符数组
}

public StringBuffer(String str) {
super(str.length() + 16);//创建str长度加上16位的字符数组
append(str);
}




2.3、常用方法

在​​StringBuffer​​类中包含了各种对字符串的增删改查方法:

  • ​synchronized StringBuffer append(Object obj)​​:追加。
  • ​synchronized StringBuffer deleteCharAt(int index)​​:删除指定位置的字符。
  • ​synchronized void setCharAt(int index, char ch)​​:修改指定位置的字符。
  • ​synchronized char charAt(int index)​​:查询指定位置的字符。
  • ​synchronized StringBuffer insert(int offset, Object obj)​​​:插入指定的对象到指定位置(对obj使用​​valueOf()​​方法先转字符串)
  • ​synchronized int length()​​:获取数组中已有字符的个数。
  • ​synchronized String toString()​​​:返回的是一个​​String​​字符串。

可以看到该方法是线程安全的,都包裹了一层​​synchronized​​,执行效率比较低。




2.2、源码分析

首先分析​​append()​​方法:原本字符串后添加指定字符串。

//StringBuffer类 
@Override
public synchronized StringBuffer append(String str) {
toStringCache = null;
super.append(str);//调用父类AbstractStringBuilder的append()
return this;
}

//1.1、AbstractStringBuilder类
public AbstractStringBuilder append(String str) {
if (str == null)
return appendNull();//若str为null,则会追加字符串"null"到后面 (调用方法1.1.1)
int len = str.length();//获取str字符串长度
ensureCapacityInternal(count + len);//调用1.1.1.1 进行确保数组容量大小够用
str.getChars(0, len, value, count);//将方法传递的str中0-len位置的字符追加到value之后 完成追加操作
count += len;//这里是便于统计字符数组中的个数
return this;
}

//1.1.1 AbstractStringBuilder类
private AbstractStringBuilder appendNull() {
int c = count;
ensureCapacityInternal(c + 4);//目的确保字符数组中的容量足够,不够开辟对应的数组 (调用方法1.1.1.1)
final char[] value = this.value;
value[c++] = 'n';
value[c++] = 'u';
value[c++] = 'l';
value[c++] = 'l';
count = c;
return this;
}

//1.1.1.1 AbstractStringBuilder类
private void ensureCapacityInternal(int minimumCapacity) {
// overflow-conscious code
if (minimumCapacity - value.length > 0) {//若是要开辟的最小数量-现有的数量>0,说明现在字符数组容量不够
value = Arrays.copyOf(value,
newCapacity(minimumCapacity));//将现有的字符数组复制到一个创建新的容量大小的字符数组中并返回给value,该容量为原本数组容量的2倍+2,若还不够则直接设置容量为minCapacity (调用1.1.1.1.1与1.1.1.1.2)
//返回开辟新空间的字符数组到value
}
}

//1.1.1.1.1 AbstractStringBuilder类
private int newCapacity(int minCapacity) {
// overflow-conscious code
int newCapacity = (value.length << 1) + 2;//上来就扩建原本数组容量的2倍+2
if (newCapacity - minCapacity < 0) {//若容量依旧不够,扩建容量为minCapacity
newCapacity = minCapacity;
}
return (newCapacity <= 0 || MAX_ARRAY_SIZE - newCapacity < 0)
? hugeCapacity(minCapacity)//依旧不满足当前扩建容量的情况
: newCapacity;//返回创建容量大小
}

//1.1.1.1.2 Arrays类
public static char[] copyOf(char[] original, int newLength) {
char[] copy = new char[newLength];//创建newLength容量大小的字符数组
System.arraycopy(original, 0, copy, 0,
Math.min(original.length, newLength));//将original字符数组中的复制到copy中(复制多少由newLength与原本字符数组的大小决定)
return copy;//返回字符数组
}
  • 中间会有扩容的操作(首次扩容为原本数组的2倍+2,依旧不够则为传递到方法参数的​​minimumCapacity​​​),将原来字符数组中内容复制到新开辟的字符数组中最后返回给​​value​​​,之后再将参数str中的字符追加到​​value​​后。
  • ​append()​​返回的是this,可以链式使用方法多次进行调用。



length()源码


//StringBuffer类
@Override
public synchronized int length() {//上了锁,是线程安全的
return count;//返回由count统计的现有字符长度
}




三、StringBuilder

Java学习笔记 String、StringBuffer与StringBuilder_java_12

​StringBuilder​​​:可变的字符序列,不是线程安全的,执行效率高,提供的方法也与​​StringBuffer​​的功能都差不多。




四、String、StringBuffer、StringBuilder对比

效率排名:​​String<StringBuffer<StringBuilder​

  • ​String​​:由于本身是不可变的字符序列,所以每次append()都需要重新创建内存空间,效率大大减低。
  • ​StringBuffer​​:可变的字符序列,是线程安全的,效率会略微慢些。
  • ​StringBuilder​​:可变的字符序列,非线程安全的,效率最高。

我们测试一下三个类的​​append()​​​方法:分别​​append()​​​200000次,​​String​​使用+来连接字符串

public static void main(String[] args) {
long startTime = 0L;
long endTime = 0L;
String text = "";
StringBuffer buffer = new StringBuffer("");
StringBuilder builder = new StringBuilder("");
//开始对比
startTime = System.currentTimeMillis();
for (int i = 0; i < 200000; i++) {
buffer.append(String.valueOf(i));
}
endTime = System.currentTimeMillis();
System.out.println("StringBuffer的执行时间:" + (endTime - startTime));
startTime = System.currentTimeMillis();
for (int i = 0; i < 200000; i++) {
builder.append(String.valueOf(i));
}
endTime = System.currentTimeMillis();
System.out.println("StringBuilder的执行时间:" + (endTime - startTime));
startTime = System.currentTimeMillis();
for (int i = 0; i < 200000; i++) {
text = text + i;
}
endTime = System.currentTimeMillis();
System.out.println("String的执行时间:" + (endTime - startTime));
}

Java学习笔记 String、StringBuffer与StringBuilder_常量区_13

  • 可以看到​​StringBuilder​​​明显最快,​​StringBuffer​​​略微慢些,相差不大。而​​String​​则高达43秒,效率特别低。



参考文章

[1]. 深入理解Java中的String

[2]. 别再问我 new 字符串创建了几个对象了!我来证明给你看!

[3]. ​​Java中String内存地址的问题(为什么String 的变量输出不是地址)​



我是长路,感谢你的耐心阅读,如有问题请指出,我会听取建议并进行修正。
欢迎关注我的公众号:长路Java,其中会包含软件安装等其他一些资料,包含一些视频教程以及学习路径分享。
编程学习qq群:891507813 我们可以一起探讨学习
注明:转载可,需要附带上文章链接