11 字符串
前置知识:10 数组
本章目标:理解 String 不可变性与常量池、掌握常用 API、学会用 StringBuilder 高效拼接、熟悉格式化、类型转换与正则入门。
概述
C 的字符串是 char 数组加一个约定俗成的 '\0',所有操作靠 <string.h> 的函数和手动内存管理;Java 把字符串升级为不可变对象 String,配合常量池、StringBuilder 和格式化 API,把 C 时代缓冲区溢出、忘记结尾零、strlen 性能这类问题整体消灭。
String 不可变性与字符串常量池
String 对象一旦创建内容就永不改变。所有「修改」方法都返回新对象:
public class ImmutabilityDemo {
public static void main(String[] args) {
String s = "hello";
s.toUpperCase(); // 返回新对象 "HELLO",原对象不动
System.out.println(s); // hello
String t = s.toUpperCase();
System.out.println(t); // HELLO
s = s + " world"; // 让变量 s 指向了新的拼接结果对象
// 原来的 "hello" 对象依然完好,等待 GC
// 为什么设计成不可变?
// 1. 线程安全:只读对象天然可共享
// 2. hashCode 可以缓存:HashMap 的 key 大量用 String
// 3. 常量池得以实现:同一字面量全局共享一个对象
// 4. 安全性:路径、类名等参数不会被中途篡改
}
}字符串常量池与 == / equals 经典区别
字面量创建的字符串存入常量池(相同字面量复用同一对象);new String() 强制在堆上新建对象:
public class StringPool {
public static void main(String[] args) {
String a = "java"; // 入池
String b = "java"; // 复用池中同一对象
String c = new String("java"); // 强制堆上新建,不查池
String d = "ja" + "va"; // 编译期常量折叠 -> 池中的 "java"
System.out.println(a == b); // true 同一对象
System.out.println(a == c); // false 堆对象 vs 池对象
System.out.println(a.equals(c)); // true 内容相同
System.out.println(a == d); // true 常量折叠发生在编译期
// intern():把堆上的字符串放入池并返回池内引用
System.out.println(a == c.intern()); // true
// 结论(背下来):
// 字符串比较内容永远用 equals;== 只在比较「是否同一对象」时使用
}
}内存布局图示:
flowchart TB subgraph 栈["栈(局部变量)"] A["a"] -->|"指向"| P1 B["b"] -->|"指向"| P1 D["d"] -->|"指向"| P1 end subgraph 方法区常量池["方法区 / 元空间:字符串常量池"] P1["\"java\" 对象"] end subgraph 堆["堆"] C1["c 变量"] --> P2["new String(\"java\")\n独立副本"] end
| 表达式 | 结果 | 原因 |
|---|---|---|
"a" == "a" | true | 同一池对象 |
new String("a") == "a" | false | new 强制新建 |
"ab" == "a"+"b" | true | 编译期常量折叠 |
s1+s2 == "ab"(s 为变量) | false | 运行期拼接产生堆对象 |
常用 API
public class StringApi {
public static void main(String[] args) {
String s = "Hello, RootStack";
// ---- 访问 ----
char ch = s.charAt(0); // 'H' —— 注意不能用 s[0]!Java 无运算符重载
int len = s.length(); // 长度是方法不是属性(数组才是 length 属性)
System.out.println(ch + " " + len);
// ---- 子串与查找 ----
System.out.println(s.substring(7)); // RootStack(到末尾)
System.out.println(s.substring(0, 5)); // Hello([0,5) 左闭右开)
System.out.println(s.indexOf("Root")); // 7,找不到返回 -1
System.out.println(s.indexOf('o')); // 4(字符版本)
System.out.println(s.lastIndexOf('o')); // 15
System.out.println(s.contains("Stack")); // true
System.out.println(s.startsWith("Hello"));// true
System.out.println(s.endsWith("xyz")); // false
// ---- 转换类:全部返回新对象 ----
System.out.println(s.toUpperCase());
System.out.println(" pad ".trim()); // 去首尾空白(<=U+0020)
System.out.println(" pad ".strip()); // Java 11+,按 Unicode 空白更准确
System.out.println(s.replace("o", "0")); // 全部替换
System.out.println(s.replaceFirst("o", "0")); // 正则替换第一个
System.out.println(String.join("-", "2026", "08", "23")); // 2026-08-23
// ---- 比较 ----
String x = "apple", y = "Banana";
System.out.println(x.compareTo(y)); // 正数:'a' > 'B'(区分大小写)
System.out.println(x.equalsIgnoreCase("APPLE"));// true 忽略大小写比较
// ---- 判空 ----
String empty = "";
System.out.println(empty.isEmpty()); // length()==0
System.out.println(" ".isBlank()); // Java 11+,只有空白也算空
}
}C 到 Java 的 API 对照表:
| 任务 | C (string.h) | Java |
|---|---|---|
| 长度 | strlen(s) O(n) | s.length() O(1),长度已缓存 |
| 复制 | strcpy(dst, src) 手动管内存 | 赋值引用即可(不可变无风险) |
| 拼接 | strcat + realloc 手工扩容 | + 或 StringBuilder |
| 比较 | strcmp | equals / compareTo |
| 查找子串 | strstr 返回指针 | indexOf 返回下标 |
| 截取 | 手动复制 + 补 ‘\0’ | substring |
StringBuilder 与链式调用
循环拼接必须用它(原理见 09 循环结构):
public class BuilderDemo {
public static void main(String[] args) {
// 构造时可预估容量避免扩容
StringBuilder sb = new StringBuilder(64);
sb.append("SELECT * FROM users");
sb.append(" WHERE age > ").append(18); // append 有各种重载
if (true) {
sb.append(" AND name LIKE '").append("%root%").append('\'');
}
// 链式调用:append 返回自身 this
String sql = sb.append(" LIMIT 10").toString();
System.out.println(sql);
// 其他常用操作:
sb.insert(0, "-- 查询语句\n"); // 指定位置插入
sb.replace(6, 8, "++"); // 区间替换 [6,8)
sb.deleteCharAt(0); // 删除单个字符
sb.delete(0, 13); // 删除区间
sb.reverse(); // 整体反转——反转字符串题目的利器
System.out.println(sb);
// 与 C 对比:C 拼 strbuf 要自己 realloc、memcpy、维护 len,
// StringBuilder 内部自动扩容(约 2 倍策略),append 均摊 O(1)
}
}StringBuffer 是 StringBuilder 的线程安全版(方法加了 synchronized),单机场景几乎总用 StringBuilder:
| 类 | 可变性 | 线程安全 | 场景 |
|---|---|---|---|
| String | 不可变 | 天然安全 | 固定内容、Map 键 |
| StringBuilder | 可变 | 否 | 单线程拼接(默认选择) |
| StringBuffer | 可变 | 是 | 多线程共享的拼接(罕见) |
字符串格式化
占位符语法与 C 的 printf 一脉相承:
public class FormatDemo {
public static void main(String[] args) {
String name = "RootStack";
double price = 1234.5678;
int count = 42;
// String.format 返回格式化后的字符串(类似 C 的 sprintf)
String line = String.format("%s 有 %d 篇教程, 总价 %.2f 元", name, count, price);
System.out.println(line);
// System.out.printf 直接输出(内部就是 format)
System.out.printf("[%s] %,d%n", name, 12345678); // %,d 千分位; %n 平台换行
System.out.printf("%08.3f%n", price); // 宽度8 补0 保留3位: 1234.568
System.out.printf("%-10s|%n", "左对齐"); // - 左对齐占10列
System.out.printf("%x %o %e%n", 255, 64, 12345.678);// 十六进制 八进制 科学计数
// 常用占位符速查(与 C 相同的部分不再注释):
// %s 字符串 %d 整数 %f 浮点 %.2f 精度 %c 字符 %b boolean %% 百分号本身
// Java 扩展:%n 平台无关换行 %tc 完整日期 %s 可用于任何对象(调 toString)
// MessageFormat 风格的另一种选择(了解即可):
String tpl = "用户 {0} 积分 {1}";
System.out.println(java.text.MessageFormat.format(tpl, name, 9900));
}
}字符串与数字互转
public class ConvertDemo {
public static void main(String[] args) {
// ---- 字符串 -> 数字 ----
int i = Integer.parseInt("123"); // 失败抛 NumberFormatException
long l = Long.parseLong("9999999999");
double d = Double.parseDouble("3.14");
boolean b = Boolean.parseBoolean("true"); // 非 "true"(忽略大小写) 一律 false
try {
Integer.parseInt("12a");
} catch (NumberFormatException e) {
System.out.println("解析失败: " + e.getMessage());
}
// C 的 atoi("12a") 会静默返回 12;Java 选择显式报错
// 进制转换解析
int hex = Integer.parseInt("ff", 16); // 255
int bin = Integer.parseInt("1010", 2); // 10
// ---- 数字 -> 字符串 ----
String s1 = String.valueOf(42); // 推荐写法
String s2 = Integer.toString(42);
String s3 = "" + 42; // 拼接写法,可读性略差但常见
String s4 = Integer.toHexString(255); // ff
String s5 = String.format("%.1f", 3.14); // "3.1"
// ---- 包装类的中间角色 ----
Integer boxed = Integer.valueOf("456"); // 解析为包装类
int unboxed = boxed; // 自动拆箱
System.out.println(i + l + d + b + hex + bin);
System.out.println(s1 + " " + s2 + " " + s3 + " " + s4 + " " + s5 + " " + unboxed);
}
}字符与 codePoint
public class CharAndCodePoint {
public static void main(String[] args) {
// char 是 UTF-16 编码单元(2 字节),不是「一个字符」
String s = "AB中";
for (int i = 0; i < s.length(); i++) {
char c = s.charAt(i);
System.out.println(c + " -> " + (int) c); // 强转得到编码值
}
// 'A'=65, 'B'=66, '中'=20013
// char 支持算术运算(提升为 int),常用技巧:
char digit = '7';
int value = digit - '0'; // 字符数字转数值 = 7(C 同款技巧)
char lower = (char) ('A' + 32); // 大小写差 32 -> 'a'
System.out.println(value + " " + lower);
// 判断工具(Character 类,替代手写 isdigit/isalpha):
System.out.println(Character.isDigit('5')); // true
System.out.println(Character.isLetter('中')); // true
System.out.println(Character.isUpperCase('A')); // true
System.out.println(Character.toLowerCase('A')); // a
// 生僻字问题:部分汉字/emoji 需要 2 个 char(代理对)
String rare = "\uD83D\uDE00"; // 一个 emoji
System.out.println(rare.length() == 2); // true!length 按 char 计
System.out.println(rare.codePointCount(0, rare.length())); // 1 按码点计
// 遍历码点的正确方式:
s.codePoints().forEach(cp ->
System.out.println(Character.toChars(cp)));
}
}正则表达式入门
import java.util.regex.Pattern;
public class RegexIntro {
public static void main(String[] args) {
String input = "订单号: A-2026, B-88";
// 最简单:matches 全串匹配
System.out.println("hello".matches("[a-z]+")); // true
System.out.println("13800138000".matches("1\\d{10}"));// true 手机号格式
// 注意 Java 字符串里反斜杠要双写 \\d
// split 按正则切分
String[] parts = input.split(",\\s*"); // 逗号加可选空白
for (String p : parts) System.out.println(p.trim());
// replaceAll 正则替换
System.out.println(input.replaceAll("\\d+", "#")); // 数字全部换成 #
// Pattern + Matcher:需要复用或提取分组时使用
Pattern p = Pattern.compile("([A-Z])-(\\d+)"); // 两个捕获组
var m = p.matcher(input);
while (m.find()) {
System.out.println("字母=" + m.group(1) + ", 数字=" + m.group(2));
}
// 常用元字符速查:
// \d 数字 \w 单词字符 \s 空白 . 任意字符
// * >=0 次 + >=1 次 ? 0 或 1 次 {m,n} m~n 次
// [] 字符集合 ^ $ 行首行尾 | 或 () 分组
}
}C 语言没有内置正则(POSIX regex.h 体验糟糕);Java 从 1.4 起内置完整正则引擎。
文本块 Text Blocks(Java 15+)
public class TextBlockDemo {
public static void main(String[] args) {
// 三引号多行字符串,换行与缩进直接保留
String json = """
{
"name": "RootStack",
"topics": ["java", "cpp"]
}
""";
System.out.println(json);
// 与传统拼接对比:
String oldWay = "{\n" +
" \"name\": \"RootStack\",\n" +
"}";
System.out.println(json.equals(oldWay.replaceAll("\\s+}\\s+$", "}"))); // 结构一致
// 规则要点:
// 1. 结尾三引号的缩进决定整体的"去除公共缩进"
// 2. 内部引号无需转义(除了连续三个引号)
// 3. 仍可用 \n 等转义,\ 后换行表示续行不换行
String path = """
C:\\tools\\bin"""; // 反斜杠仍需转义
System.out.println(path);
// SQL 示例——比一行行拼接清爽得多:
String sql = """
SELECT id, title
FROM tutorials
WHERE lang = 'java'
AND published = TRUE
ORDER BY created_at DESC
""";
System.out.println(sql.stripIndent());
}
}C 中只能靠 \ 续行或逐行拼接,Java 15 之后的多行文本体验接近脚本语言。
综合示例
/**
* 综合演示:统计一段文本中每个单词出现次数(简化版,不用 Map)
* 涉及 split、trim、正则清洗、StringBuilder 输出
*/
public class WordCountSimple {
public static void main(String[] args) {
String text = """
java is fun and java is powerful
cpp is fast but java is everywhere
""";
// 清洗并切分单词
String[] words = text.toLowerCase().split("\\s+");
// text.split 底层按正则空白切分,自动忽略首尾换行产生的空串吗?
// 不一定!开头可能有空串,这里过滤一下:
java.util.List<String> list = new java.util.ArrayList<>();
for (String w : words) {
if (!w.isBlank()) list.add(w);
}
// 收集去重的单词表
java.util.List<String> unique = new java.util.ArrayList<>();
for (String w : list) {
if (!unique.contains(w)) unique.add(w);
}
// 统计每个单词次数
int[] counts = new int[unique.size()];
for (String w : list) {
counts[unique.indexOf(w)]++;
}
// 用文本块模板 + StringBuilder 组织报表
StringBuilder report = new StringBuilder();
report.append("词频统计\n");
report.append("-".repeat(20)).append('\n'); // repeat 是 Java 11 的 String 方法
for (int i = 0; i < unique.size(); i++) {
report.append(String.format("%-12s %2d 次%n", unique.get(i), counts[i]));
}
System.out.print(report);
// 验证:反转整段文本(reverse 应用)
System.out.println(new StringBuilder(text).reverse().toString().trim());
}
}本章要点回顾
- String 不可变;字面量入常量池,new 强制新建;比较内容用
equals - 循环拼接用 StringBuilder(链式 append),StringBuffer 仅多线程场景需要
- 格式化占位符
%s %d %.2f与 C 一致;String.format 类似 sprintf - 解析失败抛异常而非静默截断;char 按 UTF-16 计数,生僻字需 codePoint
- 正则与文本块是 C 标准库没有的开箱即用能力
- 返回目录:06 变量与数据类型
- 相关:09 循环结构(StringBuilder 性能原理)
练习
| 题号 | 题目 | 链接 | 知识点 |
|---|---|---|---|
| P1014 | Catalan数 | https://www.luogu.com.cn/problem/P1014 | 字符串、数学 |