【面试】从源码角度一文带你深入理解String的不可变性

        我们都知道,在Java中,String类型是除了基本数据类型外最重要的也是最常用的一个类型,它最大的特点就是不可变性。那么,什么才是“不可变”?造成String不可变的原因是什么、这么设计有什么好处?这是面试中经常会被问到的问题。

一 什么是不可变性?

        在计算机科学中,不可变性(Immutability)指的是对象一旦被创建后,其状态就不能被修改。在Java中,意味着一旦一个String对象被创建,包含在这个对象中的字符序列就不可改变,即不能修改字符串中的字符。这种特性在Java的多个方面都有深远的影响,包括性能优化、线程安全以及字符串常量池等。

二 String类型的不可变性如何实现的?

        提到不可变性的实现原理,大多数人可能会想到字符串常量池,为了提高性能和减少内存消耗,Java虚拟机(JVM)维护了一个字符串常量池。当创建字符串常量时,JVM会首先检查字符串常量池中是否已经存在相同的字符串。如果存在,就直接返回常量池中的引用;如果不存在,就在常量池中创建一个新的字符串对象,并返回这个新对象的引用。由于String的不可变性,保证了字符串常量池中的字符串对象不会被修改,从而保证了引用的一致性。

        但我们可以进一步深入分析,从源码角度解释这个问题:

public final class String
    implements java.io.Serializable, Comparable<String>, CharSequence {
    /** The value is used for character storage. */
    private final char value[];
    
    ...
    ...
   
    public String(char value[]) {
        this.value = Arrays.copyOf(value, value.length);
    }

        我们可以看到,在String的底层实现逻辑中,String同样是通过char[]数组实现的,但是char[]数组被final进行了修饰,因此该数组的地址是不可变的,只能改变数组中元素的内容。而String类本身也由final修饰,因此该类不能被继承,没有子类,类中的方法也就不能被重写。此外,String类中并没有提供修改方法来修改数组中的元素内容,setCharAt()或append()等,因此无法改变数组元素。综合起来,String类型的变量所指向的数组完全不可变,因此想要改变变量内容,只能改变地址指向,指向一个新的地址。 

        同样,这也为我们在后续的程序中提供了一个解决问题的思路,即如何设计出一个安全、高效的不可变类?我们可以考虑如下几个方面。

  1. 将所有成员变量声明为private final
    这是实现不可变性的基础。通过将成员变量声明为private,可以确保外部代码无法直接访问这些变量,从而避免外部修改。将成员变量声明为final则确保了这些变量一旦被初始化之后就不能再被修改。

  2. 不提供修改成员变量的方法
    除了将成员变量声明为private final之外,还需要确保类中没有提供任何可以修改这些成员变量的方法。这意味着类中的所有方法都应该只返回成员变量的值,而不是它们的引用(对于对象类型的成员变量),或者根本不修改任何状态。

  3. 创建新的对象来修改状态
    如果需要“修改”一个不可变对象的状态,通常的做法是创建一个新的对象,该对象包含了所有需要的修改,并返回这个新对象的引用。这种方法在String类中通过连接操作(如使用+操作符或StringBuilder/StringBuffer)来体现。

  4. 确保没有可变的子对象
    如果类包含对可变对象的引用作为成员变量,那么即使这个类本身被设计为不可变的,这些可变的子对象仍然可以被修改,从而间接地影响不可变对象的状态。因此,需要确保所有的子对象也都是不可变的,或者至少不会通过不可变类的公共接口暴露出来。

  5. 使用不可变集合
    如果类包含集合作为成员变量,应该使用不可变集合(如Collections.unmodifiableListImmutableList等)来确保集合本身也是不可变的。

  6. 使用@Immutable注解(可选)
    虽然这不是Java语言本身提供的功能,但一些库(如Immutables、Lombok等)提供了@Immutable注解,用于在编译时帮助开发者检查类是否满足不可变性的要求。这些注解通常与代码生成或静态分析工具一起使用,以自动实现不可变性或验证现有的不可变性实现。

  7. 通过构造函数初始化所有成员变量
    为了确保对象在创建时具有明确的状态,并且这个状态之后不会被修改,应该通过构造函数来初始化所有的成员变量。这样可以确保对象一旦创建就处于完全初始化的状态,并且之后的状态不会改变。

三 这么设计有什么好处?

线程安全性:由于String对象的不可变性,多线程环境下无需担心对字符串对象的修改导致的数据竞争问题。

安全性:不可变性确保了字符串对象的内容不会被意外修改,从而增强了程序的安全性。

缓存利用:String常量池中的字符串对象可以被多个引用共享,从而节省了内存空间。

思考:String类型一定不可变吗?

我们可以通过反射来修改。

@Test
public void testString3() throws IllegalAccessException, NoSuchFieldException {
    String strObj = new String("aaa");
    System.out.println("反射执行前字符串:" + strObj);
    System.out.println("反射执行前的hash值:" + strObj.hashCode());
    Field field = strObj.getClass().getDeclaredField("value");
    field.setAccessible(true);
    char[] value = (char[]) field.get(strObj);
    value[2] = 'b';
    System.out.println("反射执行后字符串:" + strObj);
    System.out.println("反射执行后的hash值:" + strObj.hashCode());
}

// 结果
反射执行前字符串:aaa
反射执行前的hash值:96321
反射执行后字符串:aab
反射执行后的hash值:96321
  • 34
    点赞
  • 9
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值