JavaSE之String 与 StringBuilder 全面解析(附实例代码)
在 Java 开发中,字符串操作是高频场景,String
、StringBuilder
作为处理字符串的核心类,理解其特性与差异对写出高效代码至关重要。本文将从类的本质、实现原理、常用方法到实战场景,系统梳理两者的核心知识点,附完整代码示例供直接使用。
一、String 类详解
1. String 类介绍
1.1 核心概述
String
类用于表示字符串,Java 中所有带双引号的字符串字面值(如 "abc"
),本质上都是 String
类的实例。
例如:String s = "abc"
中,s
是对象名,"abc"
是 String
类的具体实例。
1.2 三大核心特点
- 不可变性 :字符串一旦创建,其值无法修改(底层数组被
final
修饰,地址锁死)。 - 可共享性 :相同内容的字符串字面值会复用常量池中的同一个对象,减少内存消耗。
例:String s1 = "abc"; String s2 = "abc";
中,s1
和s2
指向常量池同一个对象。 - 字面值即实例 :所有双引号包裹的字符串,默认都是
String
实例,无需手动new
创建。
2. String 实现原理
2.1 底层存储结构(JDK 差异)
String
的底层是被 final
修饰的数组,但数组类型随 JDK 版本变化:
JDK 版本 | 底层数组类型 | 占用内存(单个元素) |
---|---|---|
JDK 8 及之前 | private final char[] value |
2 字节(char 类型) |
JDK 9 及之后 | private final byte[] value |
1 字节(byte 类型) |
2.2 为什么从 char[]
改为 byte[]
?
核心目的是节省内存:
- 大部分场景下,字符串由 ASCII 字符(如英文字母、数字)组成,用 1 字节的
byte
即可存储,而char
固定占 2 字节,会造成内存浪费。 - 对中文等非 ASCII 字符,
byte[]
会通过编码(如 UTF-8)动态调整字节数,兼顾内存与兼容性。
2.3 不可变性的本质
底层数组被 final
修饰 → 数组地址无法修改,且 String
类未提供修改数组内容的方法(如 set
方法),因此字符串创建后值无法改变。
3. String 对象的创建方式
3.1 五种常见构造方法
构造方法 | 作用描述 |
---|---|
String() |
创建空字符串对象(无内容,长度为 0) |
String(String str) |
基于已有字符串创建新对象(如 new String("你好") ) |
String(char[] chars) |
将字符数组转为字符串(如 new String(new char[]{'a','b'}) ) |
String(byte[] bytes) |
将字节数组按平台默认编码(如 GBK)转为字符串 |
String(byte[] bytes, int offset, int length) |
截取字节数组的一部分(从 offset 索引开始,取 length 个元素)转为字符串 |
3.2 简化创建方式
直接通过字面值赋值:String 变量名 = "字符串内容"
,例如 String s = "abc"
,此方式会优先使用常量池,避免重复创建对象。
3.3 代码示例(含扩展构造)
java
package com.code.day13;
public class Day13String {
public static void main(String[] args) {
// 1. 无参构造
String s1 = new String();
System.out.println("s1=" + s1); // 输出:s1=
// 2. 基于已有字符串构造
String s2 = new String("你好");
System.out.println("s2=" + s2); // 输出:s2=你好
// 3. 基于字符数组构造
char[] chars = new char[]{'a', 'b', '3'};
String s3 = new String(chars);
System.out.println("s3=" + s3); // 输出:s3=ab3
// 4. 基于字节数组构造(默认编码)
byte[] bytes = new byte[]{97, 98, 99}; // ASCII 码:97=a,98=b,99=c
String s4 = new String(bytes);
System.out.println("s4=" + s4); // 输出:s4=abc
// 5. 扩展构造:截取字节数组的一部分(从索引0开始,取2个元素)
String s6 = new String(bytes, 0, 2);
System.out.println("s6=" + s6); // 输出:s6=ab
// 6. 扩展构造:截取字符数组的一部分(从索引1开始,取2个元素)
String s7 = new String(chars, 1, 2);
System.out.println("s7=" + s7); // 输出:s7=b3
}
}
3.4 经典面试题:new String("abc")
创建几个对象?
- 答案 :1 个或 2 个。
- 若常量池中已存在
"abc"
,则仅创建 1 个对象(new
关键字在堆中创建的对象)。 - 若常量池中不存在
"abc"
,则创建 2 个对象(1 个在常量池,1 个在堆中)。
- 若常量池中已存在
3.5 字符串拼接的底层逻辑(面试高频)
java
public class Demo05String {
public static void main(String[] args) {
String s1 = "hello";
String s2 = "world";
String s3 = "helloworld";
String s4 = "hello" + "world"; // 字面值拼接,复用常量池
String s5 = s1 + s2; // 变量拼接,底层 new StringBuilder
String s6 = s1 + "world"; // 含变量,底层 new StringBuilder
System.out.println(s3 == s4); // true(均指向常量池 "helloworld")
System.out.println(s3 == s5); // false(s5 是堆中新建对象)
System.out.println(s3 == s6); // false(s6 是堆中新建对象)
}
}
结论:
- 仅字面值拼接(如
"a"+"b"
):复用常量池,不产生新对象。 - 含变量的拼接(如
s1+"b"
):底层通过new StringBuilder
实现,会在堆中新建对象。
4. String 常用方法(分类详解)
4.1 判断类方法(对比内容)
方法 | 作用描述 | 场景示例 |
---|---|---|
boolean equals(Object obj) |
严格比较内容(区分大小写) | 密码验证 |
boolean equalsIgnoreCase(String s) |
比较内容(忽略大小写) | 验证码验证(如 "Abc" 和 "abc") |
代码示例:
java
public class Day13StringEquals {
public static void main(String[] args) {
String s1 = "hello";
String s2 = "Hello";
// 严格比较(区分大小写)
boolean result = s1.equals(s2);
System.out.println(result); // 输出:false
// 忽略大小写比较
boolean result1 = s1.equalsIgnoreCase(s2);
System.out.println(result1); // 输出:true
}
}
避坑提示 :
若比较时可能出现 null
(如用户输入),需将确定非 null
的字符串放前面,避免空指针异常:
"abc".equals(s)
(正确) vs s.equals("abc")
(若 s 为 null 则报错)。
也可使用 Objects.equals("abc", s)
(Java 7+),自动处理 null。
实战练习:登录验证:
java
package com.code.day13;
import java.util.Scanner;
public class Day13LianXi {
public static void main(String[] args) {
Scanner sc = new Scanner(System.in);
// 正确账号密码
String username = "root";
String password = "root";
// 最多3次登录机会
for (int i = 0; i < 3; i++) {
System.out.println("请输入用户名:");
String name = sc.next();
System.out.println("请输入密码:");
String pwd = sc.next();
// 验证(避免null)
if (username.equals(name) && password.equals(pwd)) {
System.out.println("登陆成功");
break;
} else {
if (i == 2) {
System.out.println("账号冻结");
} else {
System.out.println("用户名或密码错误");
}
}
}
sc.close();
}
}
4.2 获取类方法(提取内容/长度)
方法 | 作用描述 |
---|---|
String concat(String str) |
拼接字符串,返回新串(如 "a".concat("b") → "ab") |
char charAt(int index) |
根据索引获取字符(索引从 0 开始) |
int indexOf(String str) |
获取子串第一次出现的索引(无则返回 -1) |
String substring(int beginIndex) |
从 beginIndex 截取到末尾,返回新串 |
String substring(int begin, int end) |
截取 [begin, end) 区间(含头不含尾) |
int length() |
获取字符串长度(字符个数) |
代码示例:
java
public class Day13StringGet {
public static void main(String[] args) {
String s = "abcdefg";
// 拼接
String newStr1 = s.concat("hahaha");
System.out.println(newStr1); // 输出:abcdefghahaha
// 按索引取字符
char data = s.charAt(2);
System.out.println(data); // 输出:c(索引2对应第3个字符)
// 子串索引
System.out.println(s.indexOf("c")); // 输出:2
// 截取(从索引2到末尾)
System.out.println(s.substring(2)); // 输出:cdefg
// 截取(索引2到4,含2不含4)
System.out.println(s.substring(2, 4)); // 输出:cd
// 长度
System.out.println(s.length()); // 输出:7
}
}
实战练习:遍历字符串:
java
public class Day13StringGetLianXi {
public static void main(String[] args) {
String s = "abcdefg";
// 遍历每一个字符
for (int i = 0; i < s.length(); i++) {
System.out.println(s.charAt(i));
}
}
}
4.3 转换类方法(格式转换)
方法 | 作用描述 |
---|---|
char[] toCharArray() |
将字符串转为字符数组 |
byte[] getBytes() |
按默认编码转为字节数组 |
byte[] getBytes(String charset) |
按指定编码(如 "GBK")转为字节数组 |
String replace(CharSequence old, CharSequence new) |
替换子串(如 "a".replace("a","b") → "b") |
代码示例:
java
import java.util.Arrays;
import java.io.UnsupportedEncodingException;
public class Day13StringZhuan {
public static void main(String[] args) throws UnsupportedEncodingException {
String s = "abcdefg";
// 1. 转字符数组
char[] charArray = s.toCharArray();
System.out.println(charArray); // 输出:abcdefg(print直接输出数组内容)
// 2. 转字节数组(默认编码 UTF-8)
byte[] bytes = s.getBytes();
System.out.println(Arrays.toString(bytes)); // 输出:[97, 98, 99, 100, 101, 102, 103]
// 3. 替换子串
String newStr = s.replace("a", "z");
System.out.println(newStr); // 输出:zbcdefg
// 4. 按指定编码转字节数组(GBK)
byte[] gbks = "你".getBytes("GBK"); // 中文 GBK 占 2 字节
System.out.println(Arrays.toString(gbks)); // 输出:[-60, -29]
}
}
实战练习:统计字符类型次数:
java
import java.util.Scanner;
public class Day13StringZhuanLianXi {
public static void main(String[] args) {
Scanner scanner = new Scanner(System.in);
System.out.println("请输入一个字符串:");
String str = scanner.next();
// 统计变量
int big = 0; // 大写字母
int small = 0; // 小写字母
int number = 0;// 数字
// 转字符数组遍历
char[] charArray = str.toCharArray();
for (char c : charArray) {
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);
scanner.close();
}
}
4.4 分割类方法(拆分字符串)
String[] split(String regex)
:按指定规则(正则表达式)拆分字符串,返回字符串数组。
代码示例:
java
import java.util.Arrays;
public class Day13StringSplit {
public static void main(String[] args) {
// 1. 按逗号分割
String s = "a,java,python";
String[] arr = s.split(",");
System.out.println(Arrays.toString(arr)); // 输出:[a, java, python]
// 增强for遍历
for (String element : arr) {
System.out.println(element);
}
// 2. 按点分割(注意:. 在正则中代表任意字符,需转义为 \\.)
String s1 = "java.txt";
String[] split = s1.split("\\.");
for (String el : split) {
System.out.println(el); // 输出:java、txt
}
}
}
4.5 其他常用方法
方法 | 作用描述 |
---|---|
boolean contains(String s) |
判断是否包含子串(如 "abc".contains("ab") → true) |
boolean startsWith(String s) |
判断是否以子串开头(如 "abc".startsWith("a") → true) |
boolean endsWith(String s) |
判断是否以子串结尾(如 "abc".endsWith("c") → true) |
String toLowerCase() |
转为小写(如 "ABC".toLowerCase() → "abc") |
String toUpperCase() |
转为大写(如 "abc".toUpperCase() → "ABC") |
String trim() |
去除两端空格(中间空格保留,如 " a b " → "a b") |
代码示例:
java
public class Day13StringOtherMethod {
public static void main(String[] args) {
String s = "abcdefg";
// 包含子串
System.out.println(s.contains("abcd")); // 输出:true
// 开头/结尾判断
System.out.println(s.startsWith("ab")); // 输出:true
System.out.println(s.endsWith("fg")); // 输出:true
// 大小写转换
String s1 = "ABCDEFG";
System.out.println(s1.toLowerCase()); // 输出:abcdefg
System.out.println(s.toUpperCase()); // 输出:ABCDEFG
// 去空格
String s2 = " a b c ";
System.out.println(s2.trim()); // 输出:a b c(两端空格去除)
System.out.println(s2.replace(" ", "")); // 输出:abc(所有空格去除)#### 4.6 String 新特性:文本块(Java 15+ 正式特性)
在 Java 15 之前,编写多行字符串(如 HTML、JSON、SQL)需手动拼接换行符(`\n`)和转义引号(`\"`),代码可读性差且易出错。**文本块**通过三引号(`"""`)解决此问题,支持多行字符串自动转义,保留格式。
##### 核心优势
- 无需手动转义换行符、双引号;
- 直接保留字符串原始格式,代码更易读;
- 支持嵌套双引号(无需转义)。
##### 代码示例
```java
public class Day13StringKuai {
public static void main(String[] args) {
// 传统方式:手动拼接换行符和转义引号
String oldHtml = "<!DOCTYPE html>\n" +
"<html lang=\"en\">\n" +
"<head>\n" +
" <meta charset=\"UTF-8\">\n" +
" <title>Title</title>\n" +
"</head>\n" +
"<body>\n" +
" <p>刘大胆</p>\n" +
"</body>\n" +
"</html>";
System.out.println("传统方式输出:");
System.out.println(oldHtml);
// 文本块方式:三引号包裹,保留原始格式
String newHtml = """
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Title</title>
</head>
<body>
<p>刘大胆</p>
</body>
</html>
""";
System.out.println("\n文本块方式输出:");
System.out.println(newHtml);
// 嵌套双引号示例
String story = """
Elly said,"Maybe I was a bird in another life."
Noah said,"If you're a bird , I'm a bird."
""";
System.out.println("\n嵌套双引号文本块:");
System.out.println(story);
}
}
输出结果(格式完全一致)
html
传统方式输出:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Title</title>
</head>
<body>
<p>刘大胆</p>
</body>
</html>
文本块方式输出:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Title</title>
</head>
<body>
<p>刘大胆</p>
</body>
</html>
嵌套双引号文本块:
Elly said,"Maybe I was a bird in another life."
Noah said,"If you're a bird , I'm a bird."
二、StringBuilder 类详解
1. StringBuilder 类介绍
1.1 核心概述
StringBuilder
是可变字符序列 ,底层基于未被 final
修饰的字节数组(缓冲区)实现,核心作用是高效拼接字符串 ,解决 String
拼接时频繁创建对象的问题。
1.2 为什么需要 StringBuilder?
String
不可变,每次拼接都会生成新对象(如 s = s + "a"
会创建 2 个新对象),频繁拼接时:
- 内存占用高(大量临时对象);
- 拼接效率低(对象创建+垃圾回收耗时)。
StringBuilder
通过缓冲区复用解决此问题:拼接内容直接存入底层数组,仅在数组容量不足时扩容,不频繁创建新对象。
1.3 核心特点
- 可变缓冲区 :底层是未被
final
修饰的byte[]
,支持动态扩容; - 默认容量:初始缓冲区长度为 16(可通过构造方法指定初始容量);
- 自动扩容机制 :
- 若需存储的内容长度 ≤ 原容量的 2 倍 + 2 → 按"2 倍 + 2"扩容;
- 若需存储的内容长度 > 原容量的 2 倍 + 2 → 按"实际需要的长度"扩容;
- 效率高 :无线程安全锁(区别于
StringBuffer
),拼接速度更快。
2. StringBuilder 的使用
2.1 构造方法
构造方法 | 作用描述 |
---|---|
StringBuilder() |
创建空的 StringBuilder ,初始容量 16 |
StringBuilder(String str) |
基于已有字符串创建,初始容量 = 16 + 字符串长度 |
2.2 核心方法(重点)
方法 | 作用描述 | 返回值类型 |
---|---|---|
append(任意类型) |
拼接内容到缓冲区(支持 int、String、char 等) | StringBuilder(自身) |
reverse() |
反转缓冲区中的内容 | StringBuilder(自身) |
toString() |
将 StringBuilder 转为 String 类型 |
String |
length() |
获取缓冲区中有效字符的长度 | int |
关键特性:
append()
和reverse()
方法返回自身对象 ,支持链式调用(如sb.append("a").append("b")
)。
2.3 代码示例
java
public class Day13StringBuilder {
public static void main(String[] args) {
// 1. 空构造(初始容量 16)
StringBuilder sb = new StringBuilder();
System.out.println("空 StringBuilder:" + sb); // 输出:空 StringBuilder:
// 2. 基于字符串构造(初始容量 16 + "Hello" 长度 5 = 21)
StringBuilder sb1 = new StringBuilder("Hello");
System.out.println("初始 StringBuilder:" + sb1); // 输出:初始 StringBuilder:Hello
// 3. 链式拼接(append 返回自身,支持连续调用)
sb1.append("刘大胆").append("大胃袋").append(123);
System.out.println("拼接后:" + sb1); // 输出:拼接后:Hello刘大胆大胃袋123
// 4. 反转内容
sb1.reverse();
System.out.println("反转后:" + sb1); // 输出:反转后:321袋胃大胆大刘olleH
// 5. 转为 String 类型(后续可调用 String 方法)
String result = sb1.toString();
System.out.println("转为 String 后:" + result); // 输出:转为 String 后:321袋胃大胆大刘olleH
// 6. 获取有效长度
System.out.println("有效字符长度:" + sb1.length()); // 输出:有效字符长度:13
}
}
3. StringBuilder 实战练习:判断回文
回文定义 :正读和反读一致的字符串(如"上海自来水来自海上""abcba")。
实现思路 :用 StringBuilder
反转字符串,再与原字符串比较。
代码示例
java
import java.util.Scanner;
public class Day13StringBuilderLianXi {
public static void main(String[] args) {
Scanner sc = new Scanner(System.in);
System.out.println("请输入字符串:");
String original = sc.next();
// 1. 用 StringBuilder 反转字符串
StringBuilder sb = new StringBuilder(original);
StringBuilder reversedSb = sb.reverse(); // 反转后仍是 StringBuilder 类型
// 2. 转为 String 后与原字符串比较(注意:必须转 String,否则类型不匹配)
String reversedStr = reversedSb.toString();
if (original.equals(reversedStr)) {
System.out.println("该字符串是回文");
} else {
System.out.println("该字符串不是回文");
}
sc.close();
}
}
测试结果
// 输入:abcba
请输入字符串:
abcba
该字符串是回文
// 输入:abcd
请输入字符串:
abcd
该字符串不是回文
4. String、StringBuilder、StringBuffer 区别(面试重点)
三者核心功能都是处理字符串,但设计目标不同,关键差异集中在可变性 、线程安全 和效率上。
对比维度 | String | StringBuilder | StringBuffer |
---|---|---|---|
可变性 | 不可变(底层 final 数组) | 可变(底层非 final 数组) | 可变(底层非 final 数组) |
线程安全 | 安全(不可变天然线程安全) | 不安全(无同步锁) | 安全(方法加 synchronized 锁) |
拼接效率 | 低(频繁创建新对象) | 高(缓冲区复用,无锁) | 中(缓冲区复用,有锁开销) |
底层实现 | final char[](JDK8)/final byte[](JDK9+) | byte[](非 final) | byte[](非 final) |
适用场景 | 字符串不频繁修改(如常量定义) | 单线程下频繁拼接(如普通业务逻辑) | 多线程下频繁拼接(如并发日志打印) |
选型建议
- 若字符串无需修改(如配置项、固定文本)→ 用
String
; - 若单线程环境下需频繁拼接(如循环拼接、动态生成文本)→ 用
StringBuilder
(优先选,效率最高); - 若多线程环境下需频繁拼接(如多线程日志、并发生成数据)→ 用
StringBuffer
。
三、总结
- String:不可变字符串,适合存储固定内容,拼接效率低,需注意常量池复用机制;
- StringBuilder:可变字符序列,单线程高效拼接首选,底层缓冲区自动扩容,支持链式调用;
- 文本块(Java 15+):简化多行字符串编写,提升代码可读性,无需手动转义;
- 三者差异核心在"可变性"和"线程安全",需根据实际场景(是否修改、是否多线程)选择合适的类。
通过本文的梳理,相信你已掌握 String 与 StringBuilder 的核心用法与底层逻辑,在实际开发中能更高效地处理字符串操作!