Java中String、StringBuffer、StringBuilder之间的区别与常用方法
一、String、StringBuffer、StringBuilder之间的区别
Java中与字符串操作相关的类有String、StringBuffer、StringBuilder三种。他们具有不同的特性、因此使用场景有所不同。String是不可变的,适合字符串常量;StringBuffer线程安全,适合多线程操作;StringBuilder非线程安全但效率更高,适用于单线程。本文将首先分析他们的区别,然后介绍各自的常用方法。
1.String讲解
String类型是不可变的对象,在源码中使用final修饰,因此每次对String类型进行修改的时候其实都等于生成了一个新的String对象,然后将指针指向新的String对象,这样效率低下,而且会浪费有限的内存空间。所以如果字符串的内容需要经常改变,最好不要使用String。String适合作为常量使用。
截取部分源码,String类中的成员变量value被final关键字修饰,因此value只能被赋值一次,所以String类型是不可变的对象。
public final class String implements java.io.Serializable, Comparable<String>, CharSequence {
@Stable
private final byte[] value;
public String() {
this.value = "".value;
this.coder = "".coder;
}
}
通过一个例子来说明String类型的不可变这一特性。首先给字符串str赋值“a”,打印出str的地址为“488970385”。再给字符串str重新赋值“b”,打印出str的地址为“2137589296”。发现两次打印出的地址不同。说明,str虽然变量名相同,但是已经不是同一个对象了。这是因为重新给str赋值时,JVM会重新new一个String对象,并把地址赋值给str。
String str = "a";
System.out.println("地址:" + System.identityHashCode(str));
str = "b";
System.out.println("地址:" + System.identityHashCode(str));
// 输出
// 地址:488970385
// 地址:2137589296
2.StringBuffer讲解
StringBuffer是可变类和线程安全的字符串操作类,任何对它指向的字符串的操作都不会产生新的对象。每个StringBuffer对象都有一定的缓冲区容量,当字符串大小没有超过容量时,不会分配新的容量,当字符串大小超过容量时,会自动增加容量 。
截取部分源码对StringBuffer类进行分析。可以看到,StringBuffer类继承了AbstractStringBuilder类,而AbstractStringBuilder类中有一个成员变量value,这个value和String中value的区别是,AbstractStringBuilder中的value没有使用final关键字修饰,而String中的value使用关键字修饰了。因此继承了AbstractStringBuilder的StringBuffer是可变的。StringBuffer中的方法使用synchronized(同步锁)关键字修饰,保证了线程安全性。在源码中有一个值得注意的地方,就是在StringBuffer的构造方法中,默认会增加16字节的预留空间。
public final class StringBuffer
extends AbstractStringBuilder
implements Serializable, Comparable<StringBuffer>, CharSequence
{
@HotSpotIntrinsicCandidate
public StringBuffer(String str) {
super(str.length() + 16);
append(str);
}
@Override
public synchronized int compareTo(StringBuffer another) {
return super.compareTo(another);
}
@Override
public synchronized int length() {
return count;
}
@Override
public synchronized int capacity() {
return super.capacity();
}
}
abstract class AbstractStringBuilder implements Appendable, CharSequence {
byte[] value;
}
通过下面的代码看一下产生新的字符串时,是否会产生新的对象,以验证StringBuffer参数是可变的。运行结果表明,次输出的地址值相同,说明是同一个对象。未产生新的对象。
StringBuffer sb = new StringBuffer("a");
System.out.println("地址:" + System.identityHashCode(sb));
sb = sb.append("b");
System.out.println("地址:" + System.identityHashCode(sb));
// 运行结果
// 地址:1209271652
// 地址:1209271652
3.StringBuilder讲解
StringBuilder类是可变类,但是不支持线程安全,因此常在单线程场景下使用。该类被设计用作StringBuffer的简易替换,用在字符串缓冲区被单个线程使用的时候。StringBuilder比StringBuffer更快,因此在合适的场景下,考虑优先使用StringBuilder。
4.各项指标
类型 | 详情 | 参数变化 | 线程安全 | 多线程支持 |
---|---|---|---|---|
String | String的值是不可变的,这就导致每次对String的操作都会生成新的String对象,不仅效率低下,而且浪费大量优先的内存空间 | 不可变 | 线程安全 | - |
StringBuffer | StringBuffer是可变类和线程安全的字符串操作类,任何对它指向的字符串的操作都不会产生新的对象。每个StringBuffer对象都有一定的缓冲区容量,当字符串大小没有超过容量时,不会分配新的容量,当字符串大小超过容量时,会自动增加容量 | 可变 | 线程安全 | 多线程操作字符串 |
StringBuilder | 可变类,速度更快 | 可变 | 线程不安全 | 单线程操作字符串 |
5.常见争论
- 常见争论1:关于字符串相等关系的争论
//代码1
String sa=new String("Hello world");
String sb=new String("Hello world");
System.out.println(sa==sb);
// 运行结果:false
//代码2
String sc="Hello world";
String sd="Hello world";
System.out.println(sc==sd);
// 运行结果:true
实例解析:
代码1中局部变量sa,sb中存储的是JVM在堆中new出来的两个String对象的内存地址。虽然这两个String对象的值(char[]存放的字符序列)都是"Hello world"。因此"=="比较的是两个不同的堆地址。代码2中局部变量sc,sd中存储的也是地址,但却都是常量池中"Hello world"指向的堆的唯一的那个拘留字符串对象的地址 。自然相等了。
有两个结论:1.在JVM在堆中new出来的两个String对象的内存地址,值相同,但是堆地址不同。2.在String直接赋值时,都是常量池中指向的堆的唯一的拘留字符串对象的地址。值相同,堆地址也相等。(String直接赋值赋值时会在常量池中查看是否存在该字符串,若存在直接保存该值的堆地址,然而new则不同,都会重新创建并生成堆地址)。
- 字符串 “+” 操作是否相等
//代码1
String sa = "ab"; //拘留字符串对象
String sb = "cd"; //拘留字符串对象
String sab=sa+sb; //调用StringBuilder的toString()方法在堆中创建的String对象
String s="abcd"; //拘留字符串对象
System.out.println(sab==s); // false
//代码2
String sc="ab"+"cd";
String sd="abcd";
System.out.println(sc==sd); //true
实例解析:
代码1中局部变量sa,sb存储的是堆中两个拘留字符串对象的地址。而当执行sa+sb时,JVM首先会在堆中创建一个StringBuilder类,同时用sa指向的拘留字符串对象完成初始化,然后调用append方法完成对sb所指向的拘留字符串的合并操作,接着调用StringBuilder的toString()方法在堆中创建一个String对象,最后将刚生成的String对象的堆地址存放在局部变量sab中。而局部变量s存储的是常量池中"abcd"所对应的拘留字符串对象的地址。 sab与s地址当然不一样了。这里要注意了,代码1的堆中实际上有五个字符串对象:三个拘留字符串对象、一个String对象和一个StringBuilder对象。代码2中"ab"+“cd"会直接在编译期就合并成常量"abcd”, 因此相同字面值常量"abcd"所对应的是同一个拘留字符串对象,自然地址也就相同。
有两个结论:1.字符串在进行两个对象合并后,创建StringBuilder并调用StringBuilder的toString()方法在堆中创建新的String对象,从而在堆中生成新的堆地址。而字符串直接赋值则是在常量池中,所对应的拘留字符串对象的地址。2.两个字符串直接在编译期就拼接合并与字符串直接赋值,他们编译前都是相同的字面值常量,所对应的也会是同一个拘留字符串对象,自然地址也就相同。
二、常用方法
1.String
1.1 判断字符串是否相同 equals()
判断两个字符串是否相同时一般不用 ==,因为可能两个字符串指针指向的地址保存的内容相同,但是可能这两个字符串的地址不同,而当两个指针使用==比较时,==比较的是地址值。
String str1 = new String("a");
String str2 = new String("a");
System.out.println(str1 == str2);
System.out.println(str1.equals(str2));
// 运行结果
// false
// true
1.2 返回指定位置的字符 charAt()
String str = "abcde";
int idx = 0;
System.out.println("第" + idx + "个位置的字符为: " + str.charAt(idx));
// 运行结果
// 第0个位置的字符为: a
1.3 拼接字符串 concat()
String str1 = "a";
String str2 = "b";
// 将 str2 拼接到 str1 后面
System.out.println("拼接后字符串:" + str1.concat(str2));
// 运行结果
// 拼接后字符串:ab
1.4 返回字符首次/最后一次出现的索引 indexOf()/lastIndexOf()
String str = "abcdeabcde";
char ch = 'a';
System.out.println("字符 " + ch + " 在字符串中首次出现的索引为:" + str.indexOf(ch));
System.out.println("字符 " + ch + " 在字符串中最后一次出现的索引为:" + str.lastIndexOf(ch));
// 运行结果
// 字符 a 在字符串中首次出现的索引为:0
// 字符 a 在字符串中最后一次出现的索引为:5
1.5 截取字符串的子串 subString()
用法1:传入一个参数,从指定位置开始截取子串
String str = "abcde";
int idx = 1;
System.out.println("截取的子串为:" + str.substring(idx));
// 运行结果
// 截取的子串为:bcde
用法2:传入两个参数,截取两个索引位置之间的子串,含左不含右
String str = "abcdefgh";
int idx1 = 1, idx2 = 4;
System.out.println("截取的子串为:" + str.substring(idx1, idx2));
// 运行结果(包含idx1索引位置的字符,不包含idx2索引位置的字符)
// 截取的子串为:bcd
1.6 判断字符串是否为空 isEmpty()
String类提供了判断字符串是否为空的方法,返回值为boolean类型。该方法是通过字符串的length是否为0来进行判断的,因此当String的对象指向null时,会抛出空指针异常。
String str1 = "";
String str2 = " ";
String str3 = null;
System.out.println(str1.isEmpty()); //true
System.out.println(str2.isEmpty()); //false
System.out.println(str3.isEmpty()); //java.lang.NullPointerException
针对String指向空指针的情况,可以使用org.apache.commons.lang3下的StringUtils中的isEmpty()和isBlank()方法。使用前需要引入相关依赖,这里引入的版本为3.8.1。
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-lang3</artifactId>
<version>3.8.1</version>
</dependency>
StringUtils类是null安全的,即如果输入参数 String 为 null,则不会抛出 NullPointerException 空指针异常,代码考虑更全面。
String str1 = "";
String str2 = " ";
String str3 = null;
System.out.println(StringUtils.isEmpty(str1)); // true
System.out.println(StringUtils.isEmpty(str2)); // false
System.out.println(StringUtils.isEmpty(str3)); // true
1.7 判断字符串是否包含指定的子串 contains()
contains()需要的参数为CharSequence类的对象,String、StringBuffer、StringBuilder都直接或间接的继承自CharSequence类。因此String、StringBuffer、StringBuilder的对象都可以作为参数传入。
String str = "Hello World!";
StringBuffer strBuffer = new StringBuffer("Hello");
StringBuilder strBuilder = new StringBuilder("Hello");
System.out.println(str.contains("Hello")); // true
System.out.println(str.contains(strBuffer)); // true
System.out.println(str.contains(strBuilder)); // true
1.8 字符串大小写转换 toUpperCase()/toLowerCase()
String str = "Hello";
System.out.println(str.toUpperCase());
System.out.println(str.toLowerCase());
// 运行结果
// HELLO
// hello
1.9 替换字符串 replace()
replace(char oldChar, char newChar) 方法:使用参数newChar替换此字符串中出现的所有参数oldChar,返回值为 String 类型,参数为 char 类型
replace(CharSequence target, CharSequence replacement) 方法:用新字符串replacement替换所有的旧字符串target,返回值为 String 类型,参数为 CharSequence 接口
String str = "Hello World!";
System.out.println(str.replace('W', 'w')); // "w" 替换 "W"
System.out.println(str.replace("World", "Java")); // "Java" 替换 "World"
// 运行结果
// Hello world!
// Hello Java!
1.10 使用指定字符分割字符串 split()
String str = "red:yellow:blue";
String[] split = str.split(":");
for (String s : split) {
System.out.println(s);
}
// 运行结果
// red
// yellow
// blue
2.StringBuffer
2.1 创建StringBuffer对象
StringBuffer sb = new StringBuffer(); // 无参构造
StringBuffer sb1 = new StringBuffer("abcd"); // 有参构造,初始化字符串为"abcd"
StringBuffer sb2 = new StirngBuffer(100); // 有参构造,设置初始容量为100byte,避免多次扩容,一般在知道字符串长度时使用
2.2 将字符串追加到StringBuffer对象末尾 append()
StringBuffer sb = new StringBuffer();
sb.append("aaa");
sb.append("bbb");
System.out.println(sb);
// 运行结果
// aaabbb
2.3 在指定位置插入字符串 insert()
StringBuffer sb = new StringBuffer();
System.out.println(sb.append("aaaa").insert(2, "bbb"));
// 运行结果 (从索引2位置开始插入“bbb” ,StringBuffer支持链式编程)
// aabbbaa
2.4 删除指定字符串/字符 delete()/deleteCharAt()
StringBuffer sb = new StringBuffer("Hello World!");
int idx1 = 5, idx2 = 11;
System.out.println(sb.delete(idx1, idx2)); // 删除idx1和idx2之间的字符串,包含idx1,不包含idx2
System.out.println(sb.deleteCharAt(1)); // 删除索引为 1 位置的字符
// 运行结果
// Hello!
// Hllo!
2.5 替换指定范围内的字符串 replace()
StringBuffer sb = new StringBuffer("Hello World!");
int idx1 = 6, idx2 = 11;
System.out.println(sb.replace(idx1, idx2, "Java"));
// 运行结果 (替换idx1和idx2之间的字符串,包含idx1,不包含idx2)
// Hello Java!
2.6 翻转字符串 reverse()
StringBuffer sb = new StringBuffer("abcde");
System.out.println(sb.reverse());
// 运行结果
// edcba
2.7 转换为String对象 toString()
StringBuffer sb = new StringBuffer("Hello");
System.out.println("转换前的类:" + sb.getClass() + ";字符串内容:" + sb);
String str = sb.toString(); // 将StringBuffer类的对象转换成String
System.out.println("转换后的类:" + str.getClass() + ";字符串内容:" + str);
// 运行结果 (内容不变 但是已经转换成了String类的对象)
// 转换前的类:class java.lang.StringBuffer;字符串内容:Hello
// 转换后的类:class java.lang.String;字符串内容:Hello
2.8 获取StringBuffer对象的当前容量
StringBuffer sb = new StringBuffer("Hello!");
int capacity = sb.capacity();
System.out.println("capacity = " + capacity);
// 输出结果 (字符串“Hello!”所占内存为6byte,而StringBuffer占22byte。这是因为StringBuffer在初始化时会额外增加16byte的预留空间。)
// capacity = 22
2.9 获取StringBuffer对象的当前长度
StringBuffer sb = new StringBuffer("Hello!");
int length = sb.length();
System.out.println("length = " + length);
// 运行结果 (这里需要注意与capacity()函数的区别,length()函数只取有效内容的长度)
// length = 6
3.StringBuilder
由于StringBuilder与StringBuffer有相同的继承体系,这两个类实现的API接口有很高的相似性,因此StringBuilder的常用方法可以参考上述StringBuffer的介绍。
三、参考链接
String、StringBuffer和StringBuilder的详解
StringBuffer(史上最详细)
【JAVA-Day45】Java常用类StringBuffer解析
【Java 基础篇】Java StringBuilder:可变的字符串操作
Java中的String,这一篇就够了
subString的用法小结
Java中String类的常用方法
更详细的使用方法参考Java帮助文档:Java帮助文档百度网盘链接