主题
Java String与StringBuilder核心指南
一、核心知识点
1. String 类概述
1.1 String 的特点
三大核心特性:
字符串字面值都是 String 对象
- Java 程序中的所有字符串字面值(如
"abc")都作为 String 类的实例实现 - 凡是带双引号的都是 String 的对象
- Java 程序中的所有字符串字面值(如
不可变性
- 字符串是常量,它们的值在创建之后不能更改
- String 对象是不可变的,所以可以共享
字符串共享
- 由于 String 对象不可变,所以可以共享
- 相同内容的字符串字面值会指向同一个对象
示例:
java
String s1 = "abc";
String s2 = "abc";
// s1 和 s2 指向同一个对象
String s3 = "hello";
s3 += "world";
// 实际上创建了新的对象,原对象不变

1.2 String 的底层实现原理
核心原理:
- String 底层是一个被
final修饰的数组 - 数组一旦被
final修饰,地址值直接锁死,不能改变 - 这就是 String 不可变的根本原因
JDK 版本差异:
| JDK 版本 | 底层数组类型 | 内存占用 |
|---|---|---|
| JDK 8 及之前 | private final char[] value | 每个 char 占 2 字节 |
| JDK 9 及之后 | private final byte[] value | 每个 byte 占 1 字节 |
优化说明: JDK 9 之后将 char[] 改为 byte[],节省了内存空间。
2. String 的创建方式
2.1 构造方法
| 构造方法 | 说明 | 示例 |
|---|---|---|
String() | 创建空字符串对象 | String s = new String(); |
String(String str) | 根据字符串创建对象 | String s = new String("abc"); |
String(byte[] bytes) | 根据 byte 数组创建对象 | String s = new String(bytes); |
String(char[] chars) | 根据 char 数组创建对象 | String s = new String(chars); |
| 直接赋值 | 最常用的方式 | String s = "abc"; |
2.2 扩展构造方法
java
// 将 byte 数组的一部分转成 String
String(byte[] bytes, int offset, int count)
// bytes: 数组
// offset: 从哪个索引开始转
// count: 转多少个
// 将 char 数组的一部分转成 String
String(char[] chars, int offset, int length)示例:
java
byte[] bytes = {97, 98, 99, 100, 101, 102};
String s1 = new String(bytes, 0, 3); // "abc"
char[] chars = {'a', 'b', 'c', 'd', 'e', 'f'};
String s2 = new String(chars, 0, 3); // "abc"3. String 的常用方法
3.1 判断方法
| 方法 | 说明 |
|---|---|
boolean equals(Object obj) | 判断字符串内容是否一样 |
boolean equalsIgnoreCase(String str) | 判断字符串内容是否一样,忽略大小写 |
重要提示: 推荐使用 "abc".equals(s) 而不是 s.equals("abc"),避免空指针异常。
3.2 获取方法
| 方法 | 说明 |
|---|---|
String concat(String str) | 字符串拼接,返回新的字符串 |
char charAt(int index) | 获取指定索引位置上的字符 |
int indexOf(String str) | 获取指定字符在字符串中第一次出现的索引位置 |
String substring(int beginIndex) | 从指定索引开始截取字符串到最后 |
String substring(int beginIndex, int endIndex) | 从指定索引位置开始截取到 endIndex(含头不含尾) |
int length() | 获取字符串长度 |
3.3 转换方法
| 方法 | 说明 |
|---|---|
char[] toCharArray() | 将字符串转成 char 数组 |
byte[] getBytes() | 将字符串转成 byte 数组 |
byte[] getBytes(String charsetName) | 根据指定的编码集将字符串转成 byte 数组 |
String replace(CharSequence target, CharSequence replacement) | 将前面参数替换成后面的参数 |
编码说明:
| 编码集 | 说明 | 汉字占用 |
|---|---|---|
| ASCII | 主要针对英文 | - |
| GBK | 中文字符集(国标码) | 2 字节 |
| UTF-8 | 万国码 | 3 字节 |
3.4 分割方法
java
String[] split(String regex) // 按照指定的规则分割字符串注意: 如果分割符是 .,需要使用 \\. 进行转义。
3.5 其他常用方法
| 方法 | 说明 |
|---|---|
boolean contains(String s) | 判断字符串中是否包含指定的子串 |
boolean endsWith(String s) | 判断是否以指定的字符串结尾 |
boolean startsWith(String s) | 判断是否以指定的字符串开头 |
String toLowerCase() | 将字母转成小写 |
String toUpperCase() | 将字母转成大写 |
String trim() | 去掉字符串两端空格 |
4. String 新特性 - 文本块(Java 15+)
4.1 文本块的作用
- 简化跨越多行的字符串,避免对换行等特殊字符进行转义
- 增强 Java 程序中字符串的可读性
4.2 文本块语法
- 开始分隔符:
"""后跟零个或多个空格,最终以行终止符结束 - 结束分隔符:
""" - 文本块内容以开始分隔符的行终止符后的第一个字符开始
传统方式 vs 文本块:
java
// 传统方式
String html = "<html>\n" +
" <body>\n" +
" <p>Hello, 尚硅谷</p>\n" +
" </body>\n" +
"</html>\n";
// 文本块方式(Java 15+)
String html = """
<html>
<body>
<p>Hello, 尚硅谷</p>
</body>
</html>
""";4.3 文本块规则
开始分隔符后必须有行终止符
javaString err = """"""; // 错误:开始分隔符后没有行终止符可以自由使用双引号
javaString story = """ Elly said,"Maybe I was a bird in another life." Noah said,"If you're a bird, I'm a bird." """;可以使用转义字符
javaString html = """ <html>\n <body>\n <p>Hello, world</p>\n </body>\n </html>\n """;
5. StringBuilder 类
5.1 StringBuilder 的引入
问题: String 每拼接一次都会产生一个新的字符串对象,如果多次拼接,会占用内存,效率低。
解决方案: 使用 StringBuilder 或 StringBuffer。
5.2 StringBuilder 的特点
底层缓冲区
- 底层有一个缓冲区(没有被
final修饰的 byte 数组) - 拼接的字符串内容会直接放到数组中,不会随意产生新的字符串对象
- 底层始终只有一个缓冲区
- 底层有一个缓冲区(没有被
默认长度
- 底层数组默认长度为 16
自动扩容
- 如果超出数组的长度,会自动扩容(使用
Arrays.copyOf) - 如果一次性拼接的字符没有超出默认扩容的范围:默认是 2 倍 + 2
- 如果一次性拼接的字符超出了默认扩容范围,就按照实际拼接的字符个数扩容
- 如果超出数组的长度,会自动扩容(使用
5.3 StringBuilder 的构造方法
| 构造方法 | 说明 |
|---|---|
StringBuilder() | 创建空的 StringBuilder 对象 |
StringBuilder(String str) | 根据字符串创建 StringBuilder 对象 |
5.4 StringBuilder 的常用方法
| 方法 | 说明 |
|---|---|
StringBuilder append(任意类型) | 拼接,返回的是 StringBuilder 自己 |
StringBuilder reverse() | 字符串翻转,返回的是 StringBuilder 自己 |
String toString() | 将 StringBuilder 转成 String |
链式调用示例:
java
StringBuilder sb = new StringBuilder("张无忌");
sb.append("赵敏").append("小昭").append("周芷若").append("蛛儿");
System.out.println(sb); // 张无忌赵敏小昭周芷若蛛儿
sb.reverse();
System.out.println(sb); // 儿若芷周昭小敏赵忌无张二、常见面试题
⭐ 基础题
1. String 为什么是不可变的?
答案:
- 底层实现: String 底层是一个被
final修饰的数组(JDK 8 是char[],JDK 9+ 是byte[]) - final 关键字: 数组一旦被
final修饰,地址值直接锁死,不能改变 - 不可变的好处:
- 字符串常量池可以共享字符串对象,节省内存
- 避免了多线程环境下的同步问题
- 保证 hashCode 的稳定性,适合作为 HashMap 的 key
2. String 和 StringBuilder 的区别?
答案:
| 对比项 | String | StringBuilder |
|---|---|---|
| 可变性 | 不可变 | 可变 |
| 线程安全 | 安全(不可变) | 不安全 |
| 性能 | 频繁拼接性能差 | 频繁拼接性能好 |
| 使用场景 | 字符串不频繁修改 | 频繁拼接、修改字符串 |
3. String 的 equals 方法和 == 的区别?
答案:
==:比较的是地址值(引用是否指向同一个对象)equals():比较的是字符串内容是否相同
示例:
java
String s1 = "abc";
String s2 = "abc";
String s3 = new String("abc");
System.out.println(s1 == s2); // true(指向常量池中同一个对象)
System.out.println(s1 == s3); // false(s3 是 new 出来的新对象)
System.out.println(s1.equals(s3)); // true(内容相同)⭐⭐ 进阶题
4. String 创建对象的方式有哪些?有什么区别?
答案:
方式一:直接赋值
java
String s1 = "abc";- 先在字符串常量池中查找是否存在 "abc"
- 如果存在,直接返回引用
- 如果不存在,在常量池中创建对象并返回引用
方式二:new 构造方法
java
String s2 = new String("abc");- 在堆内存中创建一个新对象
- 如果常量池中没有 "abc",也会在常量池中创建一个对象
- 所以可能创建 1 或 2 个对象
5. 如何高效地进行字符串拼接?
答案:
不推荐:使用 String 直接拼接
java
String s = "";
for (int i = 0; i < 1000; i++) {
s += i; // 每次循环都创建新对象,性能差
}推荐:使用 StringBuilder
java
StringBuilder sb = new StringBuilder();
for (int i = 0; i < 1000; i++) {
sb.append(i); // 在原对象上修改,性能好
}
String s = sb.toString();6. StringBuilder 和 StringBuffer 的区别?
答案:
| 对比项 | StringBuilder | StringBuffer |
|---|---|---|
| 线程安全 | 不安全 | 安全(synchronized) |
| 性能 | 高 | 较低 |
| 出现版本 | JDK 1.5 | JDK 1.0 |
| 使用场景 | 单线程环境 | 多线程环境 |
使用建议: 单线程环境下优先使用 StringBuilder,性能更好。
⭐⭐⭐ 高级题
7. String 的 substring 方法在 JDK 6 和 JDK 7+ 有什么区别?
答案:
JDK 6:
- substring 方法会创建一个新的 String 对象,但底层仍然指向原 char 数组
- 如果原字符串很大,substring 后的小字符串会持有大字符串的引用,可能导致内存泄漏
JDK 7+:
- substring 方法会创建一个新的 String 对象,并复制需要的字符到新数组
- 避免了内存泄漏问题,但会占用更多内存
8. 为什么 JDK 9 将 String 底层从 char[] 改为 byte[]?
答案:
节省内存:
- char 类型占 2 字节,byte 类型占 1 字节
- 大多数字符串只包含 Latin-1 字符(1 字节即可表示)
- 可以节省约 50% 的内存
编码优化:
- 引入了
coder字段来标识编码方式 - Latin-1 编码使用 1 字节,UTF-16 编码使用 2 字节
- 根据字符串内容自动选择最优编码
- 引入了
性能提升:
- 减少了内存占用,提高了缓存命中率
- 减少了 GC 压力
三、实用代码片段
1. 字符串遍历
java
public static void traverseString(String str) {
// 方式一:使用 charAt
for (int i = 0; i < str.length(); i++) {
System.out.println(str.charAt(i));
}
// 方式二:使用 toCharArray
char[] chars = str.toCharArray();
for (char c : chars) {
System.out.println(c);
}
}2. 字符串统计
java
public static void countCharacters(String str) {
int big = 0; // 大写字母个数
int small = 0; // 小写字母个数
int number = 0; // 数字个数
char[] chars = str.toCharArray();
for (char c : chars) {
if (c >= 'A' && c <= 'Z') {
big++;
} else if (c >= 'a' && c <= 'z') {
small++;
} else if (c >= '0' && c <= '9') {
number++;
}
}
System.out.println("大写字母个数:" + big);
System.out.println("小写字母个数:" + small);
System.out.println("数字个数:" + number);
}3. 字符串反转
java
public static String reverseString(String str) {
// 方式一:使用 StringBuilder
return new StringBuilder(str).reverse().toString();
// 方式二:手动反转
char[] chars = str.toCharArray();
for (int i = 0, j = chars.length - 1; i < j; i++, j--) {
char temp = chars[i];
chars[i] = chars[j];
chars[j] = temp;
}
return new String(chars);
}4. 判断字符串是否为空
java
public static boolean isEmpty(String str) {
// 判断 null 和 空字符串
return str == null || str.length() == 0;
}
public static boolean isBlank(String str) {
// 判断 null、空字符串和纯空格字符串
return str == null || str.trim().length() == 0;
}5. 字符串分割与拼接
java
public static void stringSplitAndJoin() {
// 分割
String str = "abc,java,test";
String[] parts = str.split(",");
// 拼接
String joined = String.join("-", parts); // "abc-java-test"
// 使用 StringBuilder 拼接
StringBuilder sb = new StringBuilder();
for (int i = 0; i < parts.length; i++) {
if (i > 0) {
sb.append("-");
}
sb.append(parts[i]);
}
String result = sb.toString();
}四、经典练习题
练习 1:遍历字符串
题目: 遍历字符串并打印每个字符。
要求:
- 使用两种方式实现
- 方式一:使用
charAt方法 - 方式二:使用
toCharArray方法
参考答案:
java
public class StringTraversal {
public static void main(String[] args) {
String str = "abcdefg";
// 方式一:使用 charAt
System.out.println("方式一:使用 charAt");
for (int i = 0; i < str.length(); i++) {
System.out.println(str.charAt(i));
}
// 方式二:使用 toCharArray
System.out.println("方式二:使用 toCharArray");
char[] chars = str.toCharArray();
for (char c : chars) {
System.out.println(c);
}
}
}练习 2:统计字符个数
题目: 随便给一个字符串,统计该字符串中大写字母字符、小写字母字符、数字字符出现的次数(不考虑其他字符)。
要求:
- 定义三个变量分别统计大写字母、小写字母、数字的个数
- 遍历字符串进行统计
- 输出统计结果
参考答案:
java
public class CharacterCount {
public static void main(String[] args) {
String str = "sdfaWERWERWE11231";
int big = 0; // 大写字母个数
int small = 0; // 小写字母个数
int number = 0; // 数字个数
// 遍历字符串
char[] chars = str.toCharArray();
for (char c : chars) {
if (c >= 'A' && c <= 'Z') {
big++;
} else if (c >= 'a' && c <= 'z') {
small++;
} else if (c >= '0' && c <= '9') {
number++;
}
}
System.out.println("大写字母个数 = " + big);
System.out.println("小写字母个数 = " + small);
System.out.println("数字个数 = " + number);
}
}输出结果:
大写字母个数 = 6
小写字母个数 = 4
数字个数 = 5练习 3:字符串反转
题目: 将字符串进行反转。
要求:
- 使用 StringBuilder 实现
- 使用数组手动实现
参考答案:
java
public class StringReverse {
public static void main(String[] args) {
String str = "abcdefg";
// 方式一:使用 StringBuilder
String reversed1 = new StringBuilder(str).reverse().toString();
System.out.println("StringBuilder 方式:" + reversed1);
// 方式二:使用数组手动反转
char[] chars = str.toCharArray();
for (int i = 0, j = chars.length - 1; i < j; i++, j--) {
char temp = chars[i];
chars[i] = chars[j];
chars[j] = temp;
}
String reversed2 = new String(chars);
System.out.println("数组方式:" + reversed2);
}
}练习 4:字符串拼接性能对比
题目: 对比 String 和 StringBuilder 在大量拼接时的性能差异。
要求:
- 分别使用 String 和 StringBuilder 拼接 10000 次字符串
- 记录并对比执行时间
参考答案:
java
public class StringConcatPerformance {
public static void main(String[] args) {
int count = 10000;
// 使用 String 拼接
long start1 = System.currentTimeMillis();
String str = "";
for (int i = 0; i < count; i++) {
str += i;
}
long end1 = System.currentTimeMillis();
System.out.println("String 拼接耗时:" + (end1 - start1) + "ms");
// 使用 StringBuilder 拼接
long start2 = System.currentTimeMillis();
StringBuilder sb = new StringBuilder();
for (int i = 0; i < count; i++) {
sb.append(i);
}
String result = sb.toString();
long end2 = System.currentTimeMillis();
System.out.println("StringBuilder 拼接耗时:" + (end2 - start2) + "ms");
}
}预期结果:
String 拼接耗时:XXXms
StringBuilder 拼接耗时:XmsStringBuilder 的性能明显优于 String。
五、学习建议
1. 理解核心概念
- 不可变性: 理解 String 为什么不可变,以及不可变带来的好处
- 常量池: 理解字符串常量池的工作原理
- 性能差异: 理解 String 和 StringBuilder 的性能差异
2. 掌握常用方法
- 判断方法:
equals,equalsIgnoreCase - 获取方法:
charAt,indexOf,substring,length - 转换方法:
toCharArray,getBytes,replace - 分割方法:
split - StringBuilder 方法:
append,reverse,toString
3. 避免常见错误
- 空指针异常: 使用
"abc".equals(s)而不是s.equals("abc") - 性能问题: 频繁拼接字符串时使用 StringBuilder
- 编码问题: 注意
getBytes()的编码问题
4. 实践建议
- 多写代码练习字符串操作
- 理解并掌握字符串的底层原理
- 关注 JDK 新特性(如文本块)
- 在实际项目中选择合适的字符串处理方式
六、知识扩展
1. String.intern() 方法
作用: 将字符串对象加入字符串常量池。
java
String s1 = new String("abc");
String s2 = s1.intern();
String s3 = "abc";
System.out.println(s1 == s2); // false
System.out.println(s2 == s3); // true2. 字符串常量池的位置
| JDK 版本 | 常量池位置 |
|---|---|
| JDK 6 及之前 | 方法区(永久代) |
| JDK 7 | 堆内存 |
| JDK 8 及之后 | 元空间(方法区的实现) |
3. StringJoiner 类(JDK 8+)
作用: 更方便地进行字符串拼接,支持分隔符、前缀和后缀。
java
StringJoiner sj = new StringJoiner(",", "[", "]");
sj.add("a").add("b").add("c");
String result = sj.toString(); // "[a,b,c]"4. String 的不可变性的应用场景
作为 HashMap 的 key
- 不可变性保证了 hashCode 的稳定性
- 避免了键值对丢失的问题
多线程环境下的安全性
- 不可变对象天生是线程安全的
- 不需要额外的同步措施
安全性
- 不可变对象不能被恶意修改
- 适合存储敏感信息(如密码,但建议使用 char[])
学习提示: 本文档涵盖了 String 和 StringBuilder 的核心知识点、常见面试题、实用代码示例和练习题。建议结合实际编码练习,深入理解字符串的底层原理和使用技巧。