臭名昭著aTrust隔离计划1.臭名昭著aTrust隔离计划2.VPN原理与aTrust隔离网络实践3.docker-easyconnect到底做了什么4.TUN(tunnel-隧道-虚拟网卡)模式
个人项目
personal(个人展示)GitHub Actions 自动部署个人展示项目
基于Quartz搭建的个人博客1.Quartz个人博客使用教程2.使用 rsync 增量部署 Quartz 博客3.使用 GitHub Actions 自动部署 Quartz 博客5.域名绑定6.CDN加速7.Google 搜索收录排查:为什么关键词搜得到别人,却搜不到我的网站?Obsidian、双向链接与知识图谱
架构设计
分布式分布式事务RPC原理
旅行
行程攻略青岛三日游行程攻略
前端
NginxNginx配置与反向代理入门
Nodenpm与npx的区别
数据库
mysql01.MySQL基础语法与约束02.MySQL数据库类型03.MySQL多表查询04.MySQL 索引05.MySQL 事务06.MySQL 隔离级别07.MySQL MVCC08.MySQL 锁09.EXPLAIN10.慢查询与 SQL 优化
mysql面试题01.索引问题02.事务与隔离级别03.锁机制04.存储引擎与性能优化05.用户权限、字符集与备份恢复
mysql实战01.EXPLAIN实战02.测试环境菜单误删排查与恢复03.MySQL IN 详解
网络
网络安全1.无线监听网卡苹果M系列虚拟机安装Kali Linux教程3.kali怎么使用RTL8812AU4.kali基础命令
网络基础进程之间怎样通过网络传递数据
网址常用网址
中间键
esElasticsearch 入门
mq
RabbitMQRabbitMQ高级RabbitMQ基础
1. MQ 基础2. MQ 核心角色与代码对应3. MQ Consumer Group 与消费模式4. SOFAMQ 账户变更代码实践5. MQ 可靠性问题6. 消息幂等
redis缓存击穿、穿透与雪崩Redis 分布式锁Redis 缓存问题Redis 数据结构
AI
使用说明
第三方插件&技能简介Archify使用与安装指南geekgeekRun使用OfferFlow使用指南Ponytail使用指南Skills Manager使用指南
CodexCodex CLI与IDE区别及使用指南Codex Hook单独配置与提交通知Codex MCP安装与使用指南Codex第三方插件安装与使用指南
Agent开发0-学习路线1.LLM、Token、上下文窗口与模型参数2.大模型 API、请求参数与响应结构3.Spring AI ChatClient4.Prompt、System Prompt、Prompt 模板6.结构化输出、JSON Schema7.SSE 流式响应8.会话 ID、聊天记录、Redis9.超时、重试、限流、降级10.*完成可运行聊天接口11.Function Calling、Tool Calling 原理12.用 Java 方法定义 Tool13.工具参数设计和校验14.实现订单查询工具15.实现物流查询工具16.实现库存查询工具17.多个工具自动选择18.工具异常、超时、重试
diy主机
主机零件学习1.CPU2.主板3.内存4.显卡5.硬盘6.散热7.风扇8.机箱热门型号价格表
捡垃圾diy-13600kf+5060diy-14600kf+5070
English高频词汇英文基础语法简单句子音标
GitGit常用命令Sublime Merge 使用指南
Java
工具库Caffeine本地缓存Reactor教程
工作实践如何使用@Async线程池使用与排查线上服务 CPU 使用率过高排查压测下RPC接口大量失败排查记录
基础1.HashMap2.ConcurrentHashMap3.HashSet4.final5.static6.volatile7.AtomicInteger8.String动态代理反射锁与线程同步线程注解
面试题1.HashMap 存储在方法内是线程安全的吗2.ArrayList参数传递与引用重新赋值3.== 和 equals 的区别、hashcode4.重载和重写的区别5.String、StringBuffer、StringBuilder区别及使用场景6.接口和抽象类7.ArrayList 与 LinkedList 的区别和扩容机制8.HashMap 和 Hashtable 的区别及底层实现9.Object 和 Objects 的区别10.ThreadLocal的原理和使用场景11.字节码是什么及作用12.Java类加载器有哪些13.双亲委派模型14.Java异常体系15.GC是什么16.GC如何判断对象可以被回收
面试总结0.HR面试模板1.面试一2.面试二3.面试三笔试
网络与 IO(黑马程序员-纯理论)01-网络编程基础02-IO模型与零拷贝03-BIO04-NIO基础与缓冲区05-NIO通道与选择器06-AIO
juc00-目录01-进程与线程02-线程同步与通信03-并发设计模式04-Java内存模型05-CAS与原子类06-ThreadLocal07-阻塞队列08-线程池使用与原理09-任务调度10-ForkJoin与享元模式11-AQS12-ReentrantLock13-读写锁14-并发协作工具15-ConcurrentHashMap16-CopyOnWrite与同步容器17-SkipListMap18-非阻塞队列
juc(工作常见)Java并发编程三个重点拆解
jvm1. JVM运行时数据区
Spring1.Spring IoC、DI与构造方法注入CGLIB、Byte Buddy 与 Spring AOPSpring 常用注解详解Spring Bean 生命周期
Spring MVC异常处理机制
linux常用命令
mac快捷键终端常用操作
python
基础01.Python是什么,为什么要安装Python02.Python项目中.venv是什么03.Python基础语法
基础实战sql替换
HTTPXHRMS员工详情接口调用(Python HTTPX)
windowswindows激活

一、💡 一句话理解

核心结论

String 是 Java 中表示字符串的类。它的对象一旦创建,内容就不能改变;所谓“修改字符串”,实际上是生成了一个新字符串。

二、🧭 理论:它是什么

2.1 String 是对象,不是基本类型

String name = "Afyke";

"Afyke" 是字符串字面量,也是一个 String 对象。String 类位于 java.lang 包中,不需要手动导入。

2.2 String 不可变

String text = "Java";
text = text + " String";

第二行没有修改原来的 "Java",而是创建了新的 "Java String",再让变量 text 指向新对象。

不可变带来的主要好处是:字符串可以安全共享,也适合用作 HashMap 的键。这里可以结合 1.HashMap 理解。

三、⚙️ 理论:它是怎么工作的

3.1 字符串常量池

先看这段代码:

String a = "hello";
String b = "hello";
 
System.out.println(a == b); // true:a 和 b 指向同一个 String 对象

在这个例子中,a 和 b 是当前方法的局部变量。它们都保存引用,并共同指向堆中的同一个 String 对象:

图中的关系只有三步:

  1. a 保存一个对象引用。
  2. b 也保存一个对象引用。
  3. 两个引用最终指向堆中的同一个 "hello" 对象,因此 a == b 是 true。

常量池不是另一个对象存放区

在现代 HotSpot JVM 中,被池化的 String 对象本身仍在堆中。可以把字符串常量池理解成 JVM 用来查找和复用这些对象的一张表。

再看显式使用 new 的情况:

String c = new String("hello");
 
System.out.println(a == c);      // false:a 和 c 指向不同对象
System.out.println(a.equals(c)); // true:比较的是字符串内容

这一次,new 会在堆中另外创建一个 String 对象:

a 和 c 指向两个不同的对象。虽然对象内容都是 "hello",但是:

  • a == c 是 false:比较引用,两个引用指向不同对象。
  • a.equals(c) 是 true:比较内容,两个对象的内容相同。

比较口诀

== 比较“是不是同一个对象”,equals() 比较“内容是否相同”。业务代码比较字符串内容时,通常使用 equals()。

3.2 为什么循环拼接常用 StringBuilder

关键区别是:String 不可变,StringBuilder 可变。

假设循环执行三次:

String result = "";
 
for (int i = 1; i <= 3; i++) {
    result = result + i;
}

从代码语义上看,每次拼接都不能修改原来的 String,只能创建一个新字符串,再让 result 改为指向新对象:

前面的中间字符串不用后会等待垃圾回收。循环次数很多时,可能产生许多短命的中间对象。

StringBuilder 内部维护了一个可以修改的字符缓冲区。append() 会把新内容追加到当前缓冲区,并返回当前这个 StringBuilder 对象:

StringBuilder builder = new StringBuilder();
 
// append:修改当前 builder 中的内容,然后仍返回这个 builder。
builder.append("1");
builder.append("2");
builder.append("3");
 
// 最后需要 String 时,再生成最终结果。
String result = builder.toString();

整个过程可以理解为:

为什么它可以修改

StringBuilder 的设计目标就是保存“可变字符序列”。append() 修改的是它管理的缓冲区;缓冲区容量不足时会自动扩容。因此它不是完全不分配内存,而是避免每次拼接都创建一个新的 String 中间结果。

对比项String 使用 +StringBuilder.append()
内容是否可变不可变可变
连续拼接产生新的字符串结果持续修改同一个 builder
适用场景少量、简单拼接循环或大量拼接
线程安全对象不可变,便于共享非线程安全,通常用于单线程局部变量

少量、简单的拼接直接使用 + 更清楚;循环或大量追加时再使用 StringBuilder。

3.3 StringBuffer 是什么

StringBuffer 也是一个可变字符序列,使用方式与 StringBuilder 基本相同:

StringBuffer buffer = new StringBuffer();
 
// append:修改当前 buffer 的内容,并返回当前对象。
buffer.append("Java");
buffer.append(" StringBuffer");
 
String result = buffer.toString();

它们的核心区别是:StringBuffer 会在需要的位置进行同步,让多个线程对同一个对象的单次方法调用按照某种串行顺序执行;StringBuilder 不提供这种同步保证。

3.3.1 它是怎么实现线程安全的

是的,核心实现就是加锁。OpenJDK 在 append()、delete()、insert()、length() 等关键方法上使用了 synchronized。

下面是经过精简的 append() 实现:

public synchronized StringBuffer append(String str) {
    // 真正的追加逻辑由父类完成,会修改当前对象管理的缓冲区。
    super.append(str);
 
    // 返回当前 StringBuffer,所以可以继续链式调用。
    return this;
}

这里是实例方法,所以 synchronized 锁住的是当前 StringBuffer 对象,也就是 this。它可以近似理解成:

public StringBuffer append(String str) {
    synchronized (this) {
        super.append(str);
        return this;
    }
}

当线程 A 和线程 B 同时调用同一个 buffer.append() 时:

因此,同一时刻只有一个线程能进入同一个 StringBuffer 对象的同步方法。锁还会保证:前一个线程释放锁前完成的修改,对后面获得同一把锁的线程可见。

StringBuilder 为什么更快

StringBuilder 与 StringBuffer 都通过父类 AbstractStringBuilder 管理可变缓冲区,但 StringBuilder 的常用方法不使用这种同步锁。单线程使用时,它省去了竞争和同步成本。

类型是否可变线程安全主要使用场景
String否不可变,适合共享保存和传递最终字符串
StringBuilder是否单线程中的循环或大量拼接
StringBuffer是是多个线程确实要共同修改同一个缓冲区

3.3.2 为什么复合操作仍要自己加锁

线程安全的边界

StringBuffer 能保证单次同步方法调用的安全,但多个方法组合起来不一定是一个不可分割的整体。例如先调用 length() 再调用 deleteCharAt(),两次调用之间仍可能有其他线程修改内容。

如果必须让“先判断、再删除”整体不可分割,可以使用同一个 buffer 对象作为锁:

synchronized (buffer) {
    // length 和 deleteCharAt 必须作为一个整体完成。
    if (buffer.length() > 0) {
        buffer.deleteCharAt(0);
    }
}

通常优先使用 StringBuilder。只有确实需要多个线程共享并修改同一个字符缓冲区时,才考虑 StringBuffer。

四、🚀 实践:从准备到验证

4.1 前置准备

安装 JDK 17 或更高版本即可。本例不需要第三方依赖。

4.2 可以拿来干什么

String 常用于保存用户名、请求参数和日志文本。常用方法包括:

方法用途示例结果
length()获取长度"Java".length() 得到 4
contains()判断是否包含内容"Java String".contains("Java") 得到 true
substring()截取字符串"Java".substring(1, 3) 得到 "av"
trim()去掉首尾普通空白" Java ".trim() 得到 "Java"
split()按规则拆分"a,b".split(",") 得到两个元素

4.3 完整实践:代码、启动和验证

新建 StringDemo.java:

public class StringDemo {
    public static void main(String[] args) {
        String input = "  Java,String  ";
 
        // trim:去掉字符串首尾的普通空白,并返回一个新字符串。
        String cleaned = input.trim();
 
        // split:以逗号为分隔规则拆分字符串。
        String[] parts = cleaned.split(",");
 
        // equals:比较实际内容,不比较两个变量是否指向同一个对象。
        boolean isJava = "Java".equals(parts[0]);
 
        // StringBuilder:适合连续追加多个片段。
        StringBuilder result = new StringBuilder();
        result.append("第一段:").append(parts[0]);
        result.append(",第二段:").append(parts[1]);
 
        System.out.println(result);
        System.out.println("第一段是否为 Java:" + isJava);
    }
}

编译并运行:

javac StringDemo.java
java StringDemo

预期输出:

第一段:Java,第二段:String
第一段是否为 Java:true

五、🎤 面试时怎么流利地说

5.1 30 秒标准回答

String 是 Java 中表示字符串的不可变类。对象创建后内容不能修改,拼接或替换都会返回新字符串。字符串字面量可以由常量池复用;== 比较引用,equals() 比较内容。少量拼接可以使用 +,循环或大量拼接一般使用 StringBuilder。

5.2 高频追问

  • 为什么 String 不可变? String 类本身是 final,并且没有提供修改其内部内容的公开方法。不可变让字符串更容易安全共享,也能稳定地计算和使用哈希值。final 可参考 4.final。
  • new String("abc") 创建几个对象? 不能脱离上下文固定回答。若常量池中还没有 "abc",可能涉及常量池对象和堆中新对象;若已经存在,则只需再创建堆中的新对象。

六、📌 总结

  • String 是对象,并且不可变。
  • 字符串字面量可以复用常量池中的对象。
  • 比较内容用 equals(),不要误用 ==。
  • 多次拼接优先考虑 StringBuilder。
  • 多线程共享并修改同一个缓冲区时,才考虑 StringBuffer。

七、📚 官方资料