[Java学习日记05】聊聊String为什么不可变
前言:关于“String 到底能不能变”的深夜辩论
坐标:学校图书馆(空调滴水,我心冰冷)
状态:键盘磨损度 +1%,咖啡因浓度 80%。
碎碎念: 兄弟们,今天下午我在那敲代码,隔壁桌哥们突然问我:“哎,Java 的 String 为什么不可变啊?” 我当时想都没想,脱口而出:“因为它被
final修饰了啊!”结果那哥们冷笑一声,直接翻开源码怼到我脸上:“
final只能保证引用不变,或者类不能被继承,它能保证里面的数组不被改吗?”卧槽,我当时就愣住了。回来钻研了一晚上,才发现这事儿远没我想的那么简单。今天咱就来深度解剖一下 String 的“钢铁直男”属性——不可变性。
🚀 目录
-
-
1.1 源码三部曲:不仅仅是 final
-
1.2 这里的 char[] 藏着猫腻
-
-
-
2.1 字符串常量池(String Pool)的刚需
-
2.2 线程安全:天生的“冷淡风”
-
2.3 性能优化:Hash 值的“一劳永逸”
-
2.4 安全防火墙:防止 SQL 注入与路径篡改
-
1. 真相大白:String 到底是怎么变“硬”的?
很多人(比如之前的我)以为 String 不可变是因为类前面加了 final。但其实,这只是第一层防御。
1.1 源码三部曲
打开 JDK 8 源码(JDK 9 以后变成了 byte[],但逻辑一致),你会发现 String 的防御是“全方位”的:
public final class String implements java.io.Serializable, Comparable<String>, CharSequence {
// 1. 数组被私有化了,外面看不到
// 2. 被 final 修饰了,数组的引用地址不能改
private final char value[];
// ...
}
1.2 这里的坑在哪里?
-
final修饰类:说明 String 没有子类,防止别人通过继承来恶意重写方法。 -
private final char[]:这里最阴险!final只能保证这个value变量不再指向别的数组,但它拦不住你修改数组里面的元素(比如value[0] = 'a')。 -
真正的关键:String 类里面没有任何 setter 方法!它提供的所有操作方法(比如
replace,substring,toLowerCase)底层都是new了一个新的 String 对象并返回。
结论:String 的不可变性,是靠 final 类、私有成员变量,以及“绝不提供修改接口”这三连招共同达成的。
2. 灵魂拷问:为什么要费劲心思让它不可变?
设计者不傻,搞这么麻烦肯定是有巨大收益的。
2.1 字符串常量池(String Pool)的刚需
这是最重要的原因。 Java 为了省内存,搞了个常量池。如果你写 String s1 = "Java", String s2 = "Java",它们其实指向的是内存里的同一个地址。
-
大学生比喻:如果 String 是可变的,你把
s1改成了Python,那s2也跟着变了,这不乱套了吗?这就好比你改了自己的外卖订单,结果全宿舍的饭都跟着变了。
2.2 线程安全
因为 String 是不可变的,所以它在多线程环境下是绝对安全的。 你不需要给它加任何锁,随便多少个线程同时访问它,它都稳如泰山。这大大节省了系统开销。
2.3 性能优化:Hash 值的“一劳永逸”
String 经常被拿来当 HashMap 的 Key。 因为 String 不可变,所以它的 hashCode 只需要计算一次,然后就可以缓存起来(源码里有个 hash 字段)。下次再用直接取,性能飞快。如果 String 能变,那 Map 的数据结构就全毁了。
2.4 安全防火墙
这是最硬核的一点。 Java 的很多关键参数都是 String,比如数据库连接的 URL、服务器的 IP、文件的 Path。 如果 String 是可变的,黑客就可以在你的程序通过权限校验之后,悄悄把路径改掉。
3. 骚操作:用“反射”强行掰弯 String
虽然 Java 官方不建议,但咱们程序员总有“骚操作”。 通过反射(Reflection),我们可以强行突破 private 的限制,修改 String 内部的值。
String s = "Hello";
// 1. 获取 String 类的 value 字段
Field valueField = String.class.getDeclaredField("value");
// 2. 强行开启访问权限
valueField.setAccessible(true);
// 3. 拿到那个“私有”的数组
char[] v = (char[]) valueField.get(s);
// 4. 强行改它!
v[0] = 'W';
System.out.println(s); // 输出:Wello!它居然变了!
警告:这种操作会破坏字符串常量池。如果你改了 "Hello",那么全程序里所有的 "Hello" 都会变成 "Wello"。千万别在生产代码里这么干,会被开除的!
4. 不可变的代价:为什么要有 StringBuilder?
万物皆有代价。String 的不可变导致了一个大问题:产生大量临时对象。 如果你在循环里用 + 拼接字符串:
String s = "";
for(int i=0; i<100; i++) {
s += i; // 每次循环都会 new 一个对象,GC 忙得满头大汗
}
所以,当需要频繁修改字符串时,务必使用 StringBuilder 或 StringBuffer。
5. 核心对比:三兄弟大乱斗
|
特征 |
String |
StringBuffer |
StringBuilder |
|---|---|---|---|
|
可变性 |
不可变 |
可变 |
可变 |
|
线程安全 |
安全 |
安全 (synchronized) |
不安全 |
|
性能 |
最慢 |
中等 |
最快 |
|
建议 |
少量操作 |
多线程环境 |
单线程频繁操作 |
6. 结语
写完这篇,图书馆的闭馆音乐已经响起了。 String 为什么不可变?不仅仅是一个语法设定,更是 Java 在内存、安全、性能之间做出的终极权衡。
作为大学生的我们,以后面试再被问到这题,把常量池、线程安全、Hash 缓存和反射这些点全甩出来,保证面试官对你刮目相看。
最后,祝大家:
-
String 一样坚定不移(守住底线)。
-
StringBuilder 一样灵活多变(适应环境)。
-
所有的 Bug 都能被一键修复!
如果你也被“反射”修改 String 的代码惊到了,点个赞支持一下!咱们下期聊聊String, StringBuilder,和StringBuffer
更多推荐

所有评论(0)