> 文档中心 > 【JavaSE】面试高频String、StringBuffer、 StringBuilder坑点总结刨析

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

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

写在前面

👩 面试官: String 是不可变序列,这个该如何理解?
👦 路人甲: 这个,我只知道它是不可变的,别的还…
👩 面试官: 那么String的不可变是指值不可变还是地址呢?
👦 路人甲: 值…哦,好像是地址…
👩 面试官: 你这基础不牢固啊,来看下代码,告诉我结果都是什么,这个总可以吧?
在这里插入图片描述
👦 路人甲: umm,我还是好好学习吧😢

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

本篇学习目标:

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

本文来自专栏:JavaSE系列专题知识及项目 欢迎点击支持订阅专栏 ❤️
在这里插入图片描述

文章目录

  • 写在前面
  • 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 的地址不可修改,但是单个字符的内容是可以变化的。 如下代码很好的验证了这一点:
在这里插入图片描述

1.1 String创建刨析

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

String s1 = "Nezuko";

方式2️⃣ :调用构造器

String s2 = new String("Nezuko");

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

方式一 : 先从常量池查看是否有 "Nezuko" 的数据空间,如果有,则直接指向该空间;如果没有,则重新创建,然后指向。s1 最终指向的是常量池的空间地址。
方式二 : 先在中创建空间,里面维护了 value 属性,指向常量池的 "Nezuko" 空间。如果常量池没有,则重新创建,如果有,则直接通过 value 指向,s2 最终指向的是堆中的空间地址。
两种方式的内存分布图如下: (图中 s1 对应方式一,s2 对应方式二)
在这里插入图片描述
代码验证:
在这里插入图片描述

1.2 深入理解String对象特性

📖 知识回顾:

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

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

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

🍉 解析:

创建了2个字符串对象,先在常量池创建一个 “Hello” ,s1 指向该区域,而后创建 “Nezuko”。 s1由指向 “Hello” 更改为指向 “Nezuko”。其内存布局如下图:
在这里插入图片描述

2️⃣ 题目2:常量相加

String s = "Hello" + "Nezuko";

🍉 解析:

创建了1个字符串对象,原因是编译器做了优化,对创建常量池对象进行判断,是否有引用指向。 题目中的String s = "Hello" + "Nezuko" 等价于 String s = "HelloNezuko"内存布局如下图:
在这里插入图片描述

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”。
内存示意图如下:
在这里插入图片描述

🐍 扩展:看代码,判断是否为同一对象
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));  // falseSystem.out.println(s1.equalsIgnoreCase(s2));  // trueSystem.out.println(s1.length());  // 6System.out.println(s1.indexOf('U'));  // 3System.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());  // NEZUKOSystem.out.println(s.toLowerCase());  // nezukos = s.concat("62").concat("7");System.out.println(s);  // Nezuko627// 将字符串的627 替换成 Nezukos = 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 的关系图:
在这里插入图片描述
📖 说明:

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 对象)
在这里插入图片描述

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[] 大小为字符串长度+16StringBuffer sb3 = new StringBuffer("Nezuko");

2.3.2 String转换StringBuffer

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

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

2.3.3 StringBuffer转换String

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

StringBuffer sb = new StringBuffer("Nezuko627的博客"); // 方式一 使用StringBuffer提供的toStringString 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为例:
在这里插入图片描述
发现,在构造器中先计算了 str 的长度,即使用了 str.length(),但是代码中 str = null,而 null.length() 显然会报空指针异常


3 StringBuilder类

🐯 StringBuilder类的基本介绍:

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

3.1 StringBuilder结构刨析

下图为 StringBuilder 的关系图:
在这里插入图片描述
📖 说明:

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

写在最后

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

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

虫部落快搜