Java基础篇——Java String 深度解析:底层实现、不可变性、常量池与相关类全解读
String 可以说是 Java 中使用频率最高的类之一。虽然它看起来只是一种“文本类型”,但其底层设计、性能考虑以及安全机制都非常深奥。
这篇文章将从底层实现到常量池机制,再到不可变性与线程安全,完整讲解 Java 中的 String,让你彻底吃透面试高频考点。
一、String 底层如何实现?(JDK8 vs JDK9)
JDK1.8 及之前:使用 char[] 存储
private final char value[];
每个字符占 2 字节(UTF-16),因此字符串占用空间偏大。
JDK1.9 及之后:使用 byte[] + coder 存储(紧凑字符串 Compact Strings)
private final byte[] value;
private final byte coder; // LATIN1 or UTF16
JDK9 引入了“紧凑字符串”机制:
-
对于只需要 1 字节的内容(如 ASCII),使用 LATIN1 存储
-
只有需要 2 字节时才使用 UTF-16
优点:
✔ 内存占用更低
✔ CPU 缓存效率更高
✔ String 操作整体性能提升
也就是说,从 JDK9 开始,Java 的 String 内存模型更加紧凑和高效。
二、String 能否被继承?为什么?
不能。
源码中:
public final class String implements ...
final 修饰类表示:
-
不能被继承
-
设计者可以保证 String 的语义和行为不会被破坏
这也是不可变设计的一部分(后面会讲)。
三、String 为什么是不可变的?(核心考点)
String 的不可变性是面试重灾区。我们从三个角度理解:
1. 内存与性能:String 常量池的基础依赖
因为 String 不可变,JVM 才敢把内容一样的字符串合并复用。
示例:
String s1 = "hello";
String s2 = "hello";
s1 和 s2 指向池里的同一个对象。
如果 String 是可变的 ——
只要 s1 被改了,那么池中的值就变了,s2 也会受到影响。
显然无法接受,因此 必须不可变。
2. 安全性
String 常用于存储敏感内容,例如:
-
用户密码
-
数据库连接串
-
文件路径
-
网络地址
-
ClassLoader 加载类名
-
反射调用方法名
如果 String 可变:
String path = "/usr/local/file";
modify(path);
若 modify() 内部改了 path,那么系统就可能执行未知路径,直接导致安全问题。
不可变保证了:
只要你拿到一个 String,它的内容 100% 不会被改变。
3. 线程安全
多线程环境下,不可变对象天然线程安全。
多个线程访问同一个字符串,不会产生并发问题,因为内容永远不会改变。
这就是为什么 String 可以安全地用于:
-
缓存 key
-
集合中的键(HashMap / HashSet)
-
多线程共享变量
四、String 不可变是如何实现的?
简单来说:
-
底层数组 value 被 final 修饰
private final byte[] value;final 确保数组引用不能被替换。
-
数组是 private 的
外部无法获取数组引用进行修改。 -
String 没有提供任何“修改自身”的方法
replace(),substring(),concat()等操作都是返回“新对象”,不改变原对象。
因此 String 彻彻底底就是不可变对象。
五、String 的长度限制
Java 并不是“无限长字符串”,它有两个不同阶段的限制:
👉 编译期
👉 运行期
1. 编译期常量的限制:最大 65535 个字节
原因:常量池使用 CONSTANT_Utf8_info 结构,最多支持 65535(2 ^16 - 1)字节。
如果你写:
String s = "特别长的常量....";
超过上限,编译器报错。
2. 运行期字符串长度:int 最大值
String.length() 返回 int,因此字符串最大是:
Integer.MAX_VALUE = 2^31 - 1 ≈ 21 亿字符
你不可能真正创造这么长(内存撑不住),但理论上就是这个限制。
六、字符串常量池(String Pool)解析
常量池是 JVM 堆中的一块特殊区域,用来缓存字符串常量。
机制如下:
String s1 = "Hello";
String s2 = "Hello";
两个变量将引用同一个对象(复用)。
intern() 方法
String s3 = new String("Hello").intern();
解释:
-
new String("Hello")创建堆对象 -
intern() 会让 s3 指向池中的常量
"Hello"
因此:
System.out.println(s1 == s3); // true
为什么常量池可以大幅提升性能?
✔ 避免重复对象占用内存
✔ 加快字符串比较速度(== 可直接比较引用)
✔ 提高 JVM 整体性能
七、String、StringBuffer、StringBuilder 的区别
String(不可修改)——> StringBuffer(可修改,安全,但是慢) ——> StringBuilder(可修改,不安全,但是快)
| 类名 | 可变性 | 线程安全 | 适用场景 |
|---|---|---|---|
| String | ❌ 不可变 | ✔ 天然线程安全 | 字符串内容不经常改动 |
| StringBuffer | ✔ 可变 | ✔ 线程安全(方法加 synchronized) | 多线程下频繁修改字符串 |
| StringBuilder | ✔ 可变 | ❌ 非线程安全 | 单线程下大量字符串拼接性能最佳 |
面试关键点总结:
-
频繁修改字符串内容 → StringBuilder
-
必须线程安全 → StringBuffer
-
内容稳定、不常修改 → String
八、为什么 StringBuilder 更快?
因为:
-
不可变 String 每次拼接都创建新对象
-
StringBuffer 方法有 synchronized,锁开销大
-
StringBuilder 无锁,纯可变操作,性能最强
示例(JVM 优化前):
String s = "";
for (int i = 0; i < 1000; i++) {
s += i; // 每次都创建新的 String!
}
效率极差。
改成 StringBuilder:
StringBuilder sb = new StringBuilder();
for (int i = 0; i < 1000; i++) {
sb.append(i);
}
性能差距几百倍。
九、全文总结
Java 中的 String 是一个经过深思熟虑的高级设计,它的不可变特性不仅仅是语言层面的“规定”,更是为了:
✔ 提升性能(常量池、缓存复用)
✔ 提升安全性(路径、类名、密码)
✔ 提高线程安全性(天然无并发问题)
✔ 支持底层优化(JDK9 紧凑字符串)
此外,StringBuffer 与 StringBuilder 为不同场景提供了可变字符串的方案。
最终,你可以用一张表总结:
| 特性 | String | StringBuffer | StringBuilder |
|---|---|---|---|
| 可变性 | ❌ 不可变 | ✔ 可变 | ✔ 可变 |
| 线程安全 | ✔ 是 | ✔ 是 | ❌ 否 |
| 底层结构 | JDK8: char[] / JDK9+: byte[] | char[] | char[] |
| 性能 | 中 | 低 | 高 |
| 使用场景 | 稳定字符串 | 多线程修改字符串 | 单线程大量修改字符串 |
更多推荐


所有评论(0)