Java面试题——轻松应对String、StringBuffer和StringBuilder(看这一篇就够了)

#Java学了这么久,项目也做了?基础知识还不巩固?快来关注我的这篇系列博客——Java基础复习巩固吧;#

引言:

        在Java中 String、StringBuffer 和 StringBuild 都是用于操作字符串的三种不同的类,在选择使用 String 、StringBuffer 还是 StringBuilder 时,需要根据具体的应用场景和线程安全性要求来决定,以达到最佳的性能和功能平衡。那么他们三者之间又有什么区别?使用过程中又该如何选择呢?

一、String类

  1. String 类代表字符串常量。Java 程序中的所有字符串字面值(如 "abc" )都作为此类的实例实现。
  2. String类实现的接口:Serializable(序列化接口)、Comparable<String>(比较大小的接口);表示String类的实例可以被序列化或者互相比大小。
  3. 字符串是常量;它们的值(对象)在创建之后,内容不能被更改,但是可以共享。例如:

    String str1 = "abc";

    String str2 = "abc";

    //str1和str2这两个引用,指向同一个String对象:"abc";

    //也可说str1和str2这两个引用共享同一个String实例:"abc";

    且 String str = "abc" ;这种创建方式等效于:

    String str = new String("abc");

优点:线程安全,由于其不可变性,String 对象可以被安全地共享和缓存。适合在多线程环境中使用,不用担心数据被意外修改。

        什么是线程安全?(可参考以下博客):什么是线程安全?如何保证线程安全?-CSDN博客

缺点:每次对 String 对象进行修改操作(如拼接)时,实际上都会创建一个新的 String 对象,这会导致性能开销较大。

例如,如果在一个循环中频繁地拼接 String :

String str = "";
for (int i = 0; i < 1000; i++) {
    str += i;
}

 在上述代码中,每次循环都会创建一个新的 String 对象,会创建1000次 String 对象,效率低下。

如何理解String类的不可变性:

  • 当字符串重新赋值时,不会修改原来内存地址中的字符串值,而是重新分配新的内存地址进行赋值。
  • 当字符串进行拼接时,也不会对原来内存地址中的字符串进行修改,而是重新分配新的内存地址进行赋值。
  • 当调用String类中replace()方法修改字符串中指定的字符或子字符串时,也不会在原本的字符串上进行修改,也是重新分配新的内存地址进行赋值。

String类中的常用方法:

int length():返回字符串的长度
char charAt(int index):返回指定索引处的字符
boolean isEmpty():判断字符串是否为空
String toLowerCase():将字符串中的所有字符转换为小写
String toUpperCase():将字符串中的所有字符转换为大写
String trim():返回字符串的副本,去掉前导空白和尾部空白,中间的空白不会被去掉
boolean equals(Object obj):比较字符串的内容是否相同
boolean equalsIgnoreCase(String anotherString):忽略大小写,比较字符串的内容是否相同
String concat(String str):将指定字符串连接到此字符串的结尾,等价于用“+”
int compareTo(String anotherString):比较两个字符串的大小
String substring(int beginIndex):返回从beginIndex到末尾的子字符串
String substring(int beginIndex, int endIndex):返回从beginIndex到endIndex前一位的子字符串,不包括endIndex

boolean endsWith(String suffix): 判断字符串是否以指定的后缀结束
boolean startsWith(String prefix):判断字符串是否以指定的前缀开始
boolean startsWith(String prefix, int toffset):判断字符串在指定索引开始的子字符串是否以指定前缀开始
boolean contains(CharSequence s):判断当前字符串中是否包含指定的字符串

int indexOf(String str):返回指定子字符串在当前字符串中第一次出现处的索引
int indexOf(String str, int fromIndex):返回从指定的索引后,指定子字符串在当前字符串中第一次出现处的索引
int lastIndexOf(String str):返回指定子字符串在当前字符串中最后一次出现处的索引
int lastIndexOf(String str, int fromIndex):返回从指定的索引后,指定子字符串在当前字符串中最后一次出现处的索引
注:indexOf和lastIndexOf方法如果未查找到指定子字符串时,返回值都为-1。

String replace(char oldChar, char newChar):替换当前字符串中指定的子字符串
String[] split(String regex):根据指定的符号拆分当前字符串,然后返回一个String数组

String类的常考面试题:问以下代码输出结果是true还是false? 

第一个:

public static void main(String[] args) {
    final String s1="Hello"; 
    String s2="HelloWorld";
    String s3=s1+"World";
    System.out.println(s2==s3); 
}

第二个:

public static void main(String[] args) {
            String s1="Hello";
            String s2="HelloWorld";
            String s3=s1+"World";
            System.out.println(s2==s3);
    }

第一个为true,第二个为false;原因如下:

        由于 s1 被声明为 final 是常量,而字符串"world"也是常量,由于Java存在常量优化机制,因此在编译时,等号右边s1 + "World" 不会被当成字符串拼接操作,而是直接被看作一个常量字符串"HelloWorld" 。并且jvm会判断字符串常量池中是否已存在字符串"HelloWorld" ,若存在,便不会在创建新的对象,而是让s3直接指向该"HelloWorld",而 s2 也指向字符串常量池中的 "HelloWorld" ,所以 s2 和 s3 指向的是同一个字符串对象,s2 == s3 结果为 true 。

        对于第二个代码片段;这里的 s1 不是 final ,所以 s1 + "World" 在运行时会被当作字符串拼接操作,会直接创建一个新的字符串对象,用来接收拼接后的字符串"HelloWorld"s3 指向这个新创建的对象,而 s2 指向的是字符串常量池中的 "HelloWorld" ,它们不是同一个对象,所以 s2 == s3 结果为 false 。

下面再出两个类似的,看大家是否掌握了呢

    public static void main(String[] args) {
            String s1="abcd";
            String s2="ab"+"cd";
            System.out.println(s1==s2);
    }

结果为:true

解析:Java常量优化机制,同上题的第二个;"ab"、"cd"都是常量;

    public static void main(String[] args) {
            String s1="abc";
            String s2=new String("abc");
            System.out.println(s1==s2);
    }

结果为:false

解析:因为只要用到new来创建String字符串,一定是在堆区开辟了新的地址空间(字符串拼接操作背后就是new了一个新的字符串)

 

String与char[]之间相互装换

1、char[]转String,利用String类的有参构造方法:String(char[] data)

传入参数为字符数组,返回一个String类型的字符串:
char[] data={'a','b','c'};
String str = new String(data);
System.out.println(str); //"abc"

2、String转char[] ,调用String类的toCharArray()方法

String str="Hello";
char[] data = str.toCharArray();
for (int i = 0; i < data.length; i++) {
    System.out.println(data[i]);
}
/*
H
e
l
l
o
*/

资料来源:深入理解String、StringBuffer和StringBuilder_stringbiluder-CSDN博客 

二、StringBuffer类

  StringBuffer 是一个可变的字符序列,线程安全。这意味着多个线程可以同时访问和操作同一个 StringBuffer 对象,而不会导致数据不一致或错误。

它提供了一些方法,如 append 、insert 、delete 等,用于直接修改字符串的内容,而无需再像String一样创建一个新的对象。

优点:

  • 适用于多线程环境下对字符串进行修改操作。
  • 提供了一系列方法,如 append 、insert 等,用于高效地修改字符串内容。
  • 例如:
    StringBuffer str = new StringBuffer();
    for (int i = 0; i < 1000; i++) {
        str.append(i);
    }

    这种方式在性能上要优于使用 String 的拼接操作。

StringBuffer常用方法:

append("xxx"):在当前字符串后面拼接上新字符串:"xxx"
delete(int start,int end):删除字符串中指定范围的内容,左开右闭
replace(int start, int end, String str):替换指定范围的内容
insert(int n, "xxx"):在下标为n的位置插入指定的内容:xxx
reverse() :把当前字符序列逆转

三、StringBuilder类

   StringBuilder 的功能和 StringBuffer 类似,也是用于操作可变的字符序列。但不同的是,StringBuilder 不是线程安全的。

优点:

  • 在单线程环境下,性能通常比 StringBuffer 更好。
  • 例如:

    StringBuilder str = new StringBuilder();
    for (int i = 0; i < 10000; i++) {
        str.append(i);
    }

    这种单线程的字符串拼接在性能上要比StringBuffer更好一些。

在大多数情况下,如果是在单线程中操作字符串,优先选择 StringBuilder 。如果是多线程环境,需要保证线程安全,则使用 StringBuffer 。

四、补充

1. StringBuffer和StringBuilder背后的具体实现

        StringBuffer、StringBuilder和String类似,底层也是用一个数组来存储字符串的值,并且数组的默认长度为16,即一个空的StringBuffer对象数组长度为16。当实例化一个StringBuffer对象后即创建了一个大小为16个字符的字符串缓冲区。

        但是​当我们调用有参构造函数创建一个StringBuffer对象时,数组长度就不再是默认16了,而是长度为“当前所创建的字符串的长度+16”。所以一个 StringBuffer 或 StringBuilder 字符串创建完成之后,有16个字符的空间可以用于对其值进行修改。如果修改的值范围超出了16个字符,则Jvm会先检查StringBuffer对象的原char数组的容量能不能装下新的字符串,如果装不下则会对 char 数组进行扩容。
        扩容的逻辑就是创建一个新的 char 数组,将现有容量扩大一倍再加上2,如果还是不够大则直接等于需要的容量大小。扩容完成之后,将原数组的内容复制到新数组,最后将指针指向新的 char 数组。

2. String、StringBuffer和StringBuilder的异同

    相同点:都用于处理字符串相关的操作,且底层都是通过char数组实现的

    不同点:

  1. 可变性

    • String 是不可变的,一旦创建,其内容不能被修改;如果要修改,则会重新开辟内存空间创建一个新的对象来存储修改之后的字符串;
    • StringBuffer 和 StringBuilder 是可变的,可以通过相关方法直接修改其内部的字符序列。
  2. 线程安全性

    • StringBuffer 是线程安全的,适合在多线程环境中使用。
    • StringBuilder 是非线程安全的,在单线程环境中性能更好。
  3. 性能

    •  在字符串操作方面,StringBuffer和StringBuilder的性能都要优于String
    • 在单线程环境下,对字符串进行频繁修改操作时,StringBuilder 的性能通常优于 StringBuffer 。

然而,如果在一个多线程环境中,多个线程同时对一个字符串进行修改,拿就应该使用 StringBuffer 

3. String、StringBuffer和StringBuilder的性能对比

        (1)String

    public static void main(String[] args) {
        Date date01 = new Date();
        long t1 = date01.getTime();

        String str ="" ;
        for (int i = 0; i < 100000; i++) {
            str += i;
        }

        Date date02 = new Date();
        long t2 = date02.getTime();
        System.out.println("总耗时为:"+(t2-t1)+"毫秒");
    }

输出  总耗时为:15302毫秒 

仅10万次的拼接操作,String类型的字符串就耗时15302毫秒(15秒左右)

        (2)StringBuffer

    public static void main(String[] args) {
        Date date01 = new Date();
        long t1 = date01.getTime();

        StringBuffer str = new StringBuffer();
        for (int i = 0; i < 10000000; i++) {
            str.append(i);
        }

        Date date02 = new Date();
        long t2 = date02.getTime();
        System.out.println("总耗时为:"+(t2-t1)+"毫秒");
    }

输出  总耗时为:336毫秒 

1000万次的字符串拼接,StringBuffer类型的字符串耗时为336毫秒左右

        (3)StringBuilder

    public static void main(String[] args) {
        Date date01 = new Date();
        long t1 = date01.getTime();

        StringBuilder str = new StringBuilder();
        for (int i = 0; i < 10000000; i++) {
            str.append(i);
        }

        Date date02 = new Date();
        long t2 = date02.getTime();
        System.out.println("总耗时为:"+(t2-t1)+"毫秒");
    }

输出  总耗时为:263毫秒

1000万次的字符串拼接,StringBuilder类型的字符串耗时为263毫秒左右

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值