第 07 课:String 与 StringBuilder
本课目标
学完后,你应该能够:
- 理解
String是引用类型,而且字符串对象不可变。 - 正确使用
equals()比较内容,不用==代替。 - 使用长度、索引、截取、查询、替换、大小写转换和去空白方法。
- 使用
split()把字符串拆成数组,并理解它接收正则表达式。 - 使用
charAt()和循环逐字符分析文本。 - 使用
StringBuilder高效构建字符串。 - 安全处理
null、空字符串、空白字符串和非法索引。 - 完成用户名校验、手机号脱敏和技能列表格式化。
1. String 的基本语法
String language = "Java";
String是类,因此首字母大写。language是引用变量。"Java"是字符串字面量,必须使用双引号。char保存单个字符,使用单引号,例如'J'。
字符串可以包含转义字符:
String line = "第一行\n第二行"; // \n 表示换行
String quote = "他说:"你好""; // " 表示字符串中的双引号
String path = "C:\Java\code"; // \ 表示一个反斜杠
2. String 不可变
String text = "java";
text.toUpperCase();
System.out.println(text); // 仍然是 java
toUpperCase() 没有修改原对象,而是返回一个新字符串。必须接住返回结果:
String upper = text.toUpperCase();
// 或者让变量改为引用新对象
text = text.toUpperCase();
“不可变”指字符串对象的字符内容创建后不能改变,不代表变量不能改为引用另一个字符串。
同理,下面的方法都返回新字符串:
text.strip();
text.replace("a", "A");
text.substring(1);
text.toLowerCase();
如果不保存或使用返回值,操作结果就丢失。
3. length、isEmpty 与 isBlank
String text = " Java ";
int length = text.length();
boolean empty = text.isEmpty();
boolean blank = text.isBlank();
length():字符数量。isEmpty():长度是否等于 0。isBlank():是否为空,或者是否只包含空白字符。
区别:
| 字符串 | isEmpty() | isBlank() |
|---|---|---|
"" | true | true |
" " | false | true |
"Java" | false | false |
数组使用 array.length,字符串使用 text.length()。前者是属性,后者是方法。
4. 索引与 charAt
String text = "Java";
char first = text.charAt(0); // J
char last = text.charAt(text.length() - 1); // a
字符串索引与数组一样从 0 开始:
索引: 0 1 2 3
字符: J a v a
访问 text.charAt(4) 会抛出 StringIndexOutOfBoundsException,因为合法索引只有 0–3。
逐字符遍历:
for (int index = 0; index < text.length(); index++) {
char current = text.charAt(index);
System.out.println(current);
}
5. substring 截取
String text = "JavaLearning";
String first = text.substring(0, 4); // Java
String rest = text.substring(4); // Learning
substring(beginIndex, endIndex) 的范围是左闭右开:
[beginIndex, endIndex)
包含开始索引,不包含结束索引。因此 substring(0, 4) 取得索引 0、1、2、3。
常见错误:
- 开始索引小于 0;
- 结束索引大于字符串长度;
- 开始索引大于结束索引。
这些都会产生索引异常。截取手机号前必须先检查长度。
6. 查询字符串
String text = "Java Spring Boot";
text.contains("Spring"); // true
text.startsWith("Java"); // true
text.endsWith("Boot"); // true
text.indexOf("Spring"); // 5
text.indexOf("Python"); // -1
text.lastIndexOf("a"); // 最后一个 a 的索引
indexOf() 找不到时返回 -1,与数组线性查找的约定相同。
这些方法区分大小写:contains("java") 不等同于 contains("Java")。
7. equals 与 ==
String first = new String("java");
String second = new String("java");
System.out.println(first.equals(second)); // true
System.out.println(first == second); // false
equals()比较字符内容。==比较两个引用是否指向同一个对象。
字符串字面量可能因为字符串常量池而让某些 == 恰好得到 true:
String a = "Java";
String b = "Java";
System.out.println(a == b); // 可能看到 true,但不要用它比较内容
业务代码统一使用:
a.equals(b)
a.equalsIgnoreCase(b)
避免变量可能为 null 时产生空指针,可以把确定非空的常量放左侧:
"yes".equals(answer)
或者先短路检查:
answer != null && answer.equalsIgnoreCase("yes")
8. 去空白、替换与大小写
String raw = " Java Learning ";
raw.strip(); // 删除首尾 Unicode 空白
raw.trim(); // 较旧的首尾空白处理方法
raw.replace("Java", "Spring"); // 按普通文本替换
raw.toUpperCase(); // 转大写
raw.toLowerCase(); // 转小写
Java 11+ 通常优先使用 strip(),它对 Unicode 空白的处理比 trim() 更完整。
replaceAll() 的第一个参数不是普通文字,而是正则表达式:
String normalized = raw.strip().replaceAll("\s+", " ");
\s+ 在正则中表示一个或多个空白,但 Java 字符串中的反斜杠也要转义,所以源码写成 "\s+"。
9. split 拆分字符串
String skills = "Java,Spring Boot,MySQL";
String[] parts = skills.split(",");
结果是一个 String[]:
["Java", "Spring Boot", "MySQL"]
按一个或多个空白拆分:
String[] words = sentence.strip().split("\s+");
注意:split() 参数是正则表达式。点号 . 在正则里表示任意字符。如果真想按点号拆分,必须转义:
String[] parts = "a.b.c".split("\.");
10. StringBuilder
循环中反复使用 +:
String result = "";
for (int i = 1; i <= 1000; i++) {
result = result + i;
}
由于 String 不可变,每次拼接都可能创建新对象。大量循环拼接使用 StringBuilder:
StringBuilder builder = new StringBuilder();
for (int i = 1; i <= 1000; i++) {
builder.append(i);
}
String result = builder.toString();
常用方法:
builder.append("Java"); // 尾部添加
builder.insert(0, "学习"); // 指定位置插入
builder.delete(0, 2); // 删除左闭右开范围
builder.reverse(); // 原地反转
builder.length(); // 当前字符数
builder.toString(); // 转成 String
与 String 不同,StringBuilder 是可变对象,这些操作会改变同一个 builder。
11. 完整源码一:StringBasics.java
import java.util.Arrays;
public class StringBasics {
public static void main(String[] args) {
// String 使用双引号创建;字符串是引用类型,不是基本类型。
String original = " Java Learning ";
// String 不可变。strip() 返回新字符串,不会修改 original。
String cleaned = original.strip();
System.out.println("原字符串:>" + original + "<");
System.out.println("清理后:>" + cleaned + "<");
// length() 是 String 方法,需要括号;数组 length 是属性,不加括号。
System.out.println("字符数量:" + cleaned.length());
// charAt(index) 根据索引取得一个 char,索引同样从 0 开始。
char firstCharacter = cleaned.charAt(0);
System.out.println("第一个字符:" + firstCharacter);
// substring(开始索引, 结束索引) 包含开始,不包含结束。
String firstWord = cleaned.substring(0, 4);
System.out.println("第一个单词:" + firstWord);
// 常用查询方法。
System.out.println("包含 Learning:" + cleaned.contains("Learning"));
System.out.println("Java 的起始索引:" + cleaned.indexOf("Java"));
System.out.println("是否以 Java 开头:" + cleaned.startsWith("Java"));
System.out.println("是否以 ing 结尾:" + cleaned.endsWith("ing"));
// replace() 也返回新字符串。
String replaced = cleaned.replace("Learning", "Backend");
System.out.println("替换结果:" + replaced);
// 比较字符串内容使用 equals(),忽略英文大小写使用 equalsIgnoreCase()。
String first = new String("java");
String second = new String("java");
System.out.println("equals 比内容:" + first.equals(second));
System.out.println("== 比引用:" + (first == second));
System.out.println("忽略大小写:" + first.equalsIgnoreCase("JAVA"));
// split() 的参数是正则表达式;\s+ 表示一个或多个空白字符。
String sentence = "Java Spring Boot";
String[] words = sentence.split("\s+");
System.out.println("拆分结果:" + Arrays.toString(words));
// 循环拼接大量字符串时使用 StringBuilder,避免反复创建 String 对象。
StringBuilder builder = new StringBuilder();
for (int number = 1; number <= 3; number++) {
builder.append("第").append(number).append("课");
if (number < 3) {
builder.append("、");
}
}
// StringBuilder 可变;最后用 toString() 得到普通 String。
String result = builder.toString();
System.out.println("拼接结果:" + result);
}
}
执行流程
strip()产生新字符串,打印边界符号>和<可以直观看见原字符串的空格仍存在。- 使用
length()、charAt()、substring()和查询方法读取字符串。 - 两次
new String()创建内容相同但不同的对象,从而清楚展示equals()和==的区别。 split("\s+")把连续空格视为一个分隔区域。StringBuilder链式调用append(),最终只在需要普通字符串时调用toString()。
12. 完整源码二:TextAnalyzer.java
import java.util.Arrays;
import java.util.Scanner;
public class TextAnalyzer {
public static void main(String[] args) {
Scanner scanner = new Scanner(System.in);
System.out.println("请输入一段英文或中英文混合文本:");
String input = scanner.nextLine();
// 先规范空白,再把同一结果交给多个分析方法。
String normalized = normalizeWhitespace(input);
int letterCount = countLetters(normalized);
int digitCount = countDigits(normalized);
int wordCount = countWords(normalized);
String reversed = reverse(normalized);
System.out.println();
System.out.println("===== 文本分析 =====");
System.out.println("规范文本:" + normalized);
System.out.println("总字符数:" + normalized.length());
System.out.println("字母数量:" + letterCount);
System.out.println("数字数量:" + digitCount);
System.out.println("按空白拆分的词数:" + wordCount);
System.out.println("反转结果:" + reversed);
if (!normalized.isEmpty()) {
String[] words = normalized.split("\s+");
System.out.println("拆分后的内容:" + Arrays.toString(words));
}
scanner.close();
}
public static String normalizeWhitespace(String text) {
if (text == null) {
return "";
}
return text.strip().replaceAll("\s+", " ");
}
public static int countLetters(String text) {
int count = 0;
for (int index = 0; index < text.length(); index++) {
char current = text.charAt(index);
if (Character.isLetter(current)) {
count++;
}
}
return count;
}
public static int countDigits(String text) {
int count = 0;
for (int index = 0; index < text.length(); index++) {
char current = text.charAt(index);
if (Character.isDigit(current)) {
count++;
}
}
return count;
}
public static int countWords(String text) {
if (text == null || text.isBlank()) {
return 0;
}
return text.strip().split("\s+").length;
}
public static String reverse(String text) {
return new StringBuilder(text).reverse().toString();
}
}
TextAnalyzer 语法与流程
nextLine()读取整行,包括中间空格;如果用next(),遇到第一个空格就会停止。normalizeWhitespace()把各种连续空白统一成一个普通空格,后续方法得到稳定输入。Character.isLetter()能识别比a-z更广的 Unicode 字母,包括中文字符。Character.isDigit()判断字符是否为数字。countWords()使用短路保护null;这里的“词”只是按空白拆分,不等同于严格的中文分词。new StringBuilder(text).reverse().toString()先创建可变缓冲区,原地反转,再转回不可变字符串。
13. 独立练习完整答案:StringPractice.java
需求:
- 清理用户名首尾空格。
- 用户名长度必须为 4–12,只允许字母、数字和下划线。
- 将 11 位手机号中间四位替换为星号。
- 把逗号分隔的技能转换成编号列表。
完整答案:
public class StringPractice {
public static void main(String[] args) {
// 模拟用户注册与简历技能文本。
String rawUsername = " java_learner ";
String phone = "13812345678";
String rawSkills = "Java, Spring Boot, MySQL, Git";
String username = rawUsername.strip();
boolean validUsername = isValidUsername(username);
String maskedPhone = maskPhone(phone);
String skillList = formatSkills(rawSkills);
System.out.println("清理后的用户名:" + username);
System.out.println("用户名是否合法:" + validUsername);
System.out.println("脱敏手机号:" + maskedPhone);
System.out.println("技能列表:");
System.out.println(skillList);
}
public static boolean isValidUsername(String username) {
// 短路保护:username 为 null 时,后面的 length() 不会执行。
if (username == null || username.length() < 4 || username.length() > 12) {
return false;
}
// 只允许 Unicode 字母、数字和下划线。
for (int index = 0; index < username.length(); index++) {
char current = username.charAt(index);
boolean allowed = Character.isLetterOrDigit(current) || current == '_';
if (!allowed) {
return false;
}
}
return true;
}
public static String maskPhone(String phone) {
// 手机号不是 11 位时不做截取,避免 substring() 索引越界。
if (phone == null || phone.length() != 11) {
return "手机号格式无效";
}
// substring(0, 3) 取得索引 0、1、2;substring(7) 取得索引 7 到末尾。
return phone.substring(0, 3) + "****" + phone.substring(7);
}
public static String formatSkills(String rawSkills) {
if (rawSkills == null || rawSkills.isBlank()) {
return "暂无技能";
}
String[] skills = rawSkills.split(",");
StringBuilder builder = new StringBuilder();
for (int index = 0; index < skills.length; index++) {
String skill = skills[index].strip();
builder.append(index + 1)
.append(". ")
.append(skill);
if (index < skills.length - 1) {
builder.append(System.lineSeparator());
}
}
return builder.toString();
}
}
独立练习详解
用户名校验
先检查 null 和长度,再循环检查字符。发现第一个非法字符就立即返回 false,不必检查剩余字符。全部通过后才返回 true。
注意:Character.isLetterOrDigit() 支持 Unicode 字母。若业务明确只允许 ASCII 英文字母,需要写更严格的范围判断或正则表达式。
手机号脱敏
13812345678 的索引:
索引:0 1 2 3 4 5 6 7 8 9 10
字符:1 3 8 1 2 3 4 5 6 7 8
substring(0, 3)得到138。- 中间拼接
****。 substring(7)得到索引 7 到末尾的5678。- 最终结果为
138****5678。
长度检查必须在截取之前,否则短字符串会越界。
技能列表格式化
split(",") 得到字符串数组。每项可能带前导空格,因此使用 strip()。StringBuilder 在循环中追加编号、标点和技能名。只在非最后一项后追加换行,避免末尾多出空行。
14. 常见错误
用 == 比较内容
if (input == "yes") { }
应改为:
if ("yes".equalsIgnoreCase(input)) { }
忘记接住不可变操作的返回值
name.strip(); // 结果丢失
应改为:
name = name.strip();
substring 越界
在截取固定位置前检查字符串长度。
对 null 调用方法
name.isBlank(); // name 为 null 时抛出 NullPointerException
使用短路:
name == null || name.isBlank()
错误理解 split
split() 使用正则表达式。按点号、竖线等特殊字符拆分时需要正确转义。
在循环中大量使用 String +
少量拼接使用 + 很清楚;大量循环拼接使用 StringBuilder,避免创建大量中间字符串。
15. 检查题与答案
问题
- 为什么调用
text.toUpperCase()后,原字符串通常没有变化? isEmpty()与isBlank()有什么区别?"Java".substring(1, 3)得到什么?- 比较字符串内容应该使用
==还是equals()? indexOf()找不到目标时返回什么?- 为什么
"yes".equals(answer)比answer.equals("yes")更能防御null? split("\s+")中的\s+表示什么?StringBuilder和String在可变性上有什么区别?substring(0, 3)包含索引 3 吗?- 为什么手机号脱敏前必须检查长度?
name == null || name.isBlank()如何利用短路?- 增加大量循环文本时为什么推荐
StringBuilder?
答案
String不可变,该方法返回新字符串,不会修改原对象;需要保存返回值。isEmpty()只判断长度是否为 0;isBlank()还把只含空白的字符串视为空白。"av",因为包含索引 1 和 2,不包含结束索引 3。- 使用
equals();忽略英文大小写时用equalsIgnoreCase()。 -1。- 字面量
"yes"一定非空;即使answer为null,equals()也只返回false。反过来调用会产生空指针。 - 一个或多个空白字符;Java 字符串用双反斜杠表达正则中的单个反斜杠。
String不可变;StringBuilder可在同一对象上追加、删除、插入和反转。- 不包含,
substring的结束索引是排他的。 - 短字符串不具备需要的索引,直接截取会产生
StringIndexOutOfBoundsException。 - 左侧为
true时整体已经为真,右侧被跳过,所以不会对null调用方法。 String每次拼接可能创建中间对象;StringBuilder在同一可变缓冲区中累计内容,更适合大量操作。
16. 本课完成标准
- 能解释
String不可变的准确含义。 - 能正确使用
equals()、substring()、split()和charAt()。 - 能安全处理
null、空字符串和非法索引。 - 能使用循环完成字符统计。
- 能使用
StringBuilder构建多行文本。 - 能读懂并修改用户名校验、手机号脱敏和技能格式化程序。
下一课正式进入面向对象:类与对象、字段、构造方法、this、封装、getter/setter,以及一个完整的学生管理模型。