IT虾米网

java中String类设计成不可变的原因

developer 2022年05月10日 编程语言 156 0

1.什么是【不可变】?

    String不可变很简单,如下图,给一个已有字符串“abcd”第二次赋值成"abced",不是在原内存地址上修改数据,而是重新指向一个新对象,新地址。

2.String为什么不可变?从原理上分析。

    翻开JDK源码,java.lang.String类起手前三行,是这样写的:

public final class String implements Serializable, Comparable<String>, CharSequence { 
    private final char[] value; 
    private int hash;
首先,String类是用final关键字修饰,这说明String不可继承。 
其次,再看下面,String类的主力成员字段value是个char[]数组,而且是用final修饰的。final修饰的字段创建以后就不可改变。 

  有的人以为故事就这样完了,其实没有。因为虽然value是不可变的,也只是value这个引用地址不可变。挡不住Array数组是可变的事实。

    也就是说Array变量只是stack上的一个引用,数据的本体结构在heap堆。String类里的value用final修饰,只是说stack里的这个叫value的引用地址不可变。没有说堆里array本身数据不可变。看这个这个例子,

final int[] value={1,2,3} 
 
int[] another={4,5,6}; 
 
value = another;//编译器报错,final不可变

value用final修饰,编译器不允许我把value指向堆区另一个地址。但如果直接对数组元素动手,分分钟搞定。

final int[] value={1,2,3}; 
 
value[2]=100;//这时候数组里已经是{1,2,100}

    所以String是不可变,关键是因为SUN公司的工程师,在后面所有String的方法里很小心地没有去动Array里的元素,没有暴露内部成员字段。private final char value[]这一句里,private的私有访问权限的作用都比final大。而且设计师还很小心地反整个String设计成final禁止继承,避免被其他人继承后破坏。所以String是不可变的关键在于底层的实现,而不是一个final。考验的是工程师构造数据类型,封装数据的功力。

    3.不可变好处:安全与高效

    3.1 多线程安全性

    这个最简单的原因,就是为了安全。看下面这个场景,一个函数appendStr()在不可变的String参数后面加上一段“bbb”后返回。appendSb()负责在可变的StringBuilder后面加"bbb"。

public class Test { 
    //不可变的String 
    public static String appendStr(String s) { 
 
        s += "bbb"; 
 
        return s; 
 
    } 
 
 
    //可变的StringBuilder 
    public static StringBuilder appendSb(StringBuilder sb) { 
 
        return sb.append("bbb"); 
 
    } 
 
 
    public static void main(String[] args) { 
 
        String s = new String("aaa"); 
 
        String ns = Test.appendStr(s); 
 
        System.out.println("String aaa>>>" + s.toString()); 
 
 
        //StringBuilder做参数 
        StringBuilder sb = new StringBuilder("aaa"); 
 
        StringBuilder nsb = Test.appendSb(sb); 
 
        System.out.println("StringBuilder aaa >>>" + sb.toString()); 
 
    } 
 
} 
如果程序员不小心像上面例子里,直接在传进来的参数上加上“bbb”.因为Java对象参数传的是引用,所有可变的StringBuffer参数就被改变了。可以看到变量sb在Test.appendSb(sb)操作之后,就变成了"aaabbb"。有的时候这可能不是程序员的本意。所以String不可变的安全性就体现在这里。 
再看下面这个HashSet用StrinbBuilder做元素的场景,问题就更严重了,而且更为隐蔽。
public static void main(String[] args) { 
        HashSet<StringBuilder> hs = new HashSet<StringBuilder>(); 
 
        StringBuilder sb1 = new StringBuilder("aaa"); 
 
        StringBuilder sb2 = new StringBuilder("aaabbb"); 
 
        hs.add(sb1); 
 
        hs.add(sb2); //这时候HashSet里是{"aaa","aaabbb"} 
 
 
        StringBuilder sb3 = sb1; 
 
        sb3.append("bbb");//这时候HashSet里是{"aaabbb","aaabbb"} 
 
        System.out.println(hs); 
    }

StringBuilder型变量sb1和sb2分别指向了堆内的字面量“aaa”和"aaabbb"。把它们都插入一个HashSet。到这一步没问题。但如果后面我把变量sb3也指向sb1的地址,再改变sb3的值,因为StringBuilder没有不可变性的保护,sb3直接在原先“aaa”的地址上改。导致sb1的值也变了。这时候,HashsSet上就出现了两个相等的键值"aaabbb"。破坏了HashSet键值的唯一性。所以千万不要用可变类型做HashMap和HashSet键值。

还有一个大家都知道,就是在并发场景下,多个线程同时读一个资源,是不会引发竞态条件的。只有对资源做写操作才有危险。不可变对象不能被写,所以线程安全。

3.2、类加载中体现的安全性

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

最后别忘了String另外一个字符串常量池的属性。像下面这样的字符串one和two都用字面量"something"赋值。它们其实都指向同一个内存地址。

3.3、使用常量池节省空间

String one = "someString"; 
 
String two = "someString";

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

   补充说明:不可变对象天生就是线程安全的

     因为不可变对象不能被改变,所以他们可以自由地在多个线程之间共享不需要任何同步处理

     String被设计成不可变的主要目的是为了安全高效。所以,使String是一个不可变类是一个很好的设计。

4、不可变带来的缺点

    不可变对象也有一个缺点就是会制造大量垃圾,由于他们不能被重用而且对于它们的使用就是”用“然后”扔“,字符串就是一个典型的例子,它会创造很多的垃圾,给垃圾收集带来很大的麻烦。当然这只是个极端的例子,合理的使用不可变对象会创造很大的价值。

    密码应该存放在字符数组中而不是String中

    由于String在Java中是不可变的,如果你将密码以明文的形式保存成字符串,那么它将一直留在内存中,直到垃圾收集器把它清除。而由于字符串被放在字符串缓冲池中以方便重复使用,所以它就可能在内存中被保留很长时间,而这将导致安全隐患,因为任何能够访问内存(memory dump内存转储)的人都能清晰的看到文本中的密码,这也是为什么你应该总是使用加密的形式而不是明文来保存密码。由于字符串是不可变的,所以没有任何方式可以修改字符串的值,因为每次修改都将产生新的字符串,然而如果你使用char[]来保存密码,你仍然可以将其中所有的元素都设置为空或者零。所以将密码保存到字符数组中很明显的降低了密码被窃取的风险。

    当然只使用字符数组也是不够的,为了更安全你需要将数组内容进行转化。 建议使用哈希的或者是加密过的密码而不是明文,然后一旦完成验证,就将它从内存中清除掉。

    参考:

    CSDN

    CSDN


评论关闭
IT虾米网

微信公众号号:IT虾米 (左侧二维码扫一扫)欢迎添加!

Java hashCode() 和 equals()的若干问题精讲