乐趣区

String真的不可变吗

前言

提到 JavaString,都会提起 String 是不可变的。但是这点是真的吗?String的不可变是否可以破坏呢?

在验证之前,首先还是需要介绍一下 String 的不可变特性。

PS:这里还要提到自己遇到的面试题:

String str = "abc";


// 输出 def
System.out.println(str);

String 不可变特性

String的不可变指的是

  • String内部是使用一个被 final 修饰 char 数组 value 存储字符串的值
  • 数组 value 的值在对象构造的时候就已经进行了赋值
  • String不提供方法对数组 value 中的值进行修改
  • String中需要对 value 进行修改的方法(例如 replace)则是直接返回一个新的String 对象

所以 String 是不可变的。

破坏 String 的不可变

String的不可变其实主要是围绕 value 是一个值不可修改的 char 数组来实现的,但是利用 Java 的反射完全可以破坏这个特性。

关键代码如下:

        String str="test";
        // str 对象的引用地址
        printAddresses("str",str);
        try {Field field=str.getClass().getDeclaredField("value");
            //
            char[] newChars={'h','e','l','l','o'};
            // 使 private 属性的值可以被访问
            field.setAccessible(true);
            // 替换 value 数组的值
            field.set(str,newChars);
            // 替换后的值
            System.out.println("str value:"+str);
            // 替换后,str 对象的引用地址
            printAddresses("str",str);
        } catch (NoSuchFieldException e) {e.printStackTrace();
        } catch (IllegalAccessException e) {e.printStackTrace();
        }

上面打印对象地址的方法是引用了如何获取到 Java 对象的地址的示例代码,内容如下:

    /**
     * 打印对象地址
     * @param label
     * @param objects
     */
    public void printAddresses(String label, Object... objects) {System.out.print(label + ":0x");
        long last = 0;
        Unsafe unsafe=getUnsafe();
        int offset = unsafe.arrayBaseOffset(objects.getClass());
        int scale = unsafe.arrayIndexScale(objects.getClass());
        switch (scale) {
            case 4:
                // 64 位 JVM
                long factor = 8;
                final long i1 = (unsafe.getInt(objects, offset) & 0xFFFFFFFFL) * factor;
                System.out.print(Long.toHexString(i1));
                last = i1;
                for (int i = 1; i < objects.length; i++) {final long i2 = (unsafe.getInt(objects, offset + i * 4) & 0xFFFFFFFFL) * factor;
                    if (i2 > last)
                        System.out.print(", +" + Long.toHexString(i2 - last));
                    else
                        System.out.print(", -" + Long.toHexString(last - i2));
                    last = i2;
                }
                break;
            case 8:
                throw new AssertionError("Not supported");
        }
        System.out.println();}

    /**
     * 通过反射获取 Unsafe 对象
     * @return
     */
    private static Unsafe getUnsafe() {
        try {Field theUnsafe = Unsafe.class.getDeclaredField("theUnsafe");
            theUnsafe.setAccessible(true);
            return (Unsafe) theUnsafe.get(null);
        } catch (Exception e) {throw new AssertionError(e);
        }
    }

运行结果

str:0x76b4136e0
str value:hello
str:0x76b4136e0

从运行结果可以得知,使用反射可以对 String 对象的值进行修改,同时不会修改这个对象的对象地址。

结论

其实使用反射来破坏 String 的不可变存在取巧成分,但是实际上反射也是 Java 提供的特性,那么被人拿来使用就很难避免。

当时遇到前面提到的面试题的时候,还一直认为此题无解,但是随着自己不断学习后才发现,很多时候换个角度就能发现不同的办法。

退出移动版