String类不可变的原因及优点

首先我们想知道为什么字符串是不可变的,就要先理解字符串的概念
字符串在存储上类似字符数组,所以它每一位的单个元素都是可以提取的,如s=“abcdefghij”,则s[0]=“a”。

再弄清String类不可变是什么意思,了解什么是不可变对象。
不可变对象可以理解为:如果一个对象,在它正确创建完成之后,不能再改变它的状态(包括基本数据类型的值不能改变,引用类型的变量不能指向其他的对象,引用类型指向的对象的状态也不能改变),那么这个对象就是不可变的。

String s=new String ("wo");
String s1=new String("de");
s=s+s1;
System.out.println(s); //结果为wode

首先在栈中有个"s"变量指向堆中的"wo"对象,栈中"s1"变量指向堆中的"de"对象当执行到s = s + s1;系统重新在堆中new一个更大的数组出来,然后将"wo"和"de"都复制进去,然后栈中的"s"指向这个新new出来的数组…

所谓的不可变是指:它没有在原数组“wo”上进行修改,而是新建了个更大数组进行扩展,也就是说,这时候堆里还是有“wo”这个对象数组存在的,只不过这个时候"s"变量不在指向"wo"这个数组了,而是指向了新new出来的数组,这就是和StringBuffered的区别,后者是在原数组上进行修改,改变了原数组的值,StringBuffered不是通过新new一个数组去复制,而是在原数组基础上进行扩展…再让变量指向原数组…

这里有必要讲清用final修饰的“引用”即引用变量,和“final修饰的引用的对象”即“引用变量所指向的对象中的内容”的区别。
 使用final关键字修饰一个变量时,是指引用变量不能变,引用变量所指向的对象中的内容还是可以改变的。例如

String  str="hello";
str="world";

虽然看了String的源码,知道String类是用final修饰的,String在本质上也是字符数组,而且String源码里也是用final修饰字符数组这个变量的。但是final只能保证:类不可被继承、属性不可变、方法不可被重写。和String不可变无关。
上面的例子,String类型的变量str的引用地址不可变,但引用所指向的对象的内容在常量池中的值是可以变的。
str的值看似改变了,其实也是同样的误区。再次说明, str只是一个引用, 不是真正的字符串对象,在调用时str=“world”;, 方法内部创建了一个新的String对象,并把这个新的对象重新赋给了引用str。
我们从下图可以看到,当定义String str = "world"时,其实不是真正改变了str的内容,而是改变了str的引用。

逻辑如图
在这里插入图片描述
综上可以知道当满足以下条件时,对象才是不可变的:
对象创建以后其状态就不能修改。
对象的所有域都是final类型的。
对象是正确创建的(在对象的创建期间,this引用没有溢出)。

在讲String类不可变之前,还有必要说一下字符串池(常量池)
字符串池(字符串特定池)是方法区域中的一个特殊存储区域。当创建字符串时,如果字符串已经存在于池中,则将返回现有字符串的引用,而不是创建新对象。

下面的代码只在堆中创建一个字符串对象:

String string1 = "abcd";
String string2 = "abcd";

逻辑如图
在这里插入图片描述
这种方式一开始就直接在常量池中去找“abcd",找不到就创建,在栈中string1的引用指向常量池中的“abcd”,由于string1和string2是两个字符串字面量对象,所以string1和string2在内存的堆中是同一个字符串对象,zstring2的引用也指向常量池中的同一个“abcd",故

(string==string2)==true

这个时候可以逆向思维,如果字符串是可变的,用一个引用更改字符串将导致其他引用的值错误。
但如果下面这样就是另外的情况了

String string3 = new String("hello");
String string4 = new String("hello");
String string5 = new String("hello");

这时候string3、string4和string5,new了3个对象,每调用构造方法new一次对象,就在栈中开僻了3个不同的空间,分别指向堆中不同的3个实例的对象,虽然这3个实例都指向了常量池中同一个"hello",但从栈到堆是3个不同的引用。

逻辑如图
在这里插入图片描述

(string3==string4||string3==string5||string4==string5)==false

下图的代码又是一种情况

//字面量对象
String a1 = "abc"; 
//构造对象
String a2 = new String (abc); 
String a3 = a1;

a1存放的是abc这个值的地址,a1放在栈里边,对象a1指向abc的地址。
a1的值,放在字符串常量池中,而字符串常量池是堆中的一个特殊区域(这里解释一下jdk1.7才把字符串常量池放到堆里)。

a2是通过new一个对象创建的,它是在堆中另开辟一个新空间,JVM首先会对这个字面量(abc)进行检查,如果字符串常量池中存在相同内容的字符串对象的引用,则将这个引用返回,否则新的字符串对象被创建,然后将这个引用放入字符串常量池。
由于a1已经使字符串常量池中存在了abc,所以a2在堆中开辟的空间用来存储字符串常量池中abc的地址。
对a3来说,就是a3只拷贝了a1的地址,并没有拷贝a1的值,和上面所述的概念原理一样。

在这里插入图片描述
逻辑如图
在这里插入图片描述
经过上面的铺垫,可以总结下字符串为什么不可变的原因。
要理解String的不可变性,首先看一下String类中都有哪些成员变量。 在JDK1.8中,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

其中,成员变量hash并没有用final声明,但是由于第一次调用hashCode()会重新计算hash值,并且以后调用会使用已缓存的值,当然最关键的是每次计算时都得到相同的结果,所以也保证了对象的不可变。

public int hashCode() {
        int h = hash;
        if (h == 0 && value.length > 0) {
            char val[] = value;

            for (int i = 0; i < value.length; i++) {
                h = 31 * h + val[i];
            }
            hash = h;
        }
        return h;
    }

在Java中,数组也是对象, 所以value也只是一个引用,它指向一个真正的数组对象。其实执行了String s = “ABCabc”; 这句代码之后,真正的内存布局应该是这样的:
在这里插入图片描述
value是String封装的数组,value中的所有字符都是属于String这个对象的。由于value是private的,并且没有提供setValue等公共方法来修改这些值,所以在String类的外部无法修改String。也就是说一旦初始化就不能修改。此外,value变量是final的, 也就是说在String类内部,一旦这个值初始化了,value引用类型变量所引用的地址不会改变,即一直引用同一个对象。所以可以说String对象是不可变对象。但其实value所引用对象的内容完全可以发生改变(反射消除String类对象的不可变特性)。

原因总结

1.字符串在内存中的存储:字符串本质是数组,数组在创建时就开辟了一个连续地址,固定的空间的内存。
2…字符串池的要求:字符串池(字符串特定池)是方法区域中的一个特殊存储区域。当创建字符串时,如果字符串已经存在于池中,则将返回现有字符串的引用,而不是创建新对象。字符串常量池实现的前提条件是Java中String对象是不可变的,字符串的值存放在字符串常量池中。

优点

1.只有当字符串是不可变的,字符串池才有可能实现。字符串池的实现可以在运行时节约很多heap空间,因为不同的字符串引用可以指向池中的同一个字符串。但如果字符串是可变的,如果变量改变了它的值,那么其它指向这个值的变量的值也会一起改变。

2.如果字符串是可变的,那么会引起很严重的安全问题。譬如,数据库的用户名、密码都是以字符串的形式传入数据库,以获得数据库的连接,或者在socket编程中,主机名和端口都是以字符串的形式传入。因为字符串是不可变的,所以它的值是不可改变的,否则黑客们可以钻到空子,改变字符串指向的对象的值,造成安全漏洞。

3.因为字符串是不可变的,所以是多线程安全的,同一个字符串实例可以被多个线程共享。就不用因为线程安全问题而使用同步。

4.类加载器要用到字符串,不可变性提供了安全性,以便正确的类被加载。譬如你想加载java.sql.Connection类,而这个值被改为myhacked.Connection,那么会对你的数据库造成不可知的破坏。

5.因为字符串是不可变的,所以在它创建的时候hashcode就被缓存了,不需要重新计算,这就使得字符串很适合作为Map中的键,字符串的处理速度要快过其它的键对象。这就是HashMap中的键往往都使用字符串的原因。

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值