【JavaSE】面试高频String、StringBuffer、 StringBuilder坑点总结刨析

个人主页:Nezuko627的博客主页
❤️ 支持我: 点赞 收藏 关注
格言:立志做一个有思想的程序员
作者介绍:本人本科软件工程在读,博客主要涉及JavaSE、JavaEE、MySQL、SpringBoot、算法等知识。专栏内容长期更新,如有错误,欢迎评论区或者私信指正!感谢大家的支持~~~

写在前面

面试官: String 是不可变序列,这个该如何理解?
路人甲: 这个,我只知道它是不可变的,别的还…
面试官: 那么String的不可变是指值不可变还是地址呢?
路人甲: 值…哦,好像是地址…
面试官: 你这基础不牢固啊,来看下代码,告诉我结果都是什么,这个总可以吧?
【JavaSE】面试高频String、StringBuffer、 StringBuilder坑点总结刨析_第1张图片
路人甲: umm,我还是好好学习吧

没错!本期的主题就是将 String 一网打尽!

本篇学习目标:

  • ⭐️ 理解三种字符对象在内存的存在形式;
  • ⭐️ 熟悉三种字符的创建及常用方法的使用;
  • ⭐️ 理解StringBuffer,StringBuilder,String的区别;
  • ⭐️ 熟悉StringBuffer的坑点,学会深入源码看待问题。

本文来自专栏:JavaSE系列专题知识及项目 欢迎点击支持订阅专栏 ❤️
【JavaSE】面试高频String、StringBuffer、 StringBuilder坑点总结刨析_第2张图片

文章目录

  • 写在前面
  • 1.String类
    • 1.1 String创建刨析
    • 1.2 深入理解String对象特性
    • 1.3 String类常见方法
  • 2 StringBuffer类
    • 2.1 StringBuffer结构刨析
    • 2.2 String与StringBuffer对比
    • 2.3 StringBuffer的创建与转换
      • 2.3.1 StringBuffer的创建
      • 2.3.2 String转换StringBuffer
      • 2.3.3 StringBuffer转换String
    • 2.4 StringBuffer常用方法
    • 2.5 StringBuffer坑点练习
  • 3 StringBuilder类
    • 3.1 StringBuilder结构刨析
  • 4 总结
  • 写在最后


1.String类

String类的基本介绍:

1. String 对象用于保存字符串,即一组字符序列。字符串常量对象是用双引号括起来的字符序列。例:“123”,"girl"等;
2. 字符串的字符使用 Unicode 字符编码,一个字符(不区分字母还是汉字)占两个字节;
3. String 类实现了接口 Serializable,用处:String 可以串行化,可以在网络传输;
4. String 是 final 类,不能被其他类继承;
5. String 类中有属性 private final char value[] ,用于存放字符串内容。

String类的常用构造方法:

String s1 = new String();
String s2 = new String(String original);
String s3 = new String(char[] a);
String s4 = new String(char[] a, int startIndex, int count);
String s5 = new String(byte[] b);

❓❓❓ 如何理解 String 是不可修改的?

答: String 类中有属性 private final char value[] ,用于存放字符串内容,所以说 String 底层是字符串数组。而 value 是一个 final 类型,因此,不可以修改。但是这个不可修改,指的是 value 的地址不可修改,但是单个字符的内容是可以变化的。 如下代码很好的验证了这一点:
【JavaSE】面试高频String、StringBuffer、 StringBuilder坑点总结刨析_第3张图片

1.1 String创建刨析

常用的两种创建 String 对象的方式:
方式1️⃣ :直接赋值

String s1 = "Nezuko";

方式2️⃣ :调用构造器

String s2 = new String("Nezuko");

两种创建方式的区别解析:

方式一 : 先从常量池查看是否有 "Nezuko" 的数据空间,如果有,则直接指向该空间;如果没有,则重新创建,然后指向。s1 最终指向的是常量池的空间地址。
方式二 : 先在中创建空间,里面维护了 value 属性,指向常量池的 "Nezuko" 空间。如果常量池没有,则重新创建,如果有,则直接通过 value 指向,s2 最终指向的是堆中的空间地址。
两种方式的内存分布图如下: (图中 s1 对应方式一,s2 对应方式二)
【JavaSE】面试高频String、StringBuffer、 StringBuilder坑点总结刨析_第4张图片
代码验证:
【JavaSE】面试高频String、StringBuffer、 StringBuilder坑点总结刨析_第5张图片

1.2 深入理解String对象特性

知识回顾:

1. String 是一个 final 类,代表不可变的字符序列;
2. 字符串是不可变的,一个字符串的内存一旦被分配,其内容是不可变的。

题目综合练习,帮助深入理解 String 对象特性,阅读以下几段代码,分析创建对象的数目,并绘制内存布局图:
1️⃣ 题目1:引用改变

String s1 = "Hello";
s1 = "Nezuko";

解析:

创建了2个字符串对象,先在常量池创建一个 “Hello” ,s1 指向该区域,而后创建 “Nezuko”。 s1由指向 “Hello” 更改为指向 “Nezuko”。其内存布局如下图:
【JavaSE】面试高频String、StringBuffer、 StringBuilder坑点总结刨析_第6张图片

2️⃣ 题目2:常量相加

String s = "Hello" + "Nezuko";

解析:

创建了1个字符串对象,原因是编译器做了优化,对创建常量池对象进行判断,是否有引用指向。 题目中的String s = "Hello" + "Nezuko" 等价于 String s = "HelloNezuko"内存布局如下图:
【JavaSE】面试高频String、StringBuffer、 StringBuilder坑点总结刨析_第7张图片

3️⃣ 题目3:s1 + s2

String s1 = "Hello";
String s2 = "Nezuko";
String s3 = s1 + s2;

解析:

创建了3个对象,但是 s3 指向的是堆区的对象。这里我们需要重点分析,s3 = s1 + s2发生了什么,通过分析 String 源码我们可以得到,在该语句进行了如下操作:
(1)先创建一个 StringBuilder sb = new StringBuilder();
(2)执行 sb.append(“Hello”);
(3)执行 sb.append(“Nezuko”);
(4)调用 sb.toString() 返回一个字符串对象,
即 s3 指向堆中的对象,而堆中的对象的 value 属性指向常量池的 “HelloNezuko”。
内存示意图如下:
【JavaSE】面试高频String、StringBuffer、 StringBuilder坑点总结刨析_第8张图片

扩展:看代码,判断是否为同一对象
tips: 答案见注释

String s1 = "Hello";
String s2 = "Nezuko";
String s3 = s1 + s2;
String s4 = "HelloNezuko";
System.out.print(s3 == s4);  // false

s3 指向堆区的对象, 由堆区的对象中的 value 指向常量池中的 “HelloNezuko”,而 s4 直接指向常量池中的 “HelloNezuko”, 故两对象不同,所以为 false。

1.3 String类常见方法

String常见方法一览表(一):

方法名 作用
equals() 区分大小写,判断内容是否相等
equalslgnoreCase() 忽略大小写,判断内容是否相等
length() 获取字符个数,即字符串长度
indexOf() 获取字符(或者是子字符串)在字符串中第1次出现的索引,索引从0开始,找不到就返回-1
lastIndexOf() 获取字符(或者是子字符串)在字符串最后1次出现的索引,索引从0开始,找不到就返回-1
subString() 截取指定范围的子串
trim() 去除字符串的前后空格
charAt() 获取某索引处的字符

代码示例:(注释为答案)

String s1 = "NEZUKO";
String s2 = "nezuko";
System.out.println(s1.equals(s2));  // false
System.out.println(s1.equalsIgnoreCase(s2));  // true
System.out.println(s1.length());  // 6
System.out.println(s1.indexOf('U'));  // 3
System.out.println(s1.lastIndexOf('n'));  // -1
// 从索引1截取,截取完毕
System.out.println(s1.substring(1));  // EZUKO
// 从索引1开始,截取到索引3之前,即[1, 3) 左闭右开
System.out.println(s1.substring(1, 3));  // EZU

String常见方法一览表(二):

方法名 作用
toUpperCase() 转化成大写
toLowerCase() 转化成小写
concat() 拼接字符串
replace() 返回替换字符串的字符形成的新字符串
split() 以参数为标准分割字符串,返回字符串数组
toCharArray() 将字符串转成字符数组

代码示例:(注释为答案)

String s = "Nezuko";
System.out.println(s.toUpperCase());  // NEZUKO
System.out.println(s.toLowerCase());  // nezuko
s = s.concat("62").concat("7");
System.out.println(s);  // Nezuko627

// 将字符串的627 替换成 Nezuko
s = s.replace("627", "Nezuko");
System.out.println(s);  // Nezuko

// 以,分隔字符串
String message = "我,是,祢豆子";
String[] newMessage = message.split(",");
for (int i = 0; i < newMessage.length; i++) {
    System.out.print(newMessage[i]);  // 我是祢豆子
}

2 StringBuffer类

StringBuffer类的基本介绍:

1. java.lang.StringBuffer 代表 可变的字符序列,可以对字符串内容进行增删;
2. 很多方法与String相同,但是 StringBuffer是可变长度的;
3. StringBuffer 是一个容器;

2.1 StringBuffer结构刨析

下图为 StringBuffer 的关系图:
【JavaSE】面试高频String、StringBuffer、 StringBuilder坑点总结刨析_第9张图片
说明:

1️⃣ StringBuffer 的直接父类是 AbstractStringBuilder;
2️⃣ StringBuffer 实现了 Serializable 接口,即 StringBuffer 对象可串行化;
3️⃣ 在父类 AbstractStringBuilder 中,有属性 char[] value,其并没有被 final 修饰,该数组用于存储字符串的内容,且说明字符串存储在堆中;
4️⃣ StringBuffer 是 final类,不能被继承。

2.2 String与StringBuffer对比

  • String 保存的是字符串常量,里面的值不能更改,每次String类的更新实际上是更改地址,效率较低;
  • StringBuffer 保存的是字符串变量,里面的值可以修改,每次更新实际上可以更新内容, 不用每次更新地址(只有当容量不够时才更新地址),效率较高。

StringBuffer 内存布局示意图:(假设 sb 指向 StringBuffer 对象)
【JavaSE】面试高频String、StringBuffer、 StringBuilder坑点总结刨析_第10张图片

2.3 StringBuffer的创建与转换

2.3.1 StringBuffer的创建

StringBuffer常用构造器一览表:

构造方法 解释
StringBuffer() 构造一个其中不带字符的字符串缓冲区,初始容量为16字符
StringBuffer(CharSequence seq) 构造一个字符串缓冲区,它包含与指定的CharSequence相同的字符
StringBuffer(int capacity) 创建一个不带字符,但具有指定初始容量的字符串缓冲区,即对 char[] 大小进行指定
StringBuffer(String str) 构造一个字符串缓冲区,并将其内容初始化为指定的字符串内容(容量初始大小为字符串长度+16)

代码示例:

// 创建一个初始大小为16的char[] 用于存放字符内容
StringBuffer sb1 = new StringBuffer();
// 通过构造器指定 char[] 大小
StringBuffer sb2 = new StringBuffer(100);
// 通过 String 创建, char[] 大小为字符串长度+16
StringBuffer sb3 = new StringBuffer("Nezuko");

2.3.2 String转换StringBuffer

主要有使用构造器与 append 两种方式,详细见代码及注释演示:

String s = "Nezuko";
// 方式一 使用构造器
StringBuffer sb1 = new StringBuffer(s);
        
// 方式二 使用append
StringBuffer sb2 = new StringBuffer();
sb2.append(s);

2.3.3 StringBuffer转换String

主要有两种方式,使用toString 或者使用构造器,详细见代码及注释演示:

StringBuffer sb = new StringBuffer("Nezuko627的博客");
        
// 方式一 使用StringBuffer提供的toString
String s1 = sb.toString();
        
// 方式二 使用构造器
String s2 = new String(sb);

2.4 StringBuffer常用方法

1️⃣ append(): 用于拼接字符串

StringBuffer sb = new StringBuffer();
sb.append("Nezuko").append(627);  // 627在append中会转化成String再拼接
System.out.println(sb);  // Nezuko627

2️⃣ replace(): 用于修改字符串

StringBuffer sb = new StringBuffer("Nezuko627");
// 将索引[0,6)的字符替换
sb.replace(0, 6, "黄小黄");
System.out.println(sb);  // 黄小黄627

3️⃣ insert(): 在指定位置前插入字符串

StringBuffer sb = new StringBuffer("Nezuko627");
// 在索引6前插入
sb.insert(6, "blog");
System.out.println(sb);  //Nezukoblog627

相比String ,其修改字符串的方法都是在原字符串直接修改,并不需要一个字符串对象来接收,这也是其可变的一种体现形式。

2.5 StringBuffer坑点练习

坑点一: append() 参数为 null。

public class Main {
    public static void main(String[] args) {
        String s = null;
        StringBuffer sb = new StringBuffer();
        sb.append(s);
        System.out.println(sb.length());  // 4
    }
}

解析: 这里我们需要了解 append 的源码,通过 debug ,发现在底层实际上调用的是 AbstractStringBuilder 的 appendNull 方法,该方法将 null 转化成了字符串后赋值给了 sb,因此 sb 的长度为4。
输出结果: 4

坑点二:构造器参数为null。

public class Main {
    public static void main(String[] args) {
        String s = null;
        StringBuffer sb = new StringBuffer(s);
        System.out.println(sb);
    }
}

结果: 抛出空指针异常
在这里插入图片描述
解析: 首先我们先找到构造器的源码,以Jdk15为例:
【JavaSE】面试高频String、StringBuffer、 StringBuilder坑点总结刨析_第11张图片
发现,在构造器中先计算了 str 的长度,即使用了 str.length(),但是代码中 str = null,而 null.length() 显然会报空指针异常


3 StringBuilder类

StringBuilder类的基本介绍:

可变的字符序列。此类提供一个与StringBuffer兼容的API,但不保证同步(不是线程安全的)。该类用作 StringBuffer 的一个简易替换,用在字符串缓冲区被单个线程使用的时候。 在大多数实现中,StringBuilder 比 StringBuffer 快。

3.1 StringBuilder结构刨析

下图为 StringBuilder 的关系图:
【JavaSE】面试高频String、StringBuffer、 StringBuilder坑点总结刨析_第12张图片
说明:

1️⃣ StringBuilder 的直接父类是 AbstractStringBuilder;
2️⃣ StringBuilder 实现了 Serializable 接口,即 StringBuffer 对象可串行化;
3️⃣ 在父类 AbstractStringBuilder 中,有属性 char[] value,其并没有被 final 修饰,该数组用于存储字符串的内容,且说明字符串存储在堆中;
4️⃣ StringBuilder是 final类,不能被继承。

哈哈,是不是似曾相识,其实结构与 StringBuffer 一模一样! 在线程上有些区别,但是本节暂时不讨论。
由此,StringBuffer 有的方法,StringBuilder可以直接使用。 (StringBuilder 的方法没有做同步处理,线程不安全)


4 总结

三大字符串类比较:

类名 说明
String 不可变字符序列,效率低,但是复用率高
StringBuffer 可变字符序列,效率较高,线程安全
StringBuilder 可变字符序列,效率最高,线程不安全

String 为什么不适合用于大量修改:

String s = “a”;
s += “b”;
实际上原来的 “a” 字符串对象已经被丢弃了,现在又产生了一个字符串 “ab”。多次执行这样的操作,会 导致大量的副本字符串对象留在内存中,降低效率。

使用场景和原则:

场景 使用
存在字符串大量修改 StringBuilder StringBuffer
单线程情况,存在大量修改 StringBuilder
多线程情况,存在大量修改 StringBuffer
字符串很少修改,且被多个对象引用,比如配置信息等 String

写在最后

以上便是本文的全部内容啦,后续内容将会持续免费更新,如果文章对你有所帮助,麻烦动动小手点个赞 + 关注,非常感谢 ❤️ ❤️ ❤️ !
如果有问题,欢迎私信或者评论区!
在这里插入图片描述

共勉:“你间歇性的努力和蒙混过日子,都是对之前努力的清零。”
在这里插入图片描述

你可能感兴趣的:(JavaSE,面试,java,职场和发展,开发语言)