如何理解java中String的不可变性

导读:本篇文章讲解 如何理解java中String的不可变性,希望对大家有帮助,欢迎收藏,转发!站点地址:www.bmabk.com

问题

为什么大家都在说String是不可变的?

String类的声明

public final class String
    implements java.io.Serializable, Comparable<String>, CharSequence {
    /** The value is used for character storage. */
    private final char value[];
    
    ...
    ...
   
    public String(char value[]) {
        this.value = Arrays.copyOf(value, value.length);
    }

java中String声明是final类型,说明String不可继承,String里面核心存储值的value是一个char[]数组也使用了final修饰,说明value的引用地址是不改变。

final关键字的作用

使用final修饰的变量,编译器不允许我们做修改的,举例说明:

    @Test
    public void finalTest(){
       final char[] value = {'a','a','a'};
        char[] value2 = {'b','b','b'};
        value = value2;//cannot assign a value to final variable
    }

上面value = value2这一行会报错,无法为final修饰的变量赋值,意思就是使用final修饰的变量的引用不能被更改。但是引用不能被更改不代表不能改变里面的值。如下说所示:

    @Test
    public void finalTest() {
        final char[] value = {'a', 'a', 'a'};
        System.out.println(value);
        value[2] = 'b';//将索引位置为2的修改为b
        System.out.println(value);
    }

打印结果:

aaa
aab

String的不变性

这里简单的例子测试一下String的不可变性

    @Test
    public void testString() {
        String test1 = new String("aaa");
        StringBuilder test2 = new StringBuilder("aaa");
        System.out.println("输出原始的test1:" + test1);
        System.out.println("输出原始的test2:" + test2.toString());

        //追加字符串之后
        String afaddStr = addString(test1);
        StringBuilder afaddStrBuild =  addStringBuilder(test2);
        System.out.println("输出原始的test1:" + test1);
        System.out.println("输出原始的test2:" + test2.toString());
    }

    /**
     * 向字符串中添加bbb
     *
     * @param str
     */
    public String addString(String str) {
        str = str + "bbb";
        return str;
    }

    /**
     * 向字符串中追加bbb
     *
     * @param str
     */
    public StringBuilder addStringBuilder(StringBuilder str) {
        str.append("bbb");
        return str;
    }

结果输出:

输出原始的test1:aaa
输出原始的test2:aaa
输出原始的test1:aaa
输出原始的test2:aaabbb

test1在一顿操作之后还保留了原来的样子,但是test2已经被改变了,其实String这种不可变性我们经常无意间使用,最常见的使用场景就是HashSet的值。看如下示例:

    @Test
    public void testString2() {
        String key1 = new String("aaa");
        StringBuilder key2 = new StringBuilder("aaa");
        HashSet set = new HashSet<>();
        set.add(key1);
        set.add(key2);
        System.out.println("原始存储的set值:"+ set.toString());
        /**************模拟业务操作******************/
        String afaddStr = addString(key1);
        StringBuilder afaddStrBuild =  addStringBuilder(key2);
        /**************模拟业务操作****end***********/
        System.out.println("业务执行后set值:"+ set.toString());
    }
    /**
     * 向字符串中添加bbb
     *
     * @param str
     */
    public String addString(String str) {
        str = str + "bbb";
        return str;
    }
    /**
     * 向字符串中追加bbb
     *
     * @param str
     */
    public StringBuilder addStringBuilder(StringBuilder str) {
        str.append("bbb");
        return str;
    }

如果使用StringBuilder存储则我们无意间的业务操作会改变Set中的值,这可能引起不必要的麻烦。

String的不变性有哪些好处

  1. 只有当字符串是不可变的,字符串池才有可能实现。字符串池的实现可以在运行时节约很多heap空间,因为不同的字符串变量都指向池中的同一个字符串。如果字符串是可变的,那么String interning将不能实现(String interning是指对不同的字符串仅仅只保存一个,即不会保存多个相同的字符串),因为这样的话,如果变量改变了它的值,那么其它指向这个值的变量的值也会一起改变。
  2. 如果字符串是可变的,那么会引起很严重的安全问题。譬如,数据库的用户名、密码都是以字符串的形式传入来获得数据库的连接,或者在socket编程中,主机名和端口都是以字符串的形式传入。 因为字符串是不可变的,所以它的值是不可改变的,否则黑客们可以钻到空子,改变字符串指向的对象的值,造成安全漏洞。
  3. 因为字符串是不可变的,所以是多线程安全的,同一个字符串实例可以被多个线程共享。这样便不用因为线程安全问题而使用同步。字符串自己便是线程安全的。
  4. 因为字符串是不可变的,所以在它创建的时候hashcode就被缓存了,不需要重新计算。这就使得字符串很适合作为Map中的键,字符串的处理速度要快过其它的键对象。这就是HashMap中的键往往都使用字符串。

String真的是绝对不可变吗

看如下例子:

    @Test
    public void testString3() throws IllegalAccessException, NoSuchFieldException {
        String strObj = new String("aaa");
        System.out.println("反射执行前字符串:" + strObj);
        System.out.println("反射执行前的hash值:" + strObj.hashCode());
        Field field = strObj.getClass().getDeclaredField("value");
        field.setAccessible(true);
        char[] value = (char[]) field.get(strObj);
        value[2] = 'b';
        System.out.println("反射执行后字符串:" + strObj);
        System.out.println("反射执行后的hash值:" + strObj.hashCode());
    }

打印结果如下:

反射执行前字符串:aaa
反射执行前的hash值:96321
反射执行后字符串:aab
反射执行后的hash值:96321

说明通过反射我们是可以修改String的值的

参考如下:

https://www.zhihu.com/question/20618891

版权声明:本文内容由互联网用户自发贡献,该文观点仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 举报,一经查实,本站将立刻删除。

文章由极客之音整理,本文链接:https://www.bmabk.com/index.php/post/72716.html

(0)
小半的头像小半

相关推荐

极客之音——专业性很强的中文编程技术网站,欢迎收藏到浏览器,订阅我们!