1.什么是【不可变】?
String不可变很简单,如下图,给一个已有字符串“abcd”第二次赋值成"abced",不是在原内存地址上修改数据,而是重新指向一个新对象,新地址。
2.String为什么不可变?从原理上分析。
翻开JDK源码,java.lang.String类起手前三行,是这样写的:
public final class String implements Serializable, Comparable, 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 hs = new HashSet();
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[]来保存密码,你仍然可以将其中所有的元素都设置为空或者零。所以将密码保存到字符数组中很明显的降低了密码被窃取的风险。
当然只使用字符数组也是不够的,为了更安全你需要将数组内容进行转化。 建议使用哈希的或者是加密过的密码而不是明文,然后一旦完成验证,就将它从内存中清除掉。
参考:
https://blog.csdn.net/qingmengwuhen1/article/details/52175303
http://www.importnew.com/18326.html