美文网首页Android进阶
【Java】String是不可变的,真的如此吗?从原理深度剖析!

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

作者: littlefogcat | 来源:发表于2021-02-06 11:57 被阅读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之殇

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

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

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

    相关文章

      网友评论

        本文标题:【Java】String是不可变的,真的如此吗?从原理深度剖析!

        本文链接:https://www.haomeiwen.com/subject/rswntltx.html