Java中的String为什么是不可变的? -- String源码分析(通俗易懂)

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

区分对象和对象的引用
对于Java初学者, 对于String是不可变对象总是存有疑惑。看下面代码:

        String s = "ABCabc";
        System.out.println("s = " + s);
        
        s = "123456";
        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的成员变量有以下几个:

public final class String
    implements java.io.Serializable, Comparable<String>, CharSequence
{
    /** The value is used for character storage. */
    private final char value[];
 
    /** The offset is the first index of the storage that is used. */
    private final int offset;
 
    /** The count is the number of characters in the String. */
    private final int count;
 
    /** Cache the hash code for the string */
    private int hash; // Default to 0

}

在JDK1.7中,String类做了一些改动,主要是改变了substring方法执行时的行为,这和本文的主题不相关。JDK1.7中String类的主要成员变量就剩下了两个:

public final class String
    implements java.io.Serializable, Comparable<String>, CharSequence {
    /** The value is used for character storage. */
    private final char value[];
 
    /** Cache the hash code for the string */
    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等。例如如下代码:

        String a = "ABCabc";
        System.out.println("a = " + a);
        a = a.replace('A', 'a');
        System.out.println("a = " + a);

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

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

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

        String ss = "123456";
        
        System.out.println("ss = " + ss);
        
        ss.replace('1', '0');
        
        System.out.println("ss = " + ss);

打印结果:
ss = 123456
ss = 123456


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

 public static void testReflection() throws Exception {
        
        //创建字符串"Hello World", 并赋给引用s
        String s = "Hello World"; 
        
        System.out.println("s = " + s);    //Hello World
        
        //获取String类中的value字段
        Field valueFieldOfString = String.class.getDeclaredField("value");
        
        //改变value属性的访问权限
        valueFieldOfString.setAccessible(true);
        
        //获取s对象上的value属性的值
        char[] value = (char[]) valueFieldOfString.get(s);
        
        //改变value所引用的数组中的第5个字符
        value[5] = '_';
        
        System.out.println("s = " + s);  //Hello_World
    }

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

在这个过程中,s始终引用的同一个String对象,但是再反射前后,这个String对象发生了变化, 也就是说,通过反射是可以修改所谓的“不可变”对象的。但是一般我们不这么做。这个反射的实例还可以说明一个问题:如果一个对象,他组合的其他对象的状态是可以改变的,那么这个对象很可能不是不可变对象。例如一个Car对象,它组合了一个Wheel对象,虽然这个Wheel对象声明成了private final 的,但是这个Wheel对象内部的状态可以改变, 那么就不能很好的保证Car对象不可变。

<think>嗯,用户想让我解释策略模式,并且用Java代码示例。首先,我需要确保自己完全理解策略模式的概念,这样才能用通俗易懂的方式解释清楚。策略模式属于行为型设计模式,对吧?它的主要作用是将算法或行为封装成独立的类,使得它们可以互相替换,这样客户端可以根据需要选择不同的策略。 那用户可能是什么背景呢?可能是刚开始学习设计模式的开发者,或者是需要在实际项目中应用策略模式的人。他们可能已经对面向对象编程有一定了解,但对设计模式还不够熟悉。所以,我需要用简单明了的语言,避免使用太多专业术语,或者至少在使用时解释清楚。 策略模式的一个常见例子是支付方式的选择,比如支付宝、微信支付、信用卡支付等。不同的支付方式可以看作是不同的策略,用户可以根据需要切换。这样的例子比较贴近实际,容易理解。 接下来,我需要考虑如何用Java代码来展示这个例子。首先定义一个策略接口,比如PaymentStrategy,里面有一个支付方法。然后,各个具体策略类实现这个接口,比如AlipayStrategy、WechatPayStrategy。再有一个上下文类,负责持有策略对象,并调用其支付方法。 用户可能需要看到代码的结构和调用方式,所以需要展示接口、具体策略类、上下文类以及客户端的调用示例。此外,需要强调策略模式的优点,比如避免冗长的条件判断,提高代码的可维护性和扩展性。 还要注意代码的格式,确保正确使用Java语法,比如类定义、接口实现、主方法等。可能还需要解释上下文类的作用,以及如何通过构造函数或setter方法动态切换策略。 在解释过程中,要分步骤说明,先讲概念,再举例子,接着展示代码结构,最后总结优缺点。这样结构清晰,用户容易跟随。 可能用户也会想知道策略模式的应用场景,比如当有多个相似的类只在行为上不同时,或者需要在运行时选择不同的算法时。这时候可以简要提到这些应用场景,帮助用户更好地理解何时使用该模式。 最后,检查是否有遗漏的部分,比如是否提到了开闭原则,策略模式如何支持扩展而不修改现有代码。这些都是策略模式的重要优点,应该包含在解释中。 总结一下,我需要用支付方式的例子,分步骤解释策略模式的结构,展示Java代码,并讨论其优点和应用场景。这样用户就能全面理解策略模式,并知道如何在项目中应用。</think>我通过一个现实案例帮你理解策略模式,并用Java代码分步实现: 1. **核心思想**:把算法/行为封装成独立类,使它们可以像插件一样自由替换 2. **现实比喻**: - 支付方式选择(支付宝/微信/信用卡) - 导航策略(驾车/步行/公交) - 排序算法(快速排序/归并排序) 3. **Java代码实现**(支付场景): ```java // 步骤1:定义策略接口(算法标准) interface PaymentStrategy { void pay(int amount); } // 步骤2:实现具体策略类 class AlipayStrategy implements PaymentStrategy { @Override public void pay(int amount) { System.out.println("支付宝支付:" + amount + "元"); } } class WechatPayStrategy implements PaymentStrategy { @Override public void pay(int amount) { System.out.println("微信支付:" + amount + "元"); } } class CreditCardStrategy implements PaymentStrategy { @Override public void pay(int amount) { System.out.println("信用卡支付:" + amount + "元"); } } // 步骤3:创建上下文类(策略容器) class PaymentContext { private PaymentStrategy strategy; public PaymentContext(PaymentStrategy strategy) { this.strategy = strategy; } public void executePayment(int amount) { strategy.pay(amount); } // 允许动态切换策略 public void setStrategy(PaymentStrategy strategy) { this.strategy = strategy; } } // 步骤4:客户端使用 public class StrategyDemo { public static void main(String[] args) { PaymentContext context = new PaymentContext(new AlipayStrategy()); context.executePayment(100); // 输出:支付宝支付:100元 context.setStrategy(new WechatPayStrategy()); context.executePayment(200); // 输出:微信支付:200元 context.setStrategy(new CreditCardStrategy()); context.executePayment(300); // 输出:信用卡支付:300元 } } ``` 4. **模式优势**: - 消除复杂的条件判断(避免大量if-else) - 符合开闭原则(新增策略无需修改已有代码) - 算法可复用(策略类可被多个上下文使用) - 动态切换策略(运行时改变行为) 5. **应用场景**: - 需要多种算法变体 - 有多个仅在行为方式不同的相似类 - 需要隐藏算法实现细节 - 避免暴露复杂数据结构的类 **关键记忆点**:把会变化的部分(算法)封装起来,与不变的部分分离。就像给设备更换不同的电池,设备(上下文)不变,电池(策略)可换。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值