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";

s1s2 指向池里的同一个对象

如果 String 是可变的 ——
只要 s1 被改了,那么池中的值就变了,s2 也会受到影响。

显然无法接受,因此 必须不可变


2. 安全性

String 常用于存储敏感内容,例如:

  • 用户密码

  • 数据库连接串

  • 文件路径

  • 网络地址

  • ClassLoader 加载类名

  • 反射调用方法名

如果 String 可变:

String path = "/usr/local/file";
modify(path);

若 modify() 内部改了 path,那么系统就可能执行未知路径,直接导致安全问题。

不可变保证了:

只要你拿到一个 String,它的内容 100% 不会被改变。


3. 线程安全

多线程环境下,不可变对象天然线程安全。

多个线程访问同一个字符串,不会产生并发问题,因为内容永远不会改变。

这就是为什么 String 可以安全地用于:

  • 缓存 key

  • 集合中的键(HashMap / HashSet)

  • 多线程共享变量


四、String 不可变是如何实现的?

简单来说:

  1. 底层数组 value 被 final 修饰

    private final byte[] value;
    

    final 确保数组引用不能被替换。

  2. 数组是 private 的
    外部无法获取数组引用进行修改。

  3. 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();

解释:

  1. new String("Hello") 创建堆对象

  2. 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[]
性能
使用场景 稳定字符串 多线程修改字符串 单线程大量修改字符串

 

Logo

Agent 垂直技术社区,欢迎活跃、内容共建。

更多推荐