【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 |
写在最后
🌟以上便是本文的全部内容啦,后续内容将会持续免费更新,如果文章对你有所帮助,麻烦动动小手点个赞 + 关注,非常感谢 ❤️ ❤️ ❤️ !
如果有问题,欢迎私信或者评论区!
共勉:“你间歇性的努力和蒙混过日子,都是对之前努力的清零。”