对String为什么是不可变类的思考

众所周知,字符串有三贱客,分别为StringStringBuilderStringBuffer。三者之内,有且仅有String是不可变类。而我们常见的不可变类有String类和Interger类,在这里我仅对String类进行思考。

String对象的创建

原理一:当使用任何方式来创建一个字符串对象str时,运行中的JVM虚拟机会拿着这个对象在String Pool(字符串常量池/缓冲区)中找是否存在内容相同的字符串对象,如果不存在,则在池中创建一个字符串str,否则不在池中添加。

原理二:Java中,只要使用new关键字来创建对象,则一定会在堆区或者栈区创建一个新的对象

String str1="helloworld";
String str2=new String("helloworld");
//str1指向的对象放在栈中,
//str2指向的对象放在堆中(存放的是字符串"helloworld"在内存中的地址)

原理三:使用指定或者使用纯字符串串联来创建String对象,则仅仅会检查维护String Pool中的字符串,池中没有就在池中创建一个,有就不用。但绝对不会在堆栈区中再去创建String对象

原理四:使用包含变量的表达式来创建String对象,则不仅仅会见检查维护String Pool,而且还会在堆栈区创建一个String对象

在此,有两问:

为什么String类是不可变类?

这样设计String类的好处是什么?


第一个问题,为什么String类不可变?

我先去翻阅了String类的源码,正所谓万物皆可看源码。

从上图可见,String类是被final这个关键字所修饰的,实现了Serializable、Comparable、CharSequence接口

final关键字可以用来修饰引用、方法和类。在这里我不详细介绍final修饰引用和方法的作用,重点在final修饰类后的作用是什么。

  • 当用final修饰类时,该类成为最终类,无法被继承,该类就不能被其他类所继承;简称为“断子绝孙类”。当我们需要让一个类永远不被继承,此时就可以用final修饰,但要注意:final类中所有的成员方法都会隐式的定义为final方法

从上述可知,String类是不能被继承,并且它的成员方法都默认为final方法

String类的本质是一个不可变char数组String类的值实际上是通过char数组存储的,并且char数组privatefinal所修饰,因此字符串一旦创建就不能再修改了,从而保证了引用的不可变和对外的不可见

扩展:我今天刚学成反射这个内容,既然私有的成员变量可以利用暴力反射成功修改属性,那么String类是否可以通过setAccessible()方法成功被修改呢?

import java.lang.reflect.Field;

/**
 * 字符串的测试
 *
 * @author HHLJ
 * @date 2022/01/18
 */
public class StringTest {
    public static void main(String[] args) throws NoSuchFieldException, IllegalAccessException {
        //创建三个字符串,一个用于修改,两个用于比较
        String str1 = "阿弥陀佛";
        String str2 = "阿弥陀佛";
        String str3 = "无量天尊";
        //阿弥陀佛
        System.out.println(str1);
        //获取String类中的value字段
        Field stringField = str1.getClass().getDeclaredField("value");
        //改变stringField的访问权限
        stringField.setAccessible(true);
        //修改str1的数据
        stringField.set(str1,new char[]{'无','量','天','尊'});
        //无量天尊
        System.out.println(str1);
        //1170471009
        System.out.println(str1.hashCode());
        //比较
        //true
        System.out.println(str1 == str2);
        //false
        System.out.println(str1 == str3);
    }
}

扩展:由此可见,String类并不是完全不可变,我们利用反射绕过了私有权限修改了String底层的char[]数组。但是这种方式不推荐使用,不建议这么使用,这违反了 Java 对 String 类的不可变设计原则,会造成一些安全问题,整整活稍微了解了解就可以了。

第二个问题,这样设计String类的好处是什么?

主要出于这两种原因:安全性、效率

安全性

因为字符串是不可变的,所以是多线程安全的,同一个字符串实例可以被多个线程共享。这样便不用因为线程安全问题而使用同步。

String被许多的Java类用来做参数,比如网络连接地址的url,文件路径path,反射机制所需要的String参数等通常都是用String类来保存,如果String对象不是固定不变的话,将会产生很多安全隐患。

效率

字符串不变性保证了HashCode的唯一性,因此可以放心的进行缓存,这也是一种性能优化手段,意味着不必每次都取计算新的哈希值。

只有当字符串是不可变的,字符串池才有可能实现,字符串常量池是java堆内存中一个特殊的存储区域,当创建一个String对象,假如此字符串值已经存在于常量池中,则不会创建一个新的对象,而是引用已经存在的对象。

这样在大量使用字符串的情况下,可以节省内存空间提高效率,String的不可变性是实现该特性的最基本的一个必要条件。假设内存里的字符串内容可以被我们改来改去,那么这么做就毫无意义可言。