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"falsenew 强制新建
"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
比较strcmpequals / 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 标准库没有的开箱即用能力


练习

题号题目链接知识点
P1014Catalan数https://www.luogu.com.cn/problem/P1014字符串、数学