【Java】String是不可变的,真的如此吗?从原理深度剖析

11 篇文章 0 订阅

学Java的人或多或少都会得到这么一个信息:String是不可变的。那么果真如此吗?
本文前置知识:反射,Java内存模型

一、如何改变一个String

打开String的源码,赫然可以看见,其实String对象的数据储存在它的value数组中。
在早起版本的Java中,这是一个char[]类型的数组,较晚版本中替换为byte[]类型。

public final class String {
    private final byte[] value;
    // ……
}

那么,如果利用反射把这个数组替换掉,是不是就能改变String了呢?
接下来进行尝试。

创建一个modifyString方法,利用反射修改字符串中的value数组,并在main函数中测试效果(注意,在低版本Java中这里的byte[]应修改为char[]):

    private static void modifyString(String src, String dst) throws NoSuchFieldException, IllegalAccessException {
        Field valueField = String.class.getDeclaredField("value");
        valueField.setAccessible(true);
        byte[] newValue = dst.getBytes();
        valueField.set(src, newValue);
    }

    public static void main(String[] args) throws NoSuchFieldException, IllegalAccessException {
        String s = "hello, world!";
        modifyString(s, "you're so cool!");
        System.out.println("s = " + s);
    }

可以看到,输出显示s的确改变了!

s = you’re so cool!

一个大胆的想法

看到上面的结果之后,我有了一个大胆的想法。

同样是modifyString方法,但是main函数改成了下面这样:

    public static void main(String[] args) throws NoSuchFieldException, IllegalAccessException {
        String s = "hello, world!";
        modifyString(s, "you're so cool!");
        System.out.println("hello, world!");
    }

甚至直接这样:

    public static void main(String[] args) throws NoSuchFieldException, IllegalAccessException {
        modifyString("hello, world!", "you're so cool!");
        System.out.println("hello, world!");
    }

猜猜看会输出什么?有兴趣的可以自己试试。

二、原理简析

1. 字符串常量池

Java中的字符串会存储在字符串常量池中。理论上字符串常量池位于方法区,实际是存储是在堆中(见Java内存模型)。
字符串常量池中储存了使用过的字符串对象。当需要使用某个字符串时,首先在字符串常量池中查找有没有相应的对象,如果找到了就直接返回,否则就创建一个新的字符串对象,然后放进字符串常量池中。

当运行到s = "hello, world!"时,这个字符串类型的变量s就指向了对应常量池中的"hello, world!"对象。为了方便区分,这里称字符串常量池中的"hello, world!"对象helloworld
众所周知,Java中一切对象都是引用传递,当使用modifyString(s, "you're so cool!")方法修改字符串时,其实修改的就是helloworld.value。这样一来,相当于直接修改了常量池中字符串的值
s->helloworld对象

所以,当我们运行以下代码时:

    public static void main(String[] args) throws NoSuchFieldException, IllegalAccessException {
        String s = "hello, world!";
        String t = "hello, world!";
        modifyString(s, "you're so cool!");
        System.out.println(t);
    }

得到的输出是you're so cool!。本质上的原因就是,st都是指向的同一个字符串常量池中的对象helloworld。也就是说,st本质上只是类似于一个指针,真实的对象都是常量池中的helloworld
helloworld对象的内部值value被修改之后,表面上的感官即是st的值都发生了改变。

再看这段代码:

    public static void main(String[] args) throws NoSuchFieldException, IllegalAccessException {
        modifyString("hello, world!", "you're so cool!");
        System.out.println("hello, world!");
    }

同样的道理,最后System.out.println("hello, world!")输出的是you're so cool!,谜底揭晓,也是很神奇了。

2. new String()

通过new String()创建的字符串,情况就有所变化。
查看以下代码:

    public static void main(String[] args) throws NoSuchFieldException, IllegalAccessException {
        String s = "hello, world!";
        String t = new String("hello, world!");
        modifyString(s, "you're so cool!");
        System.out.println(t);
    }

最终输出hello, world!而不是you're so cool!,其原因是因为变量t指向的是在堆中创建的String对象而非字符串常量池中的helloworld对象。
new String()
在执行t = new String("hello, world!)时,会在堆内存中开辟一块空间放置这个对象,并把字符串常量池中的helloworldvalue数组赋值给t。下面是String类的构造方法:

    public String(String original) {
        this.value = original.value;
        this.coder = original.coder;
        this.hash = original.hash;
    }

当使用modifyString(s, "you're so cool!")修改字符串时,是把helloworld.value给替换掉了;而t.value没有被替换掉,仍旧是指向的"hello, world!"所对应的字节数组。如图所示:
value数组指向

看到这里,不知道你有没有一个大胆的想法?

3. 又一个大胆的想法

在上面的分析中,已经知道,对于以下代码:

    public static void main(String[] args) throws NoSuchFieldException, IllegalAccessException {
        String s = "hello, world!";
        String t = new String("hello, world!");
        modifyString(s, "you're so cool!");
        System.out.println("t = " + t);
    }

    private static void modifyString(String src, String dst) throws NoSuchFieldException, IllegalAccessException {
        Field valueField = String.class.getDeclaredField("value");
        valueField.setAccessible(true);
        byte[] newValue = dst.getBytes();
        valueField.set(src, newValue);
    }

t并没有被改变,仍然输出t = hello, world!。那么,如果改变modifyString的行为,使其直接修改value数组呢?

    private static void modifyString(String src, String dst) throws NoSuchFieldException, IllegalAccessException {
        Field valueField = String.class.getDeclaredField("value");
        valueField.setAccessible(true);
        byte[] oldValue = (byte[]) valueField.get(src);
        byte[] newValue = dst.getBytes();
        System.arraycopy(newValue, 0, oldValue, 0, Math.min(oldValue.length, newValue.length));
    }

这时候,再运行main函数,就发现,t的值也改变了。不过由于value数组的长度限制,只能显示原字符串的长度:

输出
t = you’re so coo

三、Android中的String

打开Android SDK中的String类,会发现里面已经没有value数组了。这是因为Android修改了String类的实现,直接在native层面管理value数组,而在String中加入了一个int类型的变量count,表示数组长度。
而一系列跟value相关的方法,比如charAtcompareTo等都改成了native方法。
并且,Android禁止了所有String的构造方法,创建字符串要么使用双引号的形式,要么使用StringFactory

Android这么做的原因据称是为了性能,能在字节码上面优化运行效率。我觉得这同时也是为了安全,避免对字符串常量池中的值进行修改。

反正,这个花活儿在Android里是玩不了了。

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值