String类为什么是不可变的,真的不可变么?

        前几天,有个实习生问我,String类为什么被设计成不可变的,说他看见网上的都说可变。我看了看他,说:那篇博客你没看完吧!他挠了挠头回答说:没有!我又问他:知道string类有个变量value么?他说知道,我又问这个变量有哪些修饰符修饰的,他又挠了挠头,笑着说:忘了,好像有个private。我半开玩笑的笑着说:可以啊,这都能记着!然后让他先把bug解决了晚上在告诉他!有的小伙伴就要问了,为什么不直接告诉他还有个final呢?并且这个value变量还是个char数组呢?其实不是不想直接告诉他,而是他还是个实习生,不想让他丧失独自解决问题的能力和独自学习的能力,旁敲侧击应该会更好一些!好了,不扯了,进入正题了!

        正如标题所说:String类为什么被设计成不可变?真的不可变么?首先回答第一个问号:String类为什么被设计成不可变?其实这个问题很简单,最重要的一点就是安全,当然还要考虑到综合原因,比如内存,数据同步,数据结构,设计考虑,效率优化问题等。看源码:

1public final class String
2    implements java.io.Serializable, Comparable<String>, CharSequence {
3    /** The value is used for character storage. */
4    private final char value[];
5
6    /** Cache the hash code for the string */
7    private int hash; // Default to 0
8
9   ...

源码第四行可以看到,这个value变量是char数组类型的,也就是说,字符串的值都保存在这个数组中的。再接着看,有个关键字final。为什么呢?很简单,因为数组是引用了对象,为了保证数组不可变而加了个final。那么,问题又来了,final只是保证引用的地址不可变,值还是可变的呀!(LZ此时会心一笑,想到了小伙伴们肯定要用反射这个骚操作来来迫不及待的证明了)我们知道,一个类要被设计成不可变类,要遵循以下几条规则:

1.其状态被创建后不能再被修改

2.所有域都应该是 private final的(但只做到这一步还不够,因为如果成员变量是对象,它保存的只是引用,有可能在外部改变其引用指向的值)

3.其构造函数构造对象期间,this引用没有泄露。

4.只提供成员变量的get方法,不能提供set方法(避免通过其他接口改变成员变量的值,破坏不可变特性。)

5.如果类中包含可变类对象,那么返回给客户端的时候,返回该对象的一个深拷贝,而不是该对象本身

说到这里,再来看string类源码:

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
    ....
    **
     * Allocates a new {@code String} so that it represents the sequence of
     * characters currently contained in the character array argument. The
     * contents of the character array are copied; subsequent modification of
     * the character array does not affect the newly created string.
     *
     * @param  value
     *         The initial value of the string
     */
    public String(char value[]) {
        this.value = Arrays.copyOf(value, value.length);
    }
    ......
    **
     * Converts this string to a new character array.
     *
     * @return  a newly allocated character array whose length is the length
     *          of this string and whose contents are initialized to contain
     *          the character sequence represented by this string.
     */
    public char[] toCharArray() {
        // Cannot use Arrays.copyOf because of class initialization order issues
        char result[] = new char[value.length];
        System.arraycopy(value, 0, result, 0, value.length);
        return result;
    }

如上源码所示,可以观察到String类的设计符合上面总结的不变类型的设计原则。其次,String对象的不可变是由于对String类型的所有改变内部存储结构的操作都会new出一个新的String对象。

一.不可变的好处:

1.安全:文章开头我们说了,被设计成不可变的最重要的原因就是安全,这也是好处。

             多线程角度看,如果俩个现场同时读取一个字符串,其中一个线程修改了值,如果字符串可变,那么另一个线程必然读取的数据和之前不同,试想一下,这是一件多么可怕的事情!

              类加载角度看,假入你想加载Java.util.*,而加载过程中被修改成了java.P*,哇,这下你写的代码全乱套了,数据库更是乱成了一锅粥,或爆出各种破坏性的错误,头大哟!

2.hashcode缓存:由于字符串的不可变,在创建对象时,hashcode就被缓存了,不需要重新计算。这也是Map喜欢将字符串作为键的原因,因为都缓存了,快啊,比其他对象作为键都要快!

3.节省字符串常量池的空间:(字符串常量池这个概念后续文章会详细解析)因为字符串不可变性,会有多个引用指向常量池中的同一个地址,节省了堆的空间,如果字符串可变,那么堆就要开辟N多空间来保存字符串,累啊,还浪费空间!

二.缺点:缺点就是由于不可重用性,会使得它们被用玩就仍,垃圾回收器很头大的,会产生很多垃圾!

好了,第一个问号解释完了,该第二个问号了。

第二个问号很好解答,上文中说过了呀:反射。看代码:

String s = "Hello World"; 	//创建字符串"Hello World", 并赋给引用s
System.out.println("s = " + s); 	    
//获取String类中的value字段
Field valueFieldOfString = String.class.getDeclaredField("value");
valueFieldOfString.setAccessible(true); 	//改变value属性的访问权限
char[] value = (char[]) valueFieldOfString.get(s);
value[5] = '_'; //改变value所引用的数组中的第5个字符
System.out.println("s = " + s);  			//Hello_World

打印结果为:

s = Hello World

s = Hello_World

喏,s的值变了吧,从始至终s的引用都指向一个地址,值却被改变了,哈哈哈!(这个骚操作其实是LZcopy的,LZ实在不想写了,困呀,上下眼皮都在打架了。但LZ不建议这么干!这样会破坏不可变特性的哟)

  • 7
    点赞
  • 19
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值