string和integer是不可变类,所有“修改”操作均返回新对象,原对象不变;其字段为final且无写入接口,多线程安全、适合作为hashmap键;自定义不可变类需防可变对象逃逸。

为什么 String 和 Integer 看似能“修改”,实际却不会改变对象本身
因为它们是不可变类——每次看似修改的操作(比如 substring()、toLowerCase()、intValue() + 1),都返回一个新对象,原对象内存地址和内容完全没动。
常见错误现象:String s = "hello"; s.concat(" world"); System.out.println(s); // 输出 "hello" —— 忘记赋值给新变量,误以为方法改了原对象。
使用场景:多线程共享字符串或数字配置时,不用加锁;作为 HashMap 的 key 安全可靠(哈希值不会变)。
关键点:
立即学习“Java免费学习笔记(深入)”;
-
String内部用final char[](Java 8)或final byte[](Java 9+)存储,且不提供任何写入接口 - 所有“修改”方法(
replace()、trim()、toUpperCase())都 new 一个新String - 包装类如
Integer的字段是final int value,构造后无法变更;valueOf()返回缓存或新实例,但绝不复用可变状态
自己写不可变类时,最容易漏掉的三个防护点
不是加了 final 类修饰符就万事大吉。真正让类不可变,得堵死所有可能的“逃逸路径”。
常见错误现象:外部拿到内部可变对象引用后直接修改,导致“假不可变”。例如自定义类里持有一个 ArrayList,getter 直接返回它,调用方一改就破防。
实操建议:
- 类声明为
final,防止子类覆盖方法破坏契约 - 所有字段必须
private final,且不能是可变引用类型(如ArrayList、Date) - 如果必须持有可变对象(比如
int[]或Calendar),getter 要返回防御性拷贝:return Arrays.copyOf(this.data, this.data.length); - 构造器里若接收外部传入的可变对象(如
new MyImmutable(list)),也要做深拷贝,不能直接赋值
String 在 Java 9 后的底层变化对不可变性的实际影响
Java 9 把 String 的底层从 char[] 换成 byte[] + coder 字段,是为了节省内存,但**不影响不可变语义**——你依然不能通过任何公开 API 修改它的内容。
性能影响:小写字母为主的字符串(如英文标识符)内存减半;但频繁调用 String.getBytes(StandardCharsets.UTF_8) 可能略慢,因为要按 coder 解码再转码。
兼容性注意点:
-
String.value是私有字段,反射读取会失败(模块系统限制),别依赖它 -
String.contentEquals()和equals()行为一致,但前者接受任意CharSequence,别误以为它绕过不可变检查 - 字符串拼接在编译期优化(
"a" + "b"→"ab")仍基于常量池,运行时StringBuilder构建的字符串仍是新对象,不污染原串
包装类缓存机制(如 Integer.valueOf())和不可变性的关系
Integer.valueOf(127) 返回的是缓存对象,valueOf(128) 才 new 新实例——但这和“不可变”不冲突,只关乎对象复用。只要没人能改它,复用多少次都安全。
容易踩的坑:
- 用
==比较两个Integer:在 -128 到 127 范围内可能为true,超出范围就是false,这不是 bug,是缓存策略导致的副作用 -
new Integer(42)总是新建对象,绕过缓存,也违背不可变类的最佳实践(应统一用valueOf) -
Boolean只有TRUE和FALSE两个实例,valueOf("true")必然返回缓存对象,这点比Integer更彻底
真正关键的,是别试图通过反射修改 Integer.value 字段——JDK 9+ 的模块系统默认禁止,而且改了之后整个 JVM 里的计算都可能错乱。









