主题
界面
主题
String 是你写得最多的类型。本节讲两件事:它有哪些常用方法(这部分靠查,不用背),以及为什么它是不可变的(这部分必须理解)。
第二件事的意义超出 String 本身:String 是"不可变类"最标准的范例。理解了它,你就能理解为什么 《封装》 建议把值对象设计成不可变,也能明白 LocalDate、BigDecimal、record 为什么都是"改一下就返回新对象"。
完成本节后,你应该能够:
== 的结果;strip / isBlank / repeat / split / join / format 等常用方法;StringBuilder;== 比较字符串、split 正则、null 调用这三类高频错误。== 比较的是地址吗?(《类与对象》)equals 与 hashCode 吗?(《equals、hashCode 与对象比较》 会讲,本节先按结论使用)第一,String 无处不在。 接口参数、JSON、数据库字段、日志、配置,全是字符串。字符串处理的熟练度直接决定编码速度。
第二,字符串的坑最多。 == 比较偶尔"看起来能用"(常量池)、split 的参数其实是正则、null 上调用方法直接崩——踩过的人都印象深刻。
第三,不可变性的思想会反复出现。 后面学 LocalDate、BigDecimal、Optional、record 时,你会发现它们都遵循同一套设计。
String original = "java";
String upper = original.toUpperCase();
System.out.println(original); // java —— 原对象没变
System.out.println(upper); // JAVA —— 返回的是新对象String 类被声明为 final,内部存放字符的数组也是私有的、不对外暴露——创建之后,它的内容永远不会变。所有"修改"类方法(trim、replace、substring…)都返回新字符串。
不可变的三个好处:
| 好处 | 说明 |
|---|---|
| 可安全共享 | 多个变量可以指向同一个字符串,谁也不会改坏它 |
| 哈希值可缓存 | String 的 hashCode 算一次就存下来,所以适合做 Map 的 key(《HashMap》) |
| 天生线程安全 | 多线程共享不需要加锁(《线程与同步基础》) |
== String a = "abc"; // 字面量:存进常量池,复用
String b = "abc"; // 复用同一个对象
String c = new String("abc"); // new:在堆上新建一个对象
a == b; // true —— 同一个常量池对象
a == c; // false —— 不同对象
a.equals(c); // true —— 内容相同结论:比较内容永远用 equals。 == 偶尔为 true 只是因为常量池复用了对象,不该依赖它。
一个额外现象:编译期常量折叠
String d = "ab" + "c";
d == "abc"; // true因为编译器在编译期就把两个字面量拼成了 "abc",直接进常量池。但只要拼接里出现变量(如 a + "c"),结果就在运行时生成,== 就变 false 了。
| 分类 | 方法 | 说明 |
|---|---|---|
| 判断 | isEmpty() / isBlank() | isBlank 把空白字符也算"空"(Java 11+) |
| 查找 | contains / indexOf / startsWith / endsWith | |
| 截取 | substring(begin, end) | 左闭右开,end 不包含 |
| 替换 | replace / replaceAll | replaceAll 用正则,注意转义 |
| 切分 | split(regex) | 参数是正则,"." 要写成 "\\." |
| 去空白 | strip() / stripLeading / stripTrailing | Java 11+,比 trim 更懂 Unicode |
| 大小写 | toUpperCase / toLowerCase | 建议指定 Locale |
| 拼接 | String.join("-", a, b) / String.format | |
| 其他 | repeat / lines / chars | Java 11+ 新增 |
// 慢:每次 += 都会创建新的 String 对象
String result = "";
for (int i = 0; i < 10000; i++) {
result += i;
}
// 快:StringBuilder 内部维护一个可变数组
StringBuilder builder = new StringBuilder();
for (int i = 0; i < 10000; i++) {
builder.append(i);
}为什么?String 不可变,每次拼接都要复制整段内容,10000 次拼接就是 O(n²) 的复制量。
那 a + b + c 呢?
单次拼接用 + 完全没问题,编译器会自动优化成 StringBuilder(Java 9+ 用 invokedynamic,见 《Java 语言与运行机制》 的字节码)。
只有循环内的拼接才必须手动用 StringBuilder。
String json = """
{
"name": "知识库",
"lessons": 24
}
""";三个引号包裹,内部的换行与缩进自动处理,写 SQL、JSON、HTML 时比拼接清晰得多。
String original = "java";
String upper = original.toUpperCase();
System.out.println(original + " / " + upper); // java / JAVAString text = " Java 21 学习知识库 ";
String stripped = text.strip();
System.out.println(stripped.length()); // 13(去掉前后空白)
System.out.println(stripped.contains("21")); // true
System.out.println(stripped.replace("21", "25")); // Java 25 学习知识库// 错误一:用 == 比较内容
if (status == "PAID") { } // 可能为 false,应该用 equals
// 错误二:split 的参数是正则
"2026.09.21".split("."); // 返回长度为 0 的数组!
"2026.09.21".split("\\."); // 正确
// 错误三:对可能为 null 的字符串调方法
String value = map.get("missing");
if (value.startsWith("A")) { } // 可能 NPE
if (value != null && value.startsWith("A")) { } // 正确另一个常用技巧:把常量写在前面避免 NPE——"A".equals(value) 永远不会 NPE。
文件:labs/common-classes/src/main/java/cn/kb/learn/common/StringLesson.java
package cn.kb.learn.common;
import java.util.Arrays;
public class StringLesson {
public static void main(String[] args) {
// 1. 不可变性:所有"修改"方法都返回新字符串
String original = "java";
String upper = original.toUpperCase();
System.out.println("原字符串: " + original + ",转换后: " + upper);
// 2. 字符串常量池与 ==
String a = "abc";
String b = "abc";
String c = new String("abc");
System.out.println("字面量 == 字面量: " + (a == b));
System.out.println("new String == 字面量: " + (a == c));
System.out.println("内容相等(equals): " + a.equals(c));
// 3. 编译期常量折叠
String d = "ab" + "c";
System.out.println("编译期拼接 == 字面量: " + (a == d));
// 4. 常用 API
String text = " Java 21 学习知识库 ";
String stripped = text.strip();
System.out.println("strip 后: [" + stripped + "]");
System.out.println("长度: " + stripped.length());
System.out.println("是否空白串: " + " ".isBlank());
System.out.println("包含 21: " + stripped.contains("21"));
System.out.println("替换后: " + stripped.replace("21", "25"));
System.out.println("截取前 4 个字符: " + stripped.substring(0, 4));
System.out.println("按逗号切分: " + Arrays.toString("a,b,c".split(",")));
System.out.println("拼接: " + String.join("-", "a", "b", "c"));
System.out.println("格式化: " + String.format("%s 已收录 %d 节课", "知识库", 24));
System.out.println("重复: " + "ab".repeat(3));
// 5. 循环拼接要用 StringBuilder
String byPlus = "";
for (int i = 0; i < 5; i++) {
byPlus += i;
}
StringBuilder builder = new StringBuilder();
for (int i = 0; i < 5; i++) {
builder.append(i);
}
System.out.println("加号拼接: " + byPlus);
System.out.println("StringBuilder: " + builder);
}
}运行命令:
cd labs
./mvnw -pl common-classes compile
java -cp common-classes/target/classes cn.kb.learn.common.StringLesson预期输出:
原字符串: java,转换后: JAVA
字面量 == 字面量: true
new String == 字面量: false
内容相等(equals): true
编译期拼接 == 字面量: true
strip 后: [Java 21 学习知识库]
长度: 13
是否空白串: true
包含 21: true
替换后: Java 25 学习知识库
截取前 4 个字符: Java
按逗号切分: [a, b, c]
拼接: a-b-c
格式化: 知识库 已收录 24 节课
重复: ababab
加号拼接: 01234
StringBuilder: 01234前三行的对比很关键:a == b 为 true、a == c 为 false、equals 为 true——这就是常量池造成的"时对时错"。
String c = new String("abc"); 处断点,把 a、b、c 同时加入 Watch,对比 id:a 与 b 相同,c 不同——常量池的作用一眼可见;original.toUpperCase() 后观察 original 的内容没变——按 F7 进入 toUpperCase 内部,看它最后返回的是 new String(...);stripped.substring(0, 4) 改成 substring(0, 100),用异常断点停在 StringIndexOutOfBoundsException——记住 substring 的第二个参数是结束下标(不包含);builder.append(i) 处断点,观察 StringBuilder 内部的 byte[] value 与 count 如何变化——它靠"预分配 + 追加"避免了每次复制。== 比较字符串 if (name == "张三") { } // 时对时错
if ("张三".equals(name)) { } // 正确,且不怕 nullsplit 的参数是正则 "2026.09.21".split("."); // 返回空数组
"2026.09.21".split("\\."); // 正确
"a|b".split("\\|"); // 正确修复:特殊字符(. | * + ? ( ) [ ] { } ^ $ \)都要转义;不确定时用 Pattern.quote(".")。
String value = null;
value.length(); // NullPointerException修复:先用常量做判断("x".equals(value)),或显式判空(《读懂 Java 错误信息》 讲过排查思路)。
+ 拼接 见 5.4 节。数据量大时性能差距可达数百倍。
目标:写一个字符串工具方法集,并用 StringBuilder 优化。
步骤:
== 的三行);String reverse(String text):反转字符串(不能直接用 StringBuilder.reverse(),先手写一遍,再用 StringBuilder 验证);boolean isPalindrome(String text):忽略大小写与空白,判断回文;String maskPhone(String phone):把中间四位替换成 ****(如 13812345678 → 138****5678);+ 拼接"与"用 StringBuilder 拼接"的耗时(System.currentTimeMillis()),记录差距。验收方式:能说出第 5 步差距的数量级与原因。
扩展任务:用文本块(""")改写一个包含多行的字符串拼接场景(如生成一段 JSON),对比可读性。
基础题
String 是可变类型。(对 / 错)代码阅读题
String a = "hello";
String b = "hel" + "lo";
String c = "hel" + new String("lo");
System.out.println(a == b);
System.out.println(a == c);
System.out.println(a == c.intern());intern() 做了什么?编程题
Map<String, Integer> wordCount(String text):统计一段英文文本中每个单词出现的次数(按空白切分,忽略大小写,去掉逗号与句号)。综合任务
LogFormatter 类: String format(String level, String message, LocalDateTime time):生成 [2026-09-21 14:30:00] INFO - 消息 格式的日志行;String shorten(String text, int maxLength):超长文本截断为"前 N 个字符 + ...";String repeat(String text, int times):返回重复拼接的结果(用 StringBuilder 实现,不用 repeat);StringBuilder、为什么。提示 1(方向):第 4 题的关键是"编译期折叠"与"运行时创建"的区别。
提示 2(关键点):第 5 题先 toLowerCase() 再切分,切分前用 replace 去掉标点。
提示 3(接近答案):第 6 题的 format 用 DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss"),shorten 要先判断长度是否超限。
String 是不可变类,所有修改方法都返回新对象。equals、StringBuilder。true ← "hel" + "lo" 是编译期常量折叠,进常量池,与 a 同一个对象
false ← 有 new String("lo"),拼接在运行时进行,得到新对象
true ← intern() 会把这个字符串放入常量池并返回池中对象,与 a 相同intern():查常量池,存在就返回池中对象,不存在则放入再返回。它是"手动入池"的机制,实证应用里很少用(有性能与内存风险),但面试常问。
package cn.kb.learn.common;
import java.util.HashMap;
import java.util.Map;
import java.util.TreeMap;
public class WordCount {
static Map<String, Integer> wordCount(String text) {
Map<String, Integer> counter = new HashMap<>();
if (text == null || text.isBlank()) {
return counter;
}
String cleaned = text.toLowerCase()
.replace(",", " ")
.replace(".", " ")
.replace("!", " ")
.replace("?", " ");
for (String word : cleaned.split("\\s+")) {
if (!word.isBlank()) {
counter.merge(word, 1, Integer::sum);
}
}
return counter;
}
public static void main(String[] args) {
String text = "Java is simple. Java is powerful!";
System.out.println(new TreeMap<>(wordCount(text)));
}
}要点:split("\\s+") 用 + 合并连续空白;打印时用 TreeMap 保证顺序稳定。
预期输出:
{is=2, java=2, powerful=1, simple=1}package cn.kb.learn.common;
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
public class LogFormatter {
private static final DateTimeFormatter FORMATTER = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");
public static String format(String level, String message, LocalDateTime time) {
return String.format("[%s] %s - %s", time.format(FORMATTER), level, message);
}
public static String shorten(String text, int maxLength) {
if (text == null || text.length() <= maxLength) {
return text;
}
return text.substring(0, maxLength) + "...";
}
public static String repeat(String text, int times) {
StringBuilder builder = new StringBuilder(text.length() * times);
for (int i = 0; i < times; i++) {
builder.append(text);
}
return builder.toString();
}
public static void main(String[] args) {
System.out.println(format("INFO", "知识库已加载 24 节课", LocalDateTime.of(2026, 9, 21, 14, 30, 0)));
System.out.println(shorten("这是一段很长的日志内容,需要被截断显示", 10));
System.out.println(repeat("ab", 3));
}
}哪里用了 StringBuilder:repeat 方法。原因有三:循环内拼接、次数由参数决定(可能很大)、以及提前指定容量(new StringBuilder(text.length() * times))避免内部数组反复扩容。
为什么 format 用 String.format 而不是拼 +:可读性更好,且格式串与参数分离,方便调整。
预期输出:
[2026-09-21 14:30:00] INFO - 知识库已加载 24 节课
这是一段很长的日志内...
ababab== 的影响,并坚持用 equals 比较内容;strip / isBlank / split / join / format;StringBuilder,并说清原因;==、正则转义、null 这三个方向。String 为什么适合做 key《equals、hashCode 与对象比较》:讲清"两个对象什么时候算相等",这是集合、去重、缓存的基础。