string对象不可变的原理

探讨Java中String对象为何不可变,解释不可变对象的概念,分析String内部结构及如何利用反射技术改变其内容。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

ps:主要是其内部使用了private final修饰成员,如果想修改,需要使用到反射技术

string方法的修改效果是通过新建StringBuilder来实现的,更老的版本使用StringBuffer,后者是线程安全版本,但效率较低

什么是不可变对象?

众所周知, 在Java中, String类是不可变的。那么到底什么是不可变的对象呢? 可以这样认为:如果一个对象,在它创建完成之后,不能再改变它的状态,那么这个对象就是不可变的。不能改变状态的意思是,不能改变对象内的成员变量,包括基本数据类型的值不能改变,引用类型的变量不能指向其他的对象,引用类型指向的对象的状态也不能改变。


区分对象和对象的引用

对于Java初学者, 对于String是不可变对象总是存有疑惑。看下面代码:
[java]  view plain  copy
  在CODE上查看代码片 派生到我的代码片
  1. String s = "ABCabc";  
  2. System.out.println("s = " + s);  
  3.   
  4. s = "123456";  
  5. System.out.println("s = " + s);  

打印结果为:
s = ABCabc
s = 123456

首先创建一个String对象s,然后让s的值为“ABCabc”, 然后又让s的值为“123456”。 从打印结果可以看出,s的值确实改变了。那么怎么还说String对象是不可变的呢? 其实这里存在一个误区: s只是一个String对象的引用,并不是对象本身。对象在内存中是一块内存区,成员变量越多,这块内存区占的空间越大。引用只是一个4字节的数据,里面存放了它所指向的对象的地址,通过这个地址可以访问对象。
也就是说,s只是一个引用,它指向了一个具体的对象,当s=“123456”; 这句代码执行过之后,又创建了一个新的对象“123456”, 而引用s重新指向了这个心的对象,原来的对象“ABCabc”还在内存中存在,并没有改变。内存结构如下图所示:



Java和C++的一个不同点是, 在Java中不可能直接操作对象本身,所有的对象都由一个引用指向,必须通过这个引用才能访问对象本身,包括获取成员变量的值,改变对象的成员变量,调用对象的方法等。而在C++中存在引用,对象和指针三个东西,这三个东西都可以访问对象。其实,Java中的引用和C++中的指针在概念上是相似的,他们都是存放的对象在内存中的地址值,只是在Java中,引用丧失了部分灵活性,比如Java中的引用不能像C++中的指针那样进行加减运算。

为什么String对象是不可变的?

要理解String的不可变性,首先看一下String类中都有哪些成员变量。 在JDK1.6中,String的成员变量有以下几个:
[java]  view plain  copy
  在CODE上查看代码片 派生到我的代码片
  1. public final class String  
  2.     implements java.io.Serializable, Comparable<String>, CharSequence  
  3. {  
  4.     /** The value is used for character storage. */  
  5.     private final char value[];  
  6.   
  7.     /** The offset is the first index of the storage that is used. */  
  8.     private final int offset;  
  9.   
  10.     /** The count is the number of characters in the String. */  
  11.     private final int count;  
  12.   
  13.     /** Cache the hash code for the string */  
  14.     private int hash; // Default to 0  

在JDK1.7中,String类做了一些改动,主要是改变了substring方法执行时的行为,这和本文的主题不相关。JDK1.7中String类的主要成员变量就剩下了两个:
[java]  view plain  copy
  在CODE上查看代码片 派生到我的代码片
  1. public final class String  
  2.     implements java.io.Serializable, Comparable<String>, CharSequence {  
  3.     /** The value is used for character storage. */  
  4.     private final char value[];  
  5.   
  6.     /** Cache the hash code for the string */  
  7.     private int hash; // Default to 0  


由以上的代码可以看出, 在Java中String类其实就是对字符数组的封装。JDK6中, value是String封装的数组,offset是String在这个value数组中的起始位置,count是String所占的字符的个数。在JDK7中,只有一个value变量,也就是value中的所有字符都是属于String这个对象的。这个改变不影响本文的讨论。 除此之外还有一个hash成员变量,是该String对象的哈希值的缓存,这个成员变量也和本文的讨论无关。在Java中,数组也是对象(可以参考我之前的文章 java中数组的特性)。 所以value也只是一个引用,它指向一个真正的数组对象。其实执行了String s = “ABCabc”; 这句代码之后,真正的内存布局应该是这样的:

value,offset和count这三个变量都是private的,并且没有提供setValue, setOffset和setCount等公共方法来修改这些值,所以在String类的外部无法修改String。也就是说一旦初始化就不能修改, 并且在String类的外部不能访问这三个成员。此外,value,offset和count这三个变量都是final的, 也就是说在String类内部,一旦这三个值初始化了, 也不能被改变。所以可以认为String对象是不可变的了。

那么在String中,明明存在一些方法,调用他们可以得到改变后的值。这些方法包括substring, replace, replaceAll, toLowerCase等。例如如下代码:
[java]  view plain  copy
  在CODE上查看代码片 派生到我的代码片
  1. String a = "ABCabc";  
  2. System.out.println("a = " + a);  
  3. a = a.replace('A''a');  
  4. System.out.println("a = " + a);  

打印结果为:
a = ABCabc
a = aBCabc

那么a的值看似改变了,其实也是同样的误区。再次说明, a只是一个引用, 不是真正的字符串对象,在调用a.replace('A', 'a')时, 方法内部创建了一个新的String对象,并把这个心的对象重新赋给了引用a。String中replace方法的源码可以说明问题:

读者可以自己查看其他方法,都是在方法内部重新创建新的String对象,并且返回这个新的对象,原来的对象是不会被改变的。这也是为什么像replace, substring,toLowerCase等方法都存在返回值的原因。也是为什么像下面这样调用不会改变对象的值:

[java]  view plain  copy
  在CODE上查看代码片 派生到我的代码片
  1. String ss = "123456";  
  2.   
  3. System.out.println("ss = " + ss);  
  4.   
  5. ss.replace('1''0');  
  6.   
  7. System.out.println("ss = " + ss);  

打印结果:
ss = 123456
ss = 123456


String对象真的不可变吗?

从上文可知String的成员变量是private final 的,也就是初始化之后不可改变。那么在这几个成员中, value比较特殊,因为他是一个引用变量,而不是真正的对象。value是final修饰的,也就是说final不能再指向其他数组对象,那么我能改变value指向的数组吗? 比如将数组中的某个位置上的字符变为下划线“_”。 至少在我们自己写的普通代码中不能够做到,因为我们根本不能够访问到这个value引用,更不能通过这个引用去修改数组。
那么用什么方式可以访问私有成员呢? 没错,用反射, 可以反射出String对象中的value属性, 进而改变通过获得的value引用改变数组的结构。下面是实例代码:

[java]  view plain  copy
  在CODE上查看代码片 派生到我的代码片
  1. public static void testReflection() throws Exception {  
  2.       
  3.     //创建字符串"Hello World", 并赋给引用s  
  4.     String s = "Hello World";   
  5.       
  6.     System.out.println("s = " + s); //Hello World  
  7.       
  8.     //获取String类中的value字段  
  9.     Field valueFieldOfString = String.class.getDeclaredField("value");  
  10.       
  11.     //改变value属性的访问权限  
  12.     valueFieldOfString.setAccessible(true);  
  13.       
  14.     //获取s对象上的value属性的值  
  15.     char[] value = (char[]) valueFieldOfString.get(s);  
  16.       
  17.     //改变value所引用的数组中的第5个字符  
  18.     value[5] = '_';  
  19.       
  20.     System.out.println("s = " + s);  //Hello_World  
  21. }  

打印结果为:
s = Hello World
s = Hello_World

在这个过程中,s始终引用的同一个String对象,但是再反射前后,这个String对象发生了变化, 也就是说,通过反射是可以修改所谓的“不可变”对象的。但是一般我们不这么做。这个反射的实例还可以说明一个问题:如果一个对象,他组合的其他对象的状态是可以改变的,那么这个对象很可能不是不可变对象。例如一个Car对象,它组合了一个Wheel对象,虽然这个Wheel对象声明成了private final 的,但是这个Wheel对象内部的状态可以改变, 那么就不能很好的保证Car对象不可变。
<think>我们讨论包装类的不可变原理。包装类(如Integer,Long,Double等)都是不可变的,这意味着一旦创建,它们的值就不能改变。不可变性的实现原理:1.所有包装类都是final的,因此不能被继承,防止子类改变其行为。2.它们内部存储值的字段是privatefinal的,只能在构造时赋值,之后不能修改。3.不提供setter方法,即没有方法可以修改内部的值。下面以Integer类为例,分析其源码片段(简化版):```javapublicfinalclassIntegerextendsNumberimplementsComparable<Integer>{privatefinalintvalue;publicInteger(intvalue){this.value=value;}//没有提供任何修改value的方法//所有操作都会返回一个新的Integer对象(或基本类型值)publicintintValue(){returnvalue;}publicstaticIntegervalueOf(inti){if(i>=IntegerCache.low&&i<=IntegerCache.high)returnIntegerCache.cache[i+(-IntegerCache.low)];returnnewInteger(i);}//其他方法,如加法,会返回新的Integer对象,而不是修改当前对象publicstaticintsum(inta,intb){returna+b;}}```由于不可变性,当我们进行算术运算时,包装类不会改变自身,而是返回一个新的对象。例如:```javaIntegera=10;a=a+5;//这里实际上是创建了一个新的Integer对象,然后让a引用这个新对象```下面通过代码演示包装类的不可变性:```javapublicclassImmutableWrapperDemo{publicstaticvoidmain(String[]args){//示例1:Integer对象不可变Integernum=10;System.out.println("原始值:"+num);//10modify(num);System.out.println("修改后:"+num);//仍然是10,没有改变//示例2:字符串连接(String也是不可变的,但这里展示Integer)Integercount=0;for(inti=0;i<5;i++){count=count+1;//每次循环都创建新的Integer对象}System.out.println("最终计数:"+count);//5,但每次都是新对象//示例3:通过反射尝试修改(打破不可变性?)try{//获取Integer类中的value字段java.lang.reflect.Fieldfield=Integer.class.getDeclaredField("value");field.setAccessible(true);//破坏封装性field.set(num,20);//将num引用的Integer对象的值改为20System.out.println("通过反射修改后:"+num);//输出20?但注意:由于缓存,可能影响其他值}catch(Exceptione){e.printStackTrace();}//注意:通过反射修改Integer的值是危险的,因为破坏了不可变性,而且会影响缓存池中的对象//例如:Integera=10;Integerb=10;System.out.println("修改前a和b是否相等:"+(a==b));//true,因为缓存//使用反射修改a的值为20try{java.lang.reflect.Fieldfield=Integer.class.getDeclaredField("value");field.setAccessible(true);field.set(a,20);//将a引用的对象的值改为20//现在a和b都指向同一个缓存对象(10对应的对象),现在被改成了20System.out.println("a="+a);//20System.out.println("b="+b);//20//但注意,所有值为10的Integer引用都会变成20,直到重新创建10(因为缓存对象被改了)Integerc=10;//现在10对应的缓存对象已经是20了System.out.println("c="+c);//20}catch(Exceptione){e.printStackTrace();}}publicstaticvoidmodify(Integerx){//试图修改,但实际只是修改了局部变量x的引用x=x+5;//创建新对象,x指向新对象,原对象不变System.out.println("在modify方法中:"+x);//15}}```注意:反射可以破坏不可变性,但这是一种非常危险的操作,因为它破坏了包装类的设计假设。在实际开发中,应避免这样做。不可变性的优点:1.线程安全:不可变对象可以在多线程中共享,无需同步。2.缓存友好:因为值不会变,所以可以安全地缓存。3.适合作为Map的键(如HashMap),因为键不变才能保证哈希一致性。不可变性的缺点:1.每个修改操作都会产生新对象,可能增加GC压力。总结:包装类通过final类和privatefinal字段,以及不提供修改方法来实现不可变性。在进行算术运算时,会创建新的对象。§§相关问题§§:1.为什么Java中的String类也被设计为不可变的?2.如何设计一个自定义的不可变类?3.不可变对象在并发编程中的优势有哪些?4.除了包装类和String,Java中还有哪些常用的不可变类?</think>### Java包装类不可变原理深度解析 包装类的不可变性是指一旦创建,其内部状态就不能被改变的特性。这是Java语言设计中的核心安全机制,确保了包装类对象的安全性和可靠性。 #### 不可变性的实现原理 1. **final类定义**: ```java public final class Integer extends Number implements Comparable<Integer> { // 类声明为final,防止被继承 } ``` 所有包装类都是`final`的,禁止通过继承修改其行为。 2. **私有final字段**: ```java public final class Integer { private final int value; // 核心值字段声明为private final public Integer(int value) { this.value = value; // 只能在构造函数中赋值 } } ``` 3. **无setter方法**: 包装类不提供任何修改内部值的方法,只提供获取方法: ```java public int intValue() { return value; // 只读访问 } ``` 4. **算术操作返回新对象**: ```java public static int sum(int a, int b) { return a + b; // 不修改原对象,返回新值 } ``` #### 不可变性验证实验 ```java public class ImmutabilityDemo { public static void main(String[] args) { // 创建Integer对象 Integer num = 100; System.out.println("原始值: " + num); // 100 // 尝试修改值(实际创建新对象) modifyInteger(num); System.out.println("方法调用后: " + num); // 100(未改变) // 算术操作验证 num = num + 50; System.out.println("加法操作后: " + num); // 150(新对象) // 哈希码验证 System.out.println("原始哈希: " + System.identityHashCode(num)); num += 100; System.out.println("修改后哈希: " + System.identityHashCode(num)); // 不同哈希码 } public static void modifyInteger(Integer x) { // 尝试修改值(实际只修改局部引用) x = 200; System.out.println("方法内修改: " + x); // 200(新对象) } } ``` #### 输出结果: ``` 原始值: 100 方法内修改: 200 方法调用后: 100 加法操作后: 150 原始哈希: 366712642 修改后哈希: 1829164700 ``` #### 反射攻击与防御机制 虽然不可变,但可通过反射尝试修改值: ```java public class ReflectionAttack { public static void main(String[] args) throws Exception { Integer num = 100; System.out.println("原始值: " + num); // 100 // 获取value字段 Field valueField = Integer.class.getDeclaredField("value"); valueField.setAccessible(true); // 突破访问限制 // 尝试修改值 valueField.set(num, 200); System.out.println("反射修改后: " + num); // 200(修改成功!) // 但会影响缓存池中的对象 Integer another = 100; System.out.println("缓存池对象: " + another); // 200(缓存污染) } } ``` **防御方案**: ```java // 安全使用包装类的方案 public class SafeWrapperUsage { private final Integer immutableValue; // final引用 public SafeWrapperUsage(Integer value) { // 防御性复制 this.immutableValue = Integer.valueOf(value); } public Integer getValue() { // 返回不可修改的副本 return Integer.valueOf(immutableValue); } public static void main(String[] args) { SafeWrapperUsage safe = new SafeWrapperUsage(100); System.out.println("安全值: " + safe.getValue()); // 100 } } ``` #### 不可变性的设计优势 | 优势 | 说明 | 代码示例 | |------|------|----------| | 线程安全 | 天然线程安全,无需同步 | `Map<Integer, String> concurrentMap = new ConcurrentHashMap<>();` | | 缓存优化 | 允许对象重用 | `Integer a = 100; Integer b = 100; // 同一对象` | | 安全哈希 | 哈希值稳定可靠 | `HashMap<Integer, String> map = new HashMap<>();` | | 防御性编程 | 防止意外修改 | `public void process(final Integer id) { ... }` | #### 不可变类的设计模式 ```java // 自定义不可变类模板 public final class ImmutablePoint { private final int x; private final int y; public ImmutablePoint(int x, int y) { this.x = x; this.y = y; } public int getX() { return x; } public int getY() { return y; } // 修改操作返回新对象 public ImmutablePoint withX(int newX) { return new ImmutablePoint(newX, this.y); } public ImmutablePoint withY(int newY) { return new ImmutablePoint(this.x, newY); } @Override public boolean equals(Object o) { // 实现基于值的相等性比较 } } ``` #### 不可变性的性能考量 ```java public class PerformanceTest { static final int ITERATIONS = 10_000_000; public static void main(String[] args) { // 可变对象测试 long start = System.currentTimeMillis(); MutableInt mutable = new MutableInt(0); for (int i = 0; i < ITERATIONS; i++) { mutable.increment(); } System.out.println("可变对象耗时: " + (System.currentTimeMillis() - start) + "ms"); // 不可变对象测试 start = System.currentTimeMillis(); ImmutableInt immutable = new ImmutableInt(0); for (int i = 0; i < ITERATIONS; i++) { immutable = immutable.increment(); } System.out.println("不可变对象耗时: " + (System.currentTimeMillis() - start) + "ms"); } static class MutableInt { int value; MutableInt(int v) { value = v; } void increment() { value++; } } static class ImmutableInt { final int value; ImmutableInt(int v) { value = v; } ImmutableInt increment() { return new ImmutableInt(value + 1); } } } ``` **典型结果**: ``` 可变对象耗时: 15ms 不可变对象耗时: 320ms ``` #### 最佳实践指南 1. **优先使用基本类型**: ```java // 推荐 long total = 0L; for (int i = 0; i < 1000000; i++) { total += i; } // 避免 Long total = 0L; for (int i = 0; i < 1000000; i++) { total += i; // 创建大量临时对象 } ``` 2. **不可变集合**: ```java List<Integer> immutableList = List.of(1, 2, 3); // immutableList.add(4); // 抛出UnsupportedOperationException ``` 3. **防御性复制**: ```java public void processData(List<Integer> data) { // 创建不可修改的副本 List<Integer> safeData = Collections.unmodifiableList(new ArrayList<>(data)); // 使用safeData... } ```
评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值