第 07 课:String 与 StringBuilder

0 阅读11分钟

第 07 课:String 与 StringBuilder

本课目标

学完后,你应该能够:

  1. 理解 String 是引用类型,而且字符串对象不可变。
  2. 正确使用 equals() 比较内容,不用 == 代替。
  3. 使用长度、索引、截取、查询、替换、大小写转换和去空白方法。
  4. 使用 split() 把字符串拆成数组,并理解它接收正则表达式。
  5. 使用 charAt() 和循环逐字符分析文本。
  6. 使用 StringBuilder 高效构建字符串。
  7. 安全处理 null、空字符串、空白字符串和非法索引。
  8. 完成用户名校验、手机号脱敏和技能列表格式化。

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()
""truetrue
" "falsetrue
"Java"falsefalse

数组使用 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);
    }
}

执行流程

  1. strip() 产生新字符串,打印边界符号 >< 可以直观看见原字符串的空格仍存在。
  2. 使用 length()charAt()substring() 和查询方法读取字符串。
  3. 两次 new String() 创建内容相同但不同的对象,从而清楚展示 equals()== 的区别。
  4. split("\s+") 把连续空格视为一个分隔区域。
  5. 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

需求:

  1. 清理用户名首尾空格。
  2. 用户名长度必须为 4–12,只允许字母、数字和下划线。
  3. 将 11 位手机号中间四位替换为星号。
  4. 把逗号分隔的技能转换成编号列表。

完整答案:

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. 检查题与答案

问题

  1. 为什么调用 text.toUpperCase() 后,原字符串通常没有变化?
  2. isEmpty()isBlank() 有什么区别?
  3. "Java".substring(1, 3) 得到什么?
  4. 比较字符串内容应该使用 == 还是 equals()
  5. indexOf() 找不到目标时返回什么?
  6. 为什么 "yes".equals(answer)answer.equals("yes") 更能防御 null
  7. split("\s+") 中的 \s+ 表示什么?
  8. StringBuilderString 在可变性上有什么区别?
  9. substring(0, 3) 包含索引 3 吗?
  10. 为什么手机号脱敏前必须检查长度?
  11. name == null || name.isBlank() 如何利用短路?
  12. 增加大量循环文本时为什么推荐 StringBuilder

答案

  1. String 不可变,该方法返回新字符串,不会修改原对象;需要保存返回值。
  2. isEmpty() 只判断长度是否为 0;isBlank() 还把只含空白的字符串视为空白。
  3. "av",因为包含索引 1 和 2,不包含结束索引 3。
  4. 使用 equals();忽略英文大小写时用 equalsIgnoreCase()
  5. -1
  6. 字面量 "yes" 一定非空;即使 answernullequals() 也只返回 false。反过来调用会产生空指针。
  7. 一个或多个空白字符;Java 字符串用双反斜杠表达正则中的单个反斜杠。
  8. String 不可变;StringBuilder 可在同一对象上追加、删除、插入和反转。
  9. 不包含,substring 的结束索引是排他的。
  10. 短字符串不具备需要的索引,直接截取会产生 StringIndexOutOfBoundsException
  11. 左侧为 true 时整体已经为真,右侧被跳过,所以不会对 null 调用方法。
  12. String 每次拼接可能创建中间对象;StringBuilder 在同一可变缓冲区中累计内容,更适合大量操作。

16. 本课完成标准

  • 能解释 String 不可变的准确含义。
  • 能正确使用 equals()substring()split()charAt()
  • 能安全处理 null、空字符串和非法索引。
  • 能使用循环完成字符统计。
  • 能使用 StringBuilder 构建多行文本。
  • 能读懂并修改用户名校验、手机号脱敏和技能格式化程序。

下一课正式进入面向对象:类与对象、字段、构造方法、this、封装、getter/setter,以及一个完整的学生管理模型。