CodeRoadMap
首页路线题库资源库文章社区
CodeRoadMap

程序员的学习成长路线图。内容免费开放,登录后可同步路线图与课时进度。

由 程序员柏风 独立维护 · 关于作者

学习

  • 路线图
  • 课程
  • 面试题
  • 文章
  • 我

更多

  • 资源
  • 社区
  • 关于作者
  • 内容说明

© 2026 CodeRoadMap

津ICP备2026012044号-1·津公网安备12011002026155号·coderoadmap@126.com
Java 语言基础/课时/字符串
RawBlame
课时目录14 节

Java 语言基础

  1. 01Java 与 JVM 是什么
  2. 02基本语法与 main
  3. 03基本类型与引用类型
  4. 04条件与循环
  5. 05方法与参数
  6. 06数组
  7. 07类与封装
  8. 08继承接口与多态
  9. 09集合框架入门
  10. 10字符串
  11. 11异常处理
  12. 12文件 IO 入门
  13. 13Maven 与项目结构
  14. 14综合练习与 dsa-java 分工

字符串

String 不可变、StringBuilder、formatted。

30 分钟

字符串

这一课你将能做到

  1. 流畅使用 String 的常用 API(查找、截取、替换、拆分、判断),处理本站后端日常的报文、日志、参数清洗
  2. 用一句话讲清 String 不可变的设计动机,并永远正确地区分 == 与 equals
  3. 在拼接、构建长文本时正确选择 String / StringBuilder / 文本块,包括生成 SQL、日志、JSON 时的安全习惯

为什么要学

字符串是后端世界出现频率最高的数据形态:HTTP 参数是字符串、MySQL 返回前要拼 JSON、日志是字符串、Redis 里存的值是字符串。本站后端有大量字符串操作场景:

  • 校验请求参数:StringUtils.isBlank(username) 的原生写法
  • 生成缓存 key:"user:info:" + userId(Redis 键命名规范)
  • 解析 Excel/CSV 导入:split(",")
  • 记录接口日志:占位符 {} 风格

这一课还有一个面试百年老题:String s = new String("abc") 创建了几个对象?理解了它,字符串常量池你就通了。

先建立图像

String 是「不可变的字符序列」:

s = "hello"    →  堆里的字符串对象 "hello"
     │
     └─ s.toUpperCase() 不会改 "hello",而是 new 一个 "HELLO" 返回
        "hello" 本体永远原封不动

字符串常量池(专门放字符串字面量的缓存区):
  String a = "hi";
  String b = "hi";    → a 和 b 指向池中同一个 "hi"(省内存)
  String c = new String("hi");  → 强制 new 堆中新对象,地址与 a 不同

跟着做

步骤 1:常用 API 全家福

新建 StringApiDemo.java:

public class StringApiDemo {
    public static void main(String[] args) {
        String s = "  Hello, CodeRoadMap  ";

        // 去空白:trim 只去 ASCII 空白;strip(JDK 11+)支持全角空格等 Unicode 空白
        System.out.println("[" + s.strip() + "]");     // [Hello, CodeRoadMap]

        String t = s.strip();
        System.out.println(t.length());                // 18
        System.out.println(t.charAt(0));               // H(下标访问,越界抛异常)
        System.out.println(t.indexOf("Code"));         // 7(找不到返回 -1)
        System.out.println(t.contains("Road"));        // true
        System.out.println(t.startsWith("Hello"));     // true
        System.out.println(t.toUpperCase());           // 全大写(返回新串)
        System.out.println(t.substring(7));            // CodeRoadMap(从下标 7 到末尾)
        System.out.println(t.substring(0, 5));         // Hello(含头不含尾)
        System.out.println(t.replace("CodeRoadMap", "Java"));  // Hello, Java

        // 判空三兄弟(后端参数校验天天用)
        System.out.println("".isEmpty());        // true:长度 0
        System.out.println("   ".isBlank());     // true:只有空白
        System.out.println(" a ".isBlank());     // false:有实际内容

        // 拆分:正则语法,特殊字符要转义
        String csv = "tom,20,beijing";
        String[] parts = csv.split(",");
        for (String p : parts) System.out.print(p + " | ");   // tom | 20 | beijing |
        System.out.println();
        System.out.println("a.b.c".split("\\."));  // 点号是正则元字符,必须 \\.
    }
}

步骤 2:== 与 equals 世纪之争

public class StringEqualsDemo {
    public static void main(String[] args) {
        String a = "hi";                    // 字面量 → 常量池
        String b = "hi";                    // 复用池中同一对象
        String c = new String("hi");        // 强制堆中新建
        String d = "h" + "i";               // 编译期常量折叠 → 池中同一对象

        System.out.println(a == b);         // true:同一对象
        System.out.println(a == c);         // false:堆中对象地址不同
        System.out.println(a.equals(c));    // true:内容相等
        System.out.println(a == d);         // true:编译期就合并成 "hi"

        // 最危险的真实场景:运行期拼接的结果不在池里
        String prefix = "h";
        String e = prefix + "i";            // 运行期拼接 → 新对象
        System.out.println(a == e);         // false!
        System.out.println(a.equals(e));    // true

        // 规范写法:常量在左,防 NPE
        String input = null;
        // System.out.println(input.equals("admin"));      // NPE!
        System.out.println("admin".equals(input));          // false,安全
    }
}

铁律:字符串比较永远用 equals。== 只在比「是不是同一个对象」,碰巧 true 的场景(小字面量)反而是最大的坑。

步骤 3:不可变性实证

public class ImmutableDemo {
    public static void main(String[] args) {
        String s = "hello";
        s.toUpperCase();                 // 返回值被丢弃!s 不变
        System.out.println(s);           // hello

        s = s.toUpperCase();             // 必须接住返回的新对象
        System.out.println(s);           // HELLO

        // String 的所有"修改"方法都是返回新串,原串永远不变
        String t = "abc";
        t.replace('a', 'x');
        System.out.println(t);           // abc
    }
}

步骤 4:StringBuilder 高效拼接

public class StringBuilderDemo {
    public static void main(String[] args) {
        // 场景 1:循环内拼接 → 必须用 StringBuilder
        long start = System.currentTimeMillis();
        StringBuilder sb = new StringBuilder();
        for (int i = 0; i < 100_000; i++) {
            sb.append(i).append(",");
        }
        System.out.println("StringBuilder 耗时: " + (System.currentTimeMillis() - start) + "ms");

        start = System.currentTimeMillis();
        String s = "";
        for (int i = 0; i < 20_000; i++) {
            s += i;              // 每次循环都 new 新对象 + 复制全部内容,O(n²)
        }
        System.out.println("String += 耗时: " + (System.currentTimeMillis() - start) + "ms");

        // 常用方法
        sb.setLength(0);         // 清空复用
        sb.append("redis").append(":").append("user").append(":").append(1001);
        System.out.println(sb);              // redis:user:1001
        System.out.println(sb.reverse());    // 倒序(刷题常用)

        // 场景 2:格式化输出(对照 C 的 printf)
        String line = "%s 的订单 %d 总金额 %.2f".formatted("tom", 10086, 199.5);
        System.out.println(line);
        System.out.printf("ID=%05d%n", 42);  // ID=00042(补零)
    }
}

结论:单次拼接 + 随意(编译器自动优化);循环内拼接必须 StringBuilder;StringBuffer 是老线程安全版,单线程无脑用 Builder。

步骤 5:文本块(JDK 15+)

public class TextBlockDemo {
    public static void main(String[] args) {
        // 三引号多行字符串:缩进以 """ 的位置为基准
        String json = """
                {
                  "username": "tom",
                  "age": 20
                }
                """;
        System.out.println(json);

        // 对照传统写法(换行 + 转义引号,可读性灾难):
        String old = "{\n  \"username\": \"tom\",\n  \"age\": 20\n}";
        System.out.println(json.equals(old));   // true

        // 本站后端场景:写测试 SQL、HTML 模板、内嵌配置
        String sql = """
                SELECT id, username, status
                FROM user
                WHERE status = 0
                """;
        System.out.println(sql);
    }
}

步骤 6:后端实战——缓存 key 与参数清洗

public class BackendStringDemo {
    public static void main(String[] args) {
        // Redis 缓存 key 规范:业务:对象:id(冒号分层是社区惯例)
        long userId = 1001;
        String cacheKey = "roadmap:user:info:" + userId;
        System.out.println(cacheKey);        // roadmap:user:info:1001

        // 用户输入清洗:trim + 判空 + 统一比较
        java.util.Scanner sc = new java.util.Scanner(System.in);
        System.out.print("输入用户名: ");
        String input = sc.nextLine().strip();
        if (input.isEmpty()) {
            System.out.println("用户名不能为空");
        } else if ("admin".equalsIgnoreCase(input)) {
            System.out.println("管理员登录(忽略大小写比较)");
        } else {
            System.out.println("普通用户: " + input);
        }
    }
}

讲清楚要点

  1. String 不可变的三个理由:① 安全——作为 HashMap key、文件路径、类名不会被中途篡改;② 线程安全——只读对象天然可共享;③ 常量池复用成为可能——相同字面量全局一份。
  2. 常量池:字面量("hi")进池复用;new String 强制堆中新对象;运行期拼接结果是新对象不进池(intern() 可手动入池,少用)。
  3. equals 是唯一正确的比较;equalsIgnoreCase 处理大小写不敏感场景(邮箱、验证码)。
  4. 拼接选型:静态少量拼接用 +;循环拼接用 StringBuilder;formatted/printf 管排版;多行文本用文本块。
  5. split 的正则本质:参数是正则表达式,. | * 等元字符必须转义 \\. \\|;想按多个分隔符切:split("[,;]")。
  6. 安全红线:拼接 SQL 字符串 = SQL 注入漏洞("SELECT * FROM user WHERE name = '" + input + "'"),本站后端一律用 MyBatis/JDBC 的参数占位符 ?,字符串只用于日志和展示。

你可能会懵的地方

  • String s = new String("abc") 创建几个对象? 最多 2 个:字面量 "abc" 若池中不存在则池中 1 个 + 堆中 new 的 1 个;池中已存在则只 new 1 个。这题考的是常量池机制,答出「1 或 2,取决于池中是否已有」才是满分。

  • String、StringBuilder、StringBuffer 怎么选? String:不可变,存值传值场景;StringBuilder:可变、非线程安全、单线程拼接首选;StringBuffer:可变、方法加 synchronized、多线程才需要(后端拼接场景几乎用不到它)。

  • 字符串的 + 到底怎么实现的? 编译器把 a + b 翻译成 StringBuilder 操作(JDK 9+ 用 invokedynamic 优化成 StringConcatFactory)。循环里每次 += 都生成新对象,这就是性能差异的来源。

  • 为什么 a == d("h" + "i")是 true? "h" 和 "i" 都是编译期常量,编译器直接折叠成 "hi" 放进常量池。但只要有一方是变量(运行期才知道值),就变成运行期拼接 → 新对象 → == 为 false。

  • intern() 是什么? 把堆中字符串放入常量池并返回池中引用。JDK 7 后池在堆里,intern 用得极少;数据库连接池、字符串高度重复的大数据处理偶尔用。

  • 为什么推荐 "常量".equals(变量) 而不是反过来? 变量为 null 时,null.equals(...) 直接 NPE;常量在左则返回 false。Yoda 条件,参数校验的标准防御姿势。

错法对照

错法问题正解
if (name == "admin")堆中对象恒 falseif ("admin".equals(name))
s.toUpperCase(); 以为改了 s不可变,返回值丢了s = s.toUpperCase();
循环里 result += xO(n²),10 万次明显卡顿循环外建 StringBuilder,循环内 append
"a.b.c".split(".")点号是正则元字符,得到空数组"a.b.c".split("\\.")
if (input.equals("admin")) 且 input 可能为 nullNPE"admin".equals(input)
用字符串拼接拼 SQLSQL 注入漏洞预编译参数占位符 ? / MyBatis #{}
拿 == 比 char 数组内容比地址Arrays.equals(charArr1, charArr2)

卡点急救

  1. split 得到的数组是空的:分隔符是正则元字符(.、|、*、+),加双反斜杠转义;或分隔符在结尾时默认丢弃空串,加参数 split(",", -1) 保留。
  2. 改了字符串没生效:所有「修改类」方法都返回新串,必须接收返回值。
  3. substring 报 StringIndexOutOfBoundsException:范围是 [begin, end),end 超过 length 或 begin > end 都会炸;边界先打印 length() 核对。
  4. 拼接性能差:确认拼接是否在循环内;超过万次量级换 StringBuilder,预知长度时给构造器传初始容量。
  5. 中文乱码:读写文件/网络流时统一指定 UTF-8:new String(bytes, StandardCharsets.UTF_8)(下一课 IO 详讲)。

面试常问

  • String 为什么设计成不可变? 安全(key/路径/类加载名不可篡改)、线程安全(只读共享)、常量池复用、hashCode 可缓存(String 缓存 hash,作为 HashMap key 时重复计算成本低)。

  • == 与 equals 的区别? == 比地址(引用是否同一对象);equals 默认同 ==,String 重写为逐字符比内容。字符串比较必须 equals。

  • String、StringBuilder、StringBuffer 的区别? 可变性:不可变 / 可变 / 可变;线程安全:天然安全 / 不安全 / synchronized 安全;性能:循环拼接 String 最差;选择:默认 String,拼接用 Builder,多线程共享拼接才用 Buffer。

  • 字符串常量池是什么? JVM 里缓存字符串字面量的区域(JDK 7+ 位于堆中)。相同字面量全局复用一份,new String 绕过复用强制建新对象。

  • "abc".substring(1) 会复制数组吗?(内存细节题) JDK 6 会共享原数组(有内存泄漏隐患);JDK 7u6 起改为复制,substring 返回独立小对象。答出「新版是复制」即可。

自测三问

  1. String a="hi"; String b=new String("hi"); — a==b 和 a.equals(b) 各是什么?为什么?
  2. 循环 10 万次拼接用 += 和 StringBuilder 差多少量级?底层原因是什么?
  3. "a|b".split("|") 的结果是什么?怎么改才对?

小结一句话

String 不可变、进常量池、比较永远 equals(常量在左防 NPE);循环拼接用 StringBuilder、正则元字符要转义、多行用文本块;拼 SQL 是红线——一律参数占位符。

延伸

  • Oracle:字符与字符串
  • 下一课:异常处理——try/catch/finally、受检与非受检的边界、自定义业务异常,别再让 500 直接打到用户脸上。
加载本节练习…
← 上一课集合框架入门下一课 →异常处理