String的不可变性(JDK8)

66 阅读2分钟

Java中的String是一个不可变的类,那么它的不可变性到底体现在哪?是不是因为底层使用final修饰就决定了它是不可变的呢?请带着这些问题一起阅读本文。

String的不可变性体现在哪里?

让我们来看下面这段代码

String s1 = "hello";
s1 = "world";

那么问题来了,是s1指向的内存地址上的值由"hello"变味了"world"了吗?答案:不是的。

image-20230220142247379.png

上面这张图清楚了展示了整个过程,执行完s1="world"代码之后,只是改变了s1这个引用地址,不会对原来指向内存地址上的值。

为什么String不可变呢?

让我们来看一下String类中所定义的成员变量

public final class String
    implements java.io.Serializable, Comparable<String>, CharSequence {
    
    private final char value[];
    private int hash; 
    private static final long serialVersionUID = -6849794470754667710L;

从上面这些信息可以看出

  • String类由final修饰,即不可被继承。
  • String 实现了Serializable接口:表示字符串是支持序列化的;实现了Comparable接口:表示String可以比较大小
  • 字符串在JDK8版本实际上使用char数据进行存储,而final修饰保证不能被重新赋值,即‘不可变’,那么仅仅靠一个final就解决问题了吗?下面会进行讲解

只靠final就解决问题了吗?

省流:不是的

看下面代码

final char[] value = {'a', 'b', 'c'};
value[2] = 'd';

而此时value中的内容已经变为了a,b,d了,说明final无法保证值不会改变,只是保证了final修饰的value不能被重新赋值。那么private就出场了,private首先保证外界是无法修改其值的。那么接下来看看String类内部是否存在修改value的方法呢?String类内部的方法都是首先将value复制一份,再进行对应操作,最终返回一个新的字符串对象,比如replace、concat方法等,下面以replace方法举例

public String replace(char oldChar, char newChar) {
    if (oldChar != newChar) {
        int len = value.length;
        int i = -1;
        char[] val = value; /* avoid getfield opcode */

        while (++i < len) {
            if (val[i] == oldChar) {
                break;
            }
        }
        if (i < len) {
            // 声明一个新的char[]
            char buf[] = new char[len];
            for (int j = 0; j < i; j++) {
                buf[j] = val[j];
            }
            while (i < len) {
                char c = val[i];
                buf[i] = (c == oldChar) ? newChar : c;
                i++;
            }
            // 返回新的字符串对象
            return new String(buf, true);
        }
    }
    return this;
}

String真的不可变吗?

那么用什么方法可以访问到private修饰的value呢?没错,用反射。反射可以拿到String中的value属性,从而可以进行修改。

public void reviseStr() throws Exception {
	String s1 = "Hello World"; 
	System.out.println("反射前字符串s1为:" + s1);	//Hello World
    
	//1.通过反射拿到String类中的value字段
	Field valueField = String.class.getDeclaredField("value");
	//2.改变value属性的访问权限
	valueField.setAccessible(true);
	//3.获取s对象上的value属性的值
	char[] value = (char[]) valueField.get(s1);
	//4.改变value所引用的数组中的第5个字符
	value[5] = ',';
    
	System.out.println("反射后字符串s1为:" + s1);  //Hello,World
}

image-20230220145515259.png