个人主页:Nezuko627的博客主页
❤️ 支持我: 点赞 收藏 关注
格言:立志做一个有思想的程序员
作者介绍:本人本科软件工程在读,博客主要涉及JavaSE、JavaEE、MySQL、SpringBoot、算法等知识。专栏内容长期更新,如有错误,欢迎评论区或者私信指正!感谢大家的支持~~~
面试官: String 是不可变序列,这个该如何理解?
路人甲: 这个,我只知道它是不可变的,别的还…
面试官: 那么String的不可变是指值不可变还是地址呢?
路人甲: 值…哦,好像是地址…
面试官: 你这基础不牢固啊,来看下代码,告诉我结果都是什么,这个总可以吧?
路人甲: umm,我还是好好学习吧
没错!本期的主题就是将 String 一网打尽!
本篇学习目标:
- ⭐️ 理解三种字符对象在内存的存在形式;
- ⭐️ 熟悉三种字符的创建及常用方法的使用;
- ⭐️ 理解StringBuffer,StringBuilder,String的区别;
- ⭐️ 熟悉StringBuffer的坑点,学会深入源码看待问题。
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 的地址不可修改,但是单个字符的内容是可以变化的。 如下代码很好的验证了这一点:
常用的两种创建 String 对象的方式:
方式1️⃣ :直接赋值
String s1 = "Nezuko";
方式2️⃣ :调用构造器
String s2 = new String("Nezuko");
两种创建方式的区别解析:
方式一 : 先从常量池查看是否有
"Nezuko"
的数据空间,如果有,则直接指向该空间;如果没有,则重新创建,然后指向。s1 最终指向的是常量池的空间地址。
方式二 : 先在堆中创建空间,里面维护了value
属性,指向常量池的"Nezuko"
空间。如果常量池没有,则重新创建,如果有,则直接通过value
指向,s2 最终指向的是堆中的空间地址。
两种方式的内存分布图如下: (图中 s1 对应方式一,s2 对应方式二)
代码验证:
知识回顾:
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。
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]); // 我是祢豆子
}
StringBuffer类的基本介绍:
1. java.lang.StringBuffer 代表 可变的字符序列,可以对字符串内容进行增删;
2. 很多方法与String相同,但是 StringBuffer是可变长度的;
3. StringBuffer 是一个容器;
1️⃣ StringBuffer 的直接父类是 AbstractStringBuilder;
2️⃣ StringBuffer 实现了 Serializable 接口,即 StringBuffer 对象可串行化;
3️⃣ 在父类 AbstractStringBuilder 中,有属性 char[] value,其并没有被 final 修饰,该数组用于存储字符串的内容,且说明字符串存储在堆中;
4️⃣ StringBuffer 是 final类,不能被继承。
StringBuffer 内存布局示意图:(假设 sb 指向 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");
主要有使用构造器与 append 两种方式,详细见代码及注释演示:
String s = "Nezuko";
// 方式一 使用构造器
StringBuffer sb1 = new StringBuffer(s);
// 方式二 使用append
StringBuffer sb2 = new StringBuffer();
sb2.append(s);
主要有两种方式,使用toString 或者使用构造器,详细见代码及注释演示:
StringBuffer sb = new StringBuffer("Nezuko627的博客");
// 方式一 使用StringBuffer提供的toString
String s1 = sb.toString();
// 方式二 使用构造器
String s2 = new String(sb);
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 ,其修改字符串的方法都是在原字符串直接修改,并不需要一个字符串对象来接收,这也是其可变的一种体现形式。
坑点一: 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() 显然会报空指针异常
StringBuilder类的基本介绍:
可变的字符序列。此类提供一个与StringBuffer兼容的API,但不保证同步(不是线程安全的)。该类用作 StringBuffer 的一个简易替换,用在字符串缓冲区被单个线程使用的时候。 在大多数实现中,StringBuilder 比 StringBuffer 快。
1️⃣ StringBuilder 的直接父类是 AbstractStringBuilder;
2️⃣ StringBuilder 实现了 Serializable 接口,即 StringBuffer 对象可串行化;
3️⃣ 在父类 AbstractStringBuilder 中,有属性 char[] value,其并没有被 final 修饰,该数组用于存储字符串的内容,且说明字符串存储在堆中;
4️⃣ StringBuilder是 final类,不能被继承。
哈哈,是不是似曾相识,其实结构与 StringBuffer 一模一样! 在线程上有些区别,但是本节暂时不讨论。
由此,StringBuffer 有的方法,StringBuilder可以直接使用。 (StringBuilder 的方法没有做同步处理,线程不安全)
三大字符串类比较:
类名 | 说明 |
---|---|
String | 不可变字符序列,效率低,但是复用率高 |
StringBuffer | 可变字符序列,效率较高,线程安全 |
StringBuilder | 可变字符序列,效率最高,线程不安全 |
String 为什么不适合用于大量修改:
String s = “a”;
s += “b”;
实际上原来的 “a” 字符串对象已经被丢弃了,现在又产生了一个字符串 “ab”。多次执行这样的操作,会 导致大量的副本字符串对象留在内存中,降低效率。
使用场景和原则:
场景 | 使用 |
---|---|
存在字符串大量修改 | StringBuilder StringBuffer |
单线程情况,存在大量修改 | StringBuilder |
多线程情况,存在大量修改 | StringBuffer |
字符串很少修改,且被多个对象引用,比如配置信息等 | String |
以上便是本文的全部内容啦,后续内容将会持续免费更新,如果文章对你有所帮助,麻烦动动小手点个赞 + 关注,非常感谢 ❤️ ❤️ ❤️ !
如果有问题,欢迎私信或者评论区!
共勉:“你间歇性的努力和蒙混过日子,都是对之前努力的清零。”