【JavaSE学习笔记】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.各项指标

类型详情参数变化线程安全多线程支持
StringString的值是不可变的,这就导致每次对String的操作都会生成新的String对象,不仅效率低下,而且浪费大量优先的内存空间不可变线程安全-
StringBufferStringBuffer是可变类和线程安全的字符串操作类,任何对它指向的字符串的操作都不会产生新的对象。每个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帮助文档百度网盘链接

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值