为什么String被设计成不可变

640

本文来自「架构师之巅」,搜索「jiagoushizhidian」即可关注

团队成员由饿了么、阿里、蚂蚁金服等同事组成


说实话,一开始我是不太想写这篇文章,首先稍微学过JAVA的都知道String是不可变的,其次String这个类确实很简单,也确实写不出东西,有一次和一位相当资深的架构师在聊天的时候,那位架构师突然问了我一句,“从你的角度谈谈String 为什么要设计成不可变的?”

  这个问题很简单,但是回答好可着实不容易,工作这么多年,我确实没去思考过这个问题,说起来也比较残酷,这也是我这次为什么要把它写下来的原因。

String的构成

1public final class String
2    implements java.io.SerializableComparable<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   ...

通过源码我们可以知道String底层是由char 数组构成的,我们创建一个字符串对象的时候,其实是将字符串保存在char数组中,因为数组是引用对象,为了防止数组可变,jdk加了final修饰,加了final修饰的数组只是代表了引用不可变,不代表数组的内容不可变,因此jdk为了真正防止不可变,又加了一个private修饰符。

说到String 不得不提字符串常量池,这个常量池主要存储在方法区中,当一个字符串被创建的时候,首先会去常量池中查找,如果找到了就返回对改字符串的引用,如果没找到就创建这个字符串并塞到常量池中。

下面代码只会在堆中创建一个字符串

1String s1="abc";
2String s2="abc";

640

试想一下如果String是可变的,当两个引用指向指向同一个字符串时,对其中一个做修改就会影响另外一个。

什么是不可变?

对于Java而言,除了基本类型(即int, long, double等),其余的都是对象。对于何为不可变对象,《java concurrency in practice》一书给出了一个粗略的定义:对象一旦创建后,其状态不可修改,则该对象为不可变对象。一般一个对象满足以下三点,则可以称为是不可变对象:

扫描二维码关注公众号,回复: 2329444 查看本文章
  1. 其状态不能在创建后再修改;

  2. 所有域都是final类型;

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

这里重点说明一下第2点,一个对象其所有域都是final类型,该对象也可能是可变对象。因为final关键字只是限制对象的域的引用不可变,但无法限制通过该引用去修改其对应域的内部状态。因此,严格意义上的不可变对象,其final关键字修饰的域应该也是不可变对象和primitive type值。
从技术上讲,不可变对象内部域并不一定全都声明为final类型,String类型即是如此。在String对象的内部我们可以看到有一个名为hash的域并不是final类型,这是因为String类型惰性计算hashcode并存储在hash域中(这是通过其他final类型域来保证每次的hashcode计算结果必定是相同的)。
除此之外,String对象的不可变是由于对String类型的所有改变内部存储结构的操作都会new出一个新的String对象。

不可变带来的好处

String 设计成不可变,带来的好处有以下几点

一、安全性

1、多线程安全性

因为String是不可变的,因此在多线程操作下,它是安全的,我们看下如下代码:

1public String get(String str){
2  str +="aaa";
3  return str;
4}

试想一下如果String是可变的,那么get方法内部改变了str的值,方法外部str也会随之改变。

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

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

二、使用常量池节省空间

只有当字符串是不可变的,字符串池才有可能实现。字符串池的实现可以在运行时节约很多heap空间,因为不同的字符串变量都指向池中的同一个字符串。但如果字符串是可变的,那么String interning将不能实现(String interning是指对不同的字符串仅仅只保存一个,即不会保存多个相同的字符串),因为这样的话,如果变量改变了它的值,那么其它指向这个值的变量的值也会一起改变。

三、缓存hashcode

因为字符串是不可变的,所以在它创建的时候hashcode就被缓存了,不需要重新计算。这就使得字符串很适合作为Map中的键,字符串的处理速度要快过其它的键对象。这就是HashMap中的键往往都使用字符串。
我们可以看到String中有如下代码:

1private int hash;//this is used to cache hash code.

以上代码中hash变量中就保存了一个String对象的hashcode,因为String类不可变,所以一旦对象被创建,该hash值也无法改变。所以,每次想要使用该对象的hashcode的时候,直接返回即可。

不可变带来的缺点

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

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

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

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

推荐阅读

【分布式】数据库和缓存双写一致性方案解析

【附代码】单点登录介绍和服务端实现


640?

猜你喜欢

转载自blog.csdn.net/b644ROfP20z37485O35M/article/details/81117253