【从零开始java学习|第十三篇】字符串究极知识总结
目录
一、什么是字符串
二、String 概述:不可变的文本载体
2.1 本质与核心特性
2.2 不可变性的原因
2.3 不可变性的好处
2.4 JDK 版本差异:底层存储优化(JDK 8 vs JDK 9+)
三、String 的构造方法与内存分析
3.1 常用构造方法
(1)无参构造:new String()
(2)字面量构造:new String(String original)
编辑
(3)字符数组构造:new String(char[] value)
(4)字节数组构造:new String(byte[] bytes)
(5)StringBuilder 构造:new String(StringBuilder sb)
3.2 两种创建方式的内存对比(关键考点)
四、字符串的比较:地址 vs 内容
4.1 == 运算符:比较对象地址
4.2 equals()方法:比较字符串内容
4.3 intern()方法:获取常量池引用
4.4 常见比较场景与易错点
(1)字符串拼接的地址比较
(2)空字符串与null的比较
五、StringBuilder:可变字符串的解决方案
5.1 核心特性
5.2 基本操作(代码示例)
(1)构造方法
(2)常用方法
5.3 底层原理:扩容机制
5.4 效率对比:String vs StringBuilder
六、StringJoiner:简化分隔符拼接
6.1 核心特性
6.2 基本操作(代码示例)
(1)构造方法
(2)常用方法
6.3 底层实现
七、字符串相关类的底层原理深度解析
7.1 String 底层:不可变的本质
7.2 StringBuilder 底层:可变的核心
7.3 StringJoiner 底层:封装的便捷性
八、总结与实践建议
8.1 三类字符串类的适用场景
8.2 性能优化建议
一、什么是字符串
字符串是 Java 开发中最常用的数据类型之一,用于表示文本信息。Java 提供了三类核心字符串处理类:String
(不可变字符串)、StringBuilder
(可变字符串,单线程)、StringJoiner
(简化分隔符拼接,Java 8+)。本文将从String
概述出发,逐步深入构造方法、比较逻辑、可变字符串操作,最终解析底层原理,帮助开发者全面掌握字符串知识。
二、String 概述:不可变的文本载体
2.1 本质与核心特性
String
是引用数据类型,但不属于 Java 基本类型,其核心特性是不可变性—— 字符串对象创建后,其内容(字符序列)无法被修改。
String s = "abc";
s += "d"; // 看似修改,实际是创建新对象"abcd",原"abc"仍存在
System.out.println(s); // 输出"abcd",但原"abc"未被修改
2.2 不可变性的原因
String
的不可变性由底层存储和关键字共同保证(以 JDK 8 为例):
public final class String {// 存储字符的数组,private + final:数组引用不可变,外部无法直接修改数组private final char[] value; // 哈希值缓存(不可变故只需计算一次)private int hash; // 无其他修改value数组的方法(如setCharAt())
}
final class
:String
不能被继承,避免子类破坏不可变性;private final char[] value
:value
数组引用不可变(不能指向新数组),且private
修饰让外部无法直接操作数组;- 无修改方法:
String
未提供修改数组元素的方法(如value[i] = 'x'
),所有 “修改” 操作(如substring
、concat
)都会返回新String
对象。
2.3 不可变性的好处
- 线程安全:不可变对象天生线程安全,无需同步;
- 常量池复用:相同字面量的
String
可复用常量池中的对象,节省内存; - 哈希值缓存:
hash
值仅计算一次,提升HashMap
等集合的效率。
2.4 JDK 版本差异:底层存储优化(JDK 8 vs JDK 9+)
- JDK 8 及之前:用
char[] value
存储字符(每个char
占 2 字节,UTF-16 编码); - JDK 9 及之后:用
byte[] value
+byte coder
存储(coder
标识编码:0=ISO-8859-1,1=UTF-16)。
优化原因:大部分字符串是拉丁字符(如英文字母),用byte[]
(1 字节 / 字符)可节省 50% 内存。
三、String 的构造方法与内存分析
String
提供多种构造方法,不同方法的内存分配逻辑不同,直接影响对象复用和内存占用。
3.1 常用构造方法
(1)无参构造:new String()
创建空字符串对象,底层复用常量池中的空byte/char
数组。
String s1 = new String();
// 内存逻辑:
// 1. 常量池检查是否有"空字符串"(长度为0的数组),无则创建;
// 2. 堆中创建String对象,s1的value指向常量池的空数组;
// 3. s1 != ""(堆对象vs常量池引用),但s1.equals("")为true。
(2)字面量构造:new String(String original)
根据已有字符串创建新对象,优先复用常量池中的字面量。
String s2 = new String("abc");
// 内存逻辑(JDK 8):
// 1. 常量池检查是否有"abc"的char数组,无则创建;
// 2. 堆中创建新String对象,s2的value指向常量池的"abc"数组;
// 3. 此时:"abc"(常量池引用)和s2(堆对象)地址不同,但内容相同。
(3)字符数组构造:new String(char[] value)
将字符数组转为String
,复制数组而非直接引用,保证不可变性。
char[] arr = {'a', 'b', 'c'};
String s3 = new String(arr);
arr[0] = 'd'; // 修改原数组
System.out.println(s3); // 输出"abc"(s3的value是复制后的数组,不受原数组影响)
// 内存逻辑:
// 1. 堆中创建新char数组(复制arr的内容);
// 2. 堆中创建String对象,s3的value指向新数组;
// 3. 原arr修改不影响s3,体现不可变性。
(4)字节数组构造:new String(byte[] bytes)
将字节数组按默认编码(如 UTF-8)转为String
,同样复制字节数组。
byte[] bytes = "abc".getBytes(); // 按UTF-8编码转为字节数组
String s4 = new String(bytes);
System.out.println(s4); // 输出"abc"
(5)StringBuilder 构造:new String(StringBuilder sb)
将StringBuilder
的内容转为String
,底层复制StringBuilder
的字符数组。
StringBuilder sb = new StringBuilder("abc");
String s5 = new String(sb);
// 内存逻辑:复制sb的char[] value,s5的value指向新数组,sb后续修改不影响s5。
3.2 两种创建方式的内存对比(关键考点)
创建方式 | 内存位置 | 是否复用常量池 | 地址比较结果(示例) |
---|---|---|---|
String s = "abc" | 常量池(引用指向常量池) | 是(优先复用) | s == "abc" → true |
String s = new String("abc") | 堆(对象在堆,value 指向常量池) | 是(常量池复用) | s == "abc" → false(堆 vs 常量池) |
示例验证:
String s6 = "abc"; // 常量池引用
String s7 = new String("abc"); // 堆对象
System.out.println(s6 == s7); // false(地址不同)
System.out.println(s6.equals(s7)); // true(内容相同)
四、字符串的比较:地址 vs 内容
字符串比较是高频考点,需区分==
(地址比较)和equals()
(内容比较),以及intern()
方法的作用。
4.1 ==
运算符:比较对象地址
- 若比较的是
String
引用,判断两者是否指向同一对象; - 若比较的是
String
与基本类型(如"123" == 123
),会触发类型转换,通常返回 false。
示例:
String s8 = "abc";
String s9 = "abc"; // 复用常量池,与s8指向同一对象
String s10 = new String("abc"); // 堆对象,地址不同System.out.println(s8 == s9); // true(同一常量池对象)
System.out.println(s8 == s10); // false(堆vs常量池)
4.2 equals()
方法:比较字符串内容
String
重写了Object
的equals()
方法,核心逻辑是逐字符比较内容,步骤如下:
// String的equals()源码(JDK 8)
public boolean equals(Object anObject) {1. 先判断地址是否相同,相同则直接返回true;if (this == anObject) {return true;}2. 判断参数是否为String类型,不是则返回false;if (anObject instanceof String) {String anotherString = (String)anObject;3. 比较字符数组长度,不同则返回false;int n = value.length;if (n == anotherString.value.length) {4. 逐字符比较,有不同则返回false;char v1[] = value;char v2[] = anotherString.value;int i = 0;while (n-- != 0) {if (v1[i] != v2[i])return false;i++;}5. 所有字符相同,返回true;return true;}}return false;
}
示例:
String s11 = new String("abc");
String s12 = new String("abc");
System.out.println(s11 == s12); // false(堆中两个不同对象)
System.out.println(s11.equals(s12)); // true(内容相同)
4.3 intern()
方法:获取常量池引用
intern()
是String
的 native 方法,作用是:若常量池中存在当前字符串的字面量,则返回常量池引用;若不存在,则将当前字符串加入常量池并返回引用。
示例:
String s13 = new String("abc");
String s14 = s13.intern(); // 返回常量池"abc"的引用
System.out.println(s14 == "abc"); // true(s14指向常量池)
System.out.println(s13 == s14); // false(s13在堆,s14在常量池)
4.4 常见比较场景与易错点
(1)字符串拼接的地址比较
- 编译期优化:字面量拼接(如
"a"+"b"
)会被编译器优化为"ab"
,复用常量池; - 运行期拼接:变量拼接(如
a+"b"
)会创建StringBuilder
,最终返回堆对象,不复用常量池。
示例:
String s15 = "ab";
String s16 = "a" + "b"; // 编译期优化为"ab",指向常量池
String a = "a";
String s17 = a + "b"; // 运行期拼接,返回堆对象System.out.println(s15 == s16); // true(编译期优化)
System.out.println(s15 == s17); // false(堆vs常量池)
(2)空字符串与null
的比较
""
:空字符串对象(有地址,内容为空);null
:无对象(无地址,不指向任何内存)。
示例:
String s18 = "";
String s19 = null;
System.out.println(s18.equals(s19)); // false(内容比较,null不触发NPE)
System.out.println(s19.equals(s18)); // 抛出NullPointerException(null调用方法)
五、StringBuilder:可变字符串的解决方案
String
的不可变性导致频繁拼接时创建大量临时对象,效率极低。StringBuilder
通过可变字符数组解决此问题,适用于单线程下的字符串修改场景。
5.1 核心特性
- 可变:底层用非
final
的char[] value
存储字符,支持直接修改数组; - 单线程安全:无同步锁(多线程需用
StringBuffer
,但效率低); - 高效:修改时无需创建新对象,仅在数组容量不足时扩容。
5.2 基本操作(代码示例)
(1)构造方法
// 1. 默认构造:初始容量16
StringBuilder sb1 = new StringBuilder();
// 2. 指定初始容量:避免频繁扩容
StringBuilder sb2 = new StringBuilder(32);
// 3. 用字符串初始化:容量=字符串长度+16
StringBuilder sb3 = new StringBuilder("abc");
(2)常用方法
方法 | 功能 | 示例 | 结果 |
---|---|---|---|
append(任意类型) | 追加内容到末尾 | sb3.append("d").append(123) | "abcd123" |
insert(int idx, 内容) | 在指定索引插入内容 | sb3.insert(2, "xy") | "abxycd123" |
delete(int start, int end) | 删除 [start, end) 的字符 | sb3.delete(2,4) | "abcd123" |
reverse() | 反转字符串 | sb3.reverse() | "321dcba" |
toString() | 转为String (复制数组) | sb3.toString() | "321dcba" |
示例:
StringBuilder sb = new StringBuilder("Java");
sb.append(" ").append("is").append(" ").append("easy"); // 追加
sb.insert(4, "SE"); // 插入
System.out.println(sb); // 输出"JavaSE is easy"
sb.reverse();
System.out.println(sb); // 输出"ysae si ESavaJ"
5.3 底层原理:扩容机制
StringBuilder
继承自AbstractStringBuilder
,底层逻辑由父类实现:
- 初始容量:默认 16,或
字符串长度+16
(初始化时); - 扩容触发:当追加内容后总长度超过当前容量时;
- 扩容规则:
- 计算新容量:
newCapacity = 原容量 * 2 + 2
; - 若新容量仍不足,则用 “所需最小容量” 作为新容量;
- 用
Arrays.copyOf()
复制原数组到新数组。
- 计算新容量:
示例:初始容量 16 的StringBuilder
,追加 20 个字符时:
- 原容量 16 < 20 → 扩容为
16*2+2=34
→ 复制数组到新容量 34 的数组。
5.4 效率对比:String vs StringBuilder
// 1. String拼接(效率低)
long start1 = System.currentTimeMillis();
String s = "";
for (int i = 0; i < 10000; i++) {s += i; // 每次创建新对象
}
long end1 = System.currentTimeMillis();
System.out.println("String耗时:" + (end1 - start1) + "ms"); // 约1000ms(视环境)// 2. StringBuilder拼接(效率高)
long start2 = System.currentTimeMillis();
StringBuilder sb = new StringBuilder();
for (int i = 0; i < 10000; i++) {sb.append(i); // 仅修改数组
}
String result = sb.toString();
long end2 = System.currentTimeMillis();
System.out.println("StringBuilder耗时:" + (end2 - start2) + "ms"); // 约1ms
六、StringJoiner:简化分隔符拼接
Java 8 新增StringJoiner
,专门解决 “多元素拼接 + 分隔符 + 前后缀” 场景(如拼接列表为[a, b, c]
),避免手动处理最后一个分隔符的问题。
6.1 核心特性
- 简化分隔符:无需判断 “是否为最后一个元素”;
- 支持前后缀:统一添加前缀(如
[
)和后缀(如]
); - 底层依赖 StringBuilder:效率与
StringBuilder
一致。
6.2 基本操作(代码示例)
(1)构造方法
// 1. 仅指定分隔符:StringJoiner(CharSequence delimiter)
StringJoiner sj1 = new StringJoiner(",");
// 2. 指定分隔符、前缀、后缀:StringJoiner(CharSequence delimiter, CharSequence prefix, CharSequence suffix)
StringJoiner sj2 = new StringJoiner(",", "[", "]");
(2)常用方法
方法 | 功能 | 示例 | 结果 |
---|---|---|---|
add(CharSequence) | 添加元素 | sj1.add("a").add("b") | "a,b" |
merge(StringJoiner) | 合并另一个 StringJoiner | sj1.merge(sj2.add("c")) | "a,b,[c]" |
toString() | 转为 String | sj2.toString() | "[a,b,c]" |
示例:拼接列表元素
List<String> fruits = Arrays.asList("apple", "banana", "orange");
StringJoiner sj = new StringJoiner(" | ", "Fruits: [", "]");
for (String fruit : fruits) {sj.add(fruit);
}
System.out.println(sj); // 输出"Fruits: [apple | banana | orange]"
6.3 底层实现
StringJoiner
内部持有StringBuilder
对象,核心逻辑:
add()
:第一次添加元素时先 append 前缀,之后每次添加前 append 分隔符,再 append 元素;merge()
:将另一个StringJoiner
的元素(不含其前缀后缀)添加到当前StringJoiner
;toString()
:最后 append 后缀,返回StringBuilder
的toString()
结果。
七、字符串相关类的底层原理深度解析
7.1 String 底层:不可变的本质
- 存储结构:JDK 8 用
private final char[] value
,JDK 9 + 用private final byte[] value + private final byte coder
(编码标识); - 常量池机制:JDK 7 前常量池在方法区(永久代),JDK 7 及之后移到堆内存,JDK 8 后方法区变为元空间,常量池仍在堆中;
- 不可变保障:
final
修饰数组引用 +private
访问权限 + 无修改方法,确保内容无法被外部修改。
7.2 StringBuilder 底层:可变的核心
- 继承关系:
StringBuilder extends AbstractStringBuilder
,复用父类的char[] value
(非final
); - 扩容逻辑:由
AbstractStringBuilder
的ensureCapacityInternal()
方法实现,核心是 “2 倍 + 2” 扩容,避免频繁数组复制; - 线程不安全:无
synchronized
修饰,多线程下并发修改可能导致数组越界或数据错乱(需用StringBuffer
,但效率低)。
7.3 StringJoiner 底层:封装的便捷性
- 核心成员:
private final String prefix
(前缀)、private final String delimiter
(分隔符)、private final String suffix
(后缀)、private StringBuilder value
(底层拼接容器); - add () 逻辑:
public StringJoiner add(CharSequence newElement) {prepareBuilder().append(newElement); // prepareBuilder()处理前缀和分隔符return this; } private StringBuilder prepareBuilder() {if (value == null) {value = new StringBuilder().append(prefix); // 第一次添加:先加前缀} else {value.append(delimiter); // 非第一次:先加分隔符}return value; }
八、总结与实践建议
8.1 三类字符串类的适用场景
类名 | 不可变性 | 线程安全 | 适用场景 |
---|---|---|---|
String | 是 | 是 | 字符串无需修改(如常量、配置项、固定文本) |
StringBuilder | 否 | 否 | 单线程下频繁修改(如循环拼接、动态生成) |
StringJoiner | 否 | 否 | 多元素拼接需分隔符 / 前后缀(如列表、CSV) |
8.2 性能优化建议
- 避免频繁 String 拼接:循环中用
StringBuilder
,而非String s += "x"
; - 指定 StringBuilder 初始容量:根据预期长度设置容量(如
new StringBuilder(1024)
),减少扩容次数; - 优先用 StringJoiner 处理分隔符:比手动用
StringBuilder
拼接更简洁,不易出错; - 复用 String 常量:用
String s = "abc"
而非new String("abc")
,利用常量池复用内存。
通过掌握String
的不可变性、StringBuilder
的可变逻辑、StringJoiner
的便捷性,以及三者的底层原理,开发者可在实际开发中选择合适的工具,兼顾代码简洁性和性能。
如果我的内容对你有帮助,请点赞,评论,收藏。接下来我将继续更新相关内容!