乐途乐途
主页
  • 计算机基础

    • TCP/IP
    • Linux
    • HTTP
  • 数据库

    • SQL
    • MySQL 5.7
  • 编程语言

    • C
    • C++
    • Java SE
    • Python2
    • Python3
  • 数据格式

    • JSON
    • XML
  • 认证与安全

    • JWT
  • 工具

    • Markdown
  • Git

    • GitFlow
  • Quartz

    • Quartz
  • Java

    • Maven 入门
    • Maven 进阶
    • MyBatis
    • Spring
    • Spring MVC
  • Java

    • Spring Boot
    • Spring Cloud
    • Spring Cloud Alibaba
    • Spring Security
    • Spring AI
    • Spring Batch
    • Kafka
    • Java 设计模式
  • 缓存

    • Redis
  • 搜索引擎

    • Elasticsearch
  • 分布式协调

    • ZooKeeper
联系
阿里云
主页
  • 计算机基础

    • TCP/IP
    • Linux
    • HTTP
  • 数据库

    • SQL
    • MySQL 5.7
  • 编程语言

    • C
    • C++
    • Java SE
    • Python2
    • Python3
  • 数据格式

    • JSON
    • XML
  • 认证与安全

    • JWT
  • 工具

    • Markdown
  • Git

    • GitFlow
  • Quartz

    • Quartz
  • Java

    • Maven 入门
    • Maven 进阶
    • MyBatis
    • Spring
    • Spring MVC
  • Java

    • Spring Boot
    • Spring Cloud
    • Spring Cloud Alibaba
    • Spring Security
    • Spring AI
    • Spring Batch
    • Kafka
    • Java 设计模式
  • 缓存

    • Redis
  • 搜索引擎

    • Elasticsearch
  • 分布式协调

    • ZooKeeper
联系
阿里云
  • 学习路径
  • 第1章 Java概述与环境搭建

    • Java概述与环境搭建
    • Java语言概述
    • 解释型语言与编译型语言对比
    • JDK安装与配置
    • JDK、JRE、JVM 详解
    • HelloWorld程序详解
    • IDE 介绍
  • 第2章 标识符与基本数据类型

    • 章节导读
    • 变量概述
    • 常量概述
    • 基本类型与包装类
    • 字节型 byte
    • 短整型 short
    • 整型 int
    • 长整型 long
    • 单精度浮点型 float
    • 双精度浮点型 double
    • 字符型 char
    • 布尔型 boolean
    • 类型转换
  • 第3章 运算符与表达式

    • 章节导读
    • 算术运算符
    • 赋值运算符
    • 关系运算符
    • 逻辑运算符
    • 位运算符
    • 条件运算符
    • 运算符优先级
    • 表达式
  • 第4章 流程控制

    • 章节导读
    • 常见的程序运行流程
    • if-else 选择结构
    • switch 多分支选择
    • while 循环
    • do-while 循环
    • for 循环
    • break 与 continue
  • 第5章 数组

    • 章节导读
    • 一维数组
    • 多维数组
    • Arrays 工具类
  • 第6章 类与对象

    • 章节导读
    • 类与对象
    • 方法定义与调用
    • 构造方法
    • 封装
    • 访问修饰符
    • package 与 import
    • static 关键字
    • this 关键字
    • 参数传递 详解
    • 枚举
    • 成员内部类
    • 局部内部类
    • 静态内部类
    • 匿名内部类
  • 第7章 接口与继承

    • 章节导读
    • 继承
    • super 关键字
    • final 关键字
    • 多态
    • 向上转型与向下转型
    • 抽象类
    • 接口
    • 抽象类与接口对比
  • 第8章 注解

    • 章节导读
    • 注解基础
    • 元注解详解
    • 自定义注解
  • 第9章 常用类

    • 章节导读:Java 常用类
    • Object 类:万类之祖
    • 包装类:基本类型的对象化
    • String:不可变的字符串
    • StringBuffer:线程安全的可变字符串
    • StringBuilder:可变的字符串构建器
    • Math:数学运算工具类
    • Random:伪随机数生成器
    • 大数值运算 详解
    • 日期时间API 详解
  • 第10章 异常机制

    • 章节导读
    • 异常体系与分类
    • try-catch-finally
    • try-with-resources
    • throws 与 throw
    • 自定义异常
  • 第11章 泛型

    • 章节导读
    • 泛型基础
    • 通配符与PECS原则
    • 类型擦除
  • 第12章 集合框架

    • 章节导读
    • 集合框架概述
    • ArrayList
    • LinkedList
    • HashMap 详解
    • LinkedHashMap 详解
    • TreeMap 详解
    • HashSet
    • TreeSet 详解
    • TreeSet 与 Comparable
    • Collections 工具类详解
  • 第13章 IO流

    • 章节导读
    • IO流概述
    • 字节流
    • 字符流
    • 缓冲流
    • 转换流 详解
    • 序列化 详解
    • NIO与Files 详解
    • NIO与Files工具类
  • 第14章 多线程与并发

    • 第十六章 多线程与并发 —— 章节导读
    • 线程基础详解
    • synchronized 详解
    • Lock 与显式锁详解
    • volatile 详解
    • wait 与 notify 详解
    • ThreadLocal详解
    • 原子类详解
    • 并发工具类详解
    • 线程池详解
  • 第15章 反射

    • 章节导读
    • 反射概述与 Class 对象
    • Constructor 与对象创建
    • Field与Method详解
    • 反射应用详解
  • 第16章 JDK8新特性

    • 章节导读
    • Lambda 表达式
    • Stream API 基础
    • Stream API 高级详解
    • Optional 详解
    • 新日期时间API详解
  • 第17章 JDK9-11新特性

    • 章节导读
    • 模块化系统 — Project Jigsaw(JDK 9)
    • var 局部变量类型推断(JDK 10)
    • 集合工厂方法与增强(JDK 9 / 10 / 11)
    • 接口增强:private 方法(JDK 9)
    • Stream API 增强(JDK 9)
    • Optional 增强(JDK 9 / 10 / 11)
    • String 新增方法(JDK 11)
    • HTTP Client 与 Files 增强(JDK 11)
    • 直接运行 Java 源文件 — JEP 330(JDK 11)
  • 第18章 JDK12-17新特性

    • 章节导读
    • Switch 表达式(JDK 12 预览 / JDK 14 正式)
    • 文本块 Text Blocks(JDK 13 预览 / JDK 15 正式)
    • Records 记录类(JDK 14 预览 / JDK 16 正式)
    • 密封类 Sealed Classes(JDK 15 预览 / JDK 17 正式)
    • instanceof 模式匹配(JDK 14 预览 / JDK 16 正式)
    • Switch 模式匹配 — Pattern Matching for switch(JDK 17 预览 / JDK 21 正式)
    • Helpful NPE 与 String 增强(JDK 12 / JDK 14 / JDK 15)
    • Stream 增强(JDK 12 / JDK 16)
    • 日期时间增强 — Day Period 支持(JDK 16)
  • 第19章 JDK18-21新特性

    • 章节导读
    • 虚拟线程(JDK 19 预览 / JDK 20 第二预览 / JDK 21 正式)
    • 序列集合(JDK 21 正式)
    • Switch 模式匹配(JDK 17 预览 / JDK 18 第二预览 / JDK 20 第四预览 / JDK 21 正式)
    • Record 模式匹配(JDK 19 预览 / JDK 20 第二预览 / JDK 21 正式)
    • 未命名模式与变量(JDK 21 预览 / JDK 22 正式)
  • 第20章 JDK 22-25 新特性

    • 章节导读
    • 字符串模板(JDK 22 预览 / JDK 23 第二预览 / JDK 24 第三预览)
    • Stream Gatherers(JDK 22 预览 / JDK 24 第二预览)
    • 隐式声明类与实例方法(JDK 23 预览 / JDK 24 第二预览)
    • 原始类型模式匹配(JDK 24 预览)
  • 附录

    • Java 核心知识点
    • Java SE 专业术语
    • Java特性索引(JDK 8 → 25)

String:不可变的字符串

本章定位:String 是 Java 中使用频率最高的类,没有之一。理解它的不可变性、常量池机制和拼接原理,是区分"会用 Java"和"懂 Java"的分水岭。


小崔的困惑

飞翔科技技术部新入职的小崔正在写员工管理模块。他写了这样一段代码来比较两个员工的姓名:

String name1 = "大翔";
String name2 = new String("大翔");
System.out.println(name1 == name2);  // false ???

"明明值一样,为什么 == 比较返回 false?"小崔百思不得其解。架构师白歌走过来,说:"这涉及到 String 的不可变性和常量池机制,来,我们从头讲起。"


String 的不可变性

定义与源码分析

String 类的不可变性由三个关键设计保证:

设计要素源码体现作用
final classpublic final class String禁止子类化,防止通过继承破坏不可变性
private final char[]private final char value[];(JDK 8)字符数组私有且不可变引用
所有方法返回新对象substring()、replace() 等均不修改原数组任何"修改"操作都产生新的 String 对象

JDK 9 变化:JDK 9 起 char[] 改为 byte[],配合 coder 字段标识编码(LATIN1 / UTF16),称为"紧凑字符串"(Compact Strings)。但不变性的设计思想保持不变。本章基于 JDK 8。

不可变性的好处

  1. 线程安全:多个线程共享同一个 String 对象无需同步
  2. Hash 缓存:hashCode() 只计算一次并缓存(字段 hash),提高 HashMap 性能
  3. 字符串常量池:可安全地复用相同的字符串字面量
  4. 安全:类加载器、网络连接参数等敏感字符串不会被意外修改

String 常量池(String Pool)

什么是字符串常量池?

字符串常量池是 JVM 堆内存中的一块特殊区域(JDK 7+ 移至堆中),用于存储字符串字面量。它的核心设计思想是复用:相同的字符串只存一份。

字面量创建 vs new 创建

创建方式示例对象数量是否入池
字面量String s = "hello";0~1 个(池中无则创建)自动入池
newString s = new String("hello");1~2 个(池中无则先在池中创建字面量,再在堆中创建 new 对象)堆对象不入池
intern()s.intern();0~1 个手动入池

完整示例一:字面量与 new 的内存分析

场景

小崔需要存储飞翔科技三位创始人的姓名,白歌让他通过画内存图理解不同创建方式的行为。

代码

public class StringPoolDemo {
    public static void main(String[] args) {
        // ========== 字面量创建 ==========
        String a = "飞翔科技";      // ① 常量池创建 "飞翔科技"
        String b = "飞翔科技";      // ② 常量池已存在,直接复用

        System.out.println("a == b: " + (a == b));           // true(同一引用)

        // ========== new 创建 ==========
        String c = new String("飞翔科技");  // ③ 堆中新建对象,常量池已有则不复建
        String d = new String("飞翔科技");  // ④ 堆中再建一个对象

        System.out.println("a == c: " + (a == c));           // false(常量池 vs 堆)
        System.out.println("c == d: " + (c == d));           // false(两个不同堆对象)

        // ========== intern() 手动入池 ==========
        String e = c.intern();            // ⑤ 将 c 的值试图放入常量池,返回池中引用

        System.out.println("a == e: " + (a == e));           // true(都指向常量池)
        System.out.println("c == e: " + (c == e));           // false(堆 vs 常量池)

        // ========== equals() 比较内容 ==========
        System.out.println("a.equals(c): " + a.equals(c));   // true(内容相同)
    }
}

运行输出

a == b: true
a == c: false
c == d: false
a == e: true
c == e: false
a.equals(c): true

内存图

结论:字面量创建的字符串指向常量池,new 创建的字符串指向堆对象。intern() 可以将堆对象"拉回"常量池。


== 与 equals() 的区别

这是 Java 面试中的"送命题",也是小崔踩的第一个坑:

比较方式比较内容String 中的行为
==引用地址(栈中存储的指针)判断是否指向同一对象
equals()内容(String 重写了 equals)判断字符序列是否相同
// String 类重写的 equals() 方法核心逻辑(简化版)
public boolean equals(Object anObject) {
    if (this == anObject) return true;              // 先比较地址,快速返回
    if (anObject instanceof String) {
        String anotherString = (String) anObject;
        int n = value.length;
        if (n == anotherString.value.length) {
            char v1[] = value;
            char v2[] = anotherString.value;
            int i = 0;
            while (n-- != 0) {                       // 逐字符比较
                if (v1[i] != v2[i]) return false;
                i++;
            }
            return true;
        }
    }
    return false;
}

字符串拼接的底层原理

+ 运算符的编译优化

小崔写了这样一段代码做员工信息拼接:

String info = "姓名:" + name + ",部门:" + dept + ",薪资:" + salary;

白歌让他用 javap -c 反编译看看字节码。结果让所有人大跌眼镜——编译器把它变成了:

// 编译器优化后的等价代码
String info = new StringBuilder()
    .append("姓名:")
    .append(name)
    .append(",部门:")
    .append(dept)
    .append(",薪资:")
    .append(salary)
    .toString();

循环拼接的性能陷阱

// ❌ 反例:循环中使用 + 拼接
String result = "";
for (int i = 0; i < 10000; i++) {
    result += i;  // 每次循环都 new StringBuilder() → toString() → 产生大量临时对象!
}
// 实际执行:创建了 10000 个 StringBuilder 对象 + 10000 个 String 对象
// ✅ 正确:循环外使用 StringBuilder
StringBuilder sb = new StringBuilder();
for (int i = 0; i < 10000; i++) {
    sb.append(i);  // 仅一个 StringBuilder,内部数组扩容
}
String result = sb.toString();

String 常用方法大全

速查表格

方法签名功能返回值
int length()返回字符串长度字符个数
char charAt(int index)返回指定索引的字符char
String substring(int begin, int end)截取 [begin, end) 子串新 String
int indexOf(String str)查找子串首次出现位置索引,找不到返回 -1
boolean contains(CharSequence s)是否包含指定序列boolean
String replace(char old, char new)替换所有匹配字符新 String
String[] split(String regex)按正则分割String 数组
String trim()去除首尾空白字符新 String
String toUpperCase()转大写新 String
String toLowerCase()转小写新 String
boolean startsWith(String prefix)是否以指定前缀开头boolean
boolean endsWith(String suffix)是否以指定后缀结尾boolean
String intern()返回常量池中的引用池中 String

完整示例二:飞翔科技员工姓名处理

场景

小崔需要从 HR 系统导出的数据中清洗员工姓名——去除空格、统一大小写、提取姓氏、判断邮箱前缀。

代码

public class EmployeeNameProcessor {
    public static void main(String[] args) {
        String rawName = "  Zhang San  ";   // HR 导出的原始姓名
        String email = "zhang.san@feixiang.com";

        // 1. 去除首尾空格
        String trimmed = rawName.trim();
        System.out.println("trim: [" + rawName + "] → [" + trimmed + "]");
        // 输出: trim: [  Zhang San  ] → [Zhang San]

        // 2. 转大写 / 小写
        System.out.println("upper: " + trimmed.toUpperCase()); // ZHANG SAN
        System.out.println("lower: " + trimmed.toLowerCase()); // zhang san

        // 3. 提取姓和名
        int spaceIndex = trimmed.indexOf(" ");  // 查找空格位置
        String surname = trimmed.substring(0, spaceIndex);  // [0, spaceIndex)
        String givenName = trimmed.substring(spaceIndex + 1); // [spaceIndex+1, end)
        System.out.println("姓氏: " + surname);  // Zhang
        System.out.println("名字: " + givenName); // San

        // 4. 从邮箱提取前缀
        String emailPrefix = email.substring(0, email.indexOf("@"));
        System.out.println("邮箱前缀: " + emailPrefix);  // zhang.san

        // 5. 判断邮箱是否公司邮箱
        boolean isCompany = email.endsWith("@feixiang.com");
        System.out.println("是否公司邮箱: " + isCompany);   // true

        // 6. 敏感信息脱敏(手机号中间四位变 ****)
        String phone = "13812345678";
        String masked = phone.substring(0, 3) + "****" + phone.substring(7);
        System.out.println("脱敏手机号: " + masked);  // 138****5678

        // 7. split 分割
        String departments = "技术部,产品部,市场部,财务部";
        String[] deptArray = departments.split(",");
        System.out.println("部门列表:");
        for (String dept : deptArray) {
            System.out.println("  - " + dept.trim());
        }

        // 8. replace 替换
        String path = "C:\\Users\\feixiang\\documents";
        String unixPath = path.replace('\\', '/');
        System.out.println("Unix 路径: " + unixPath);
        // C:/Users/feixiang/documents
    }
}

运行输出

trim: [  Zhang San  ] → [Zhang San]
upper: ZHANG SAN
lower: zhang san
姓氏: Zhang
名字: San
邮箱前缀: zhang.san
是否公司邮箱: true
脱敏手机号: 138****5678
部门列表:
  - 技术部
  - 产品部
  - 市场部
  - 财务部
Unix 路径: C:/Users/feixiang/documents

易错场景

❌ 反例一:用 == 比较字符串内容

这是小崔入职第一天就犯的错:

// ❌ 错误:== 比较的是地址
public boolean isBoss(String name) {
    return name == "大翔";  // 如果传入的是 new String("大翔"),永远返回 false!
}

// 测试
System.out.println(isBoss("大翔"));                    // true(巧合,因为字面量)
System.out.println(isBoss(new String("大翔")));        // false!
System.out.println(isBoss(scanner.nextLine()));        // false!用户输入不在常量池

纠正:

// ✅ 正确:使用 equals() 比较内容
public boolean isBoss(String name) {
    return "大翔".equals(name);  // 常量在前,防止 NPE
}

❌ 反例二:循环中用 + 拼接字符串

// ❌ 错误:每次循环创建新的 StringBuilder 和 String
public String buildEmployeeList(List<String> names) {
    String result = "";
    for (String name : names) {
        result += name + ",";   // 10000 个名字 = 10000 个 StringBuilder + 10000 个 String
    }
    return result;
}

纠正:

// ✅ 正确:使用 StringBuilder
public String buildEmployeeList(List<String> names) {
    StringBuilder sb = new StringBuilder();
    for (String name : names) {
        sb.append(name).append(",");
    }
    return sb.toString();
}

❌ 反例三:substring 索引越界

// ❌ 错误:substring 的 endIndex 是 exclusive
String name = "大翔";
String sub = name.substring(0, 3);  // StringIndexOutOfBoundsException! 长度只有2

纠正:

// ✅ 正确:endIndex 不超过 length()
String name = "大翔";
String sub = name.substring(0, name.length());  // "大翔"(完整复制)

提醒:substring(begin, end) 的 end 索引不包含,有效范围是 [begin, end)。


面试考点

Q1:String 为什么被设计为不可变?

四个方面:① 线程安全——多线程共享无需同步;② Hash 缓存——hashCode 只需计算一次并缓存,快速用于 HashMap;③ 字符串常量池——不可变才能安全复用;④ 安全考虑——类加载器、数据库连接 URL 等不会被意外篡改。此外,不可变性也符合 String 作为"值类型"的语义直觉。

Q2:String s = new String("abc") 创建了几个对象?

1~2 个。首先 JVM 检查常量池中是否有 "abc":如果没有,先在常量池创建一个;然后在堆上用 new 创建一个新的 String 对象(其内部的 value[] 指向常量池中的字符数组)。因此可能是 1 个(池中已有 + 堆对象)或 2 个(先在池中创建 + 再在堆中创建)。

Q3:String、StringBuilder、StringBuffer 的区别?

String 不可变,每次修改产生新对象;StringBuilder 可变,线程不安全,JDK 5 引入,性能最好;StringBuffer 可变,线程安全(synchronized),JDK 1.0 即有。单线程环境优先用 StringBuilder,多线程字符串操作才用 StringBuffer。

Q4:intern() 方法的作用是什么?JDK 7 前后有何变化?

intern() 返回字符串在常量池中的引用。JDK 6 及以前常量池在方法区(永久代),intern() 会把字符串复制到常量池;JDK 7+ 常量池移到堆中,如果常量池中没有,intern() 只在常量池记录堆中对象的引用而不再复制,节省了内存。

Q5:String 的 "+" 拼接底层是如何实现的?

编译器将其优化为 StringBuilder.append() 链式调用。对于编译时常量(如 "a" + "b"),编译器直接做常量折叠产生 "ab";对于变量参与的情况,反编译后可见 new StringBuilder().append(...).toString()。因此单条语句中的多个 + 只创建一个 StringBuilder,但循环中的 + 每次迭代都会创建新的 StringBuilder。


白歌总结:"String 是面试官试探候选人基础深度的第一道门槛。能把常量池、intern()、拼接字节码这三个问题讲透,基本就算过了 String 这一关。"


JDK 11 String 增强(详见第17章「String 新方法」)

JDK 11 为 String 新增了六个实用方法,建议在学习完本章后进一步阅读:

  • isBlank() — 判断字符串为空或仅含空白字符(支持 Unicode 全角空格)
  • strip() / stripLeading() / stripTrailing() — Unicode 安全去空白,替代 trim()
  • lines() — 按行分割返回 Stream<String>,替代 split("\\R")
  • repeat(int n) — 将字符串重复 n 次拼接

这些方法在日常开发中使用频率极高,尤其是 isBlank() 和 strip(),建议全面替换 trim() 和 isEmpty() 的旧用法。

JDK 12-17 String 增强(详见第18章「HelpfulNPE与String增强」及「文本块」)

JDK 12-15 进一步丰富了 String 的工具方法链,建议在掌握本章和 JDK 11 增强后进一步阅读:

  • indent(int n) — 调整每行缩进(JDK 12),配合多行文本处理
  • transform(Function) — 自定义链式转换(JDK 12),将字符串传递给任意函数
  • stripIndent() — 去除公共前导空格(JDK 15),文本块的核心配套方法
  • translateEscapes() — 翻译转义序列(JDK 15),处理配置文件中的 \n \t 字面量
  • formatted(Object...) — 实例方法版 String.format()(JDK 15),支持链式调用
  • 文本块 """..."""(JDK 15)— 多行字符串所见即所得

这些方法让 String 的链式处理能力达到了新高度,推荐配合使用。

上一页
包装类:基本类型的对象化
下一页
StringBuffer:线程安全的可变字符串