【JAVA】String为什么不可变

一、前言

关于String的介绍,可以先参考我的另外一篇文章【JAVA】String源码浅谈

本篇文章,主要来探讨String的不可变性


二、到底什么是不可变

可以这样理解,一个对象在创建完成后,不能去改变它的状态,不能改变它的成员变量。如果变量包含基本数据类型,那么这个基本数据类型的值不能改变;如果包含引用类型,那么这个引用类型的变量不能指向别的对象,而且该引用类型指向的变量的状态也不能改变。

这里引用一下比较官方的解释,Effective Java 中第 15 条 使可变性最小化 中对 不可变类 的解释:

不可变类只是其实例不能被修改的类。每个实例中包含的所有信息都必须在创建该实例的时候就提供,并且在对象的整个生命周期内固定不变。为了使类不可变,要遵循下面五条规则:

  1. 不要提供任何会修改对象状态的方法。
  2. 保证类不会被扩展。 一般的做法是让这个类称为 final 的,防止子类化,破坏该类的不可变行为。
  3. 使所有的域都是 final 的。
  4. 使所有的域都成为私有的。 防止客户端获得访问被域引用的可变对象的权限,并防止客户端直接修改这些对象。
  5. 确保对于任何可变性组件的互斥访问。 如果类具有指向可变对象的域,则必须确保该类的客户端无法获得指向这些对象的引用。
     

三、新手的疑惑

public class Main {
    public static void main(String[] args) {
        String a="abcd";
        a="efg";
        System.out.println(a);
    }
}

这段代码可以输出efg,a确实String类型的啊,那现在a被改变了,说明String是可变的啊。

我们可以输出a在改变前后所指向的对象地址

可以看得出,a在改变前后,所指向的对象地址不同,也就是说,a仅仅是指向了不同的对象,但对象abcd本身的状态并未改变,"abcd"还是那个"abcd",所以说,String是不可变的。


四、不可变类的好处

【1】安全可靠

String在java中用处广泛,例如数据库的连接字符串、网络请求的url、io操作的文件名与类加载机制传递的类名字符串等。如果String是可变的,将会十分危险。

扫描二维码关注公众号,回复: 10431860 查看本文章

【2】简单高效

        1、String缓存其哈希码

多次调用String对象的hashCode方法,最多只会在第一次调用时,计算它的哈希码,此后的调用,将直接返回缓存的哈希码。这使得,它作为HashMap的健时,计算索引位置非常的高效。

这提醒我们,不要用可变对象做HashMap或HashSet的键,否则特别容易引起匪夷所思的问题。

2、String常量池

关于String的常量池有关的内容,可以参考我的另外一篇文章【JAVA】字符串的创建与存储机制

有了String常量池,那么在大量使用字符串的情况下,可以节省内存空间,提高效率。但之所以能实现这个特性,String的不可变性是最基本的一个必要条件。要是内存里字符串内容能改来改去,这么做就完全没有意义了。

【3】线程安全

不可变对象本质上就是线程安全的,它们不要求同步,不可变对象可以被自由地共享。


五、String真的无法改变吗

仔细观察String的成员变量,可以发现,value数组虽然被final修饰,但是我们依然可以改变数组中元素的内容,只是不能更改引用的指向罢了。

public final class String
    implements java.io.Serializable, Comparable<String>, CharSequence {
    /** The value is used for character storage. */
    private final char value[];

    /** Cache the hash code for the string */
    private int hash; // Default to 0

    /** use serialVersionUID from JDK 1.0.2 for interoperability */
    private static final long serialVersionUID = -6849794470754667710L;

}

现在尝试一下,改变某个元素内容。还是老方法,利用反射机制。

import java.lang.reflect.Field;

public class Main {

    public static void main(String[] args) {

        String s = "abcd";
        System.out.println("改变前的地址:" + System.identityHashCode(s));
        System.out.println("改变前的哈希:"+s.hashCode());
        try {
            Field field = String.class.getDeclaredField("value");
            field.setAccessible(true);
            char[] array = (char[]) field.get(s);
            array[0] = 'b';
            System.out.println(s);
            System.out.println("改变后的地址:" + System.identityHashCode(s));
            System.out.println("改变后的哈希:"+s.hashCode());
        } catch (NoSuchFieldException | IllegalAccessException e) {
            e.printStackTrace();
        }
    }
}

输出:

可以看得出,改变前后的字符串的内存地址没有发生变化,哈希值也没有发生变化,但是字符串的内容确实是发生了本质的变化。不得不说啊,反射真的是可以做出违反语言设计规则的事情出来。

这个例子也说明了,如果一个对象是通过其他某些对象组合起来的,如果这些对象是易变的,那么这个对象大概率是可变对象。

反射只是一个极端的例子,我们在日常编程中,不会利用反射去修改String,这将导致很多意料之外的问题,比如代码有功能上的错误,降低可移植性等。

发布了177 篇原创文章 · 获赞 583 · 访问量 115万+

猜你喜欢

转载自blog.csdn.net/qq_33591903/article/details/105051808
今日推荐