☕️【Java 技术之旅】带你一起探究 String 类不可变的特性
前提介绍
在 Java 中 String 类的使用的频率可谓相当高。它是 Java 语言中的核心类,在 java.lang 包下,主要用于字符串的比较、查找、拼接等等操作。如果要深入理解一个类,最好的方法就是看看源码:
什么是字符串
字符串是由引号所括起来的一系列字符序列。
字符串类(String)
从源码中,可以看出以下几点:
String 类被 final 关键字修饰,表示 String 类不能被继承,且它的属性和方法都被 final 所修饰任何操作都会生成新对象。
String:: subString(),String::concat() 等方法都会生成一个新的 String 对象,不会在原对象上进行操作从下面 String 源码部分中很容易得到上面的结论:
String 类实现了 Serializable、CharSequence、 Comparable 接口。
String 类的值是通过 char 数组存储的,并且 char 数组被 private 和 final 修饰,字符串一旦创建就不能再修改。
String 不可变性
String 对象一旦被创建就是固定不变的了,对 String 对象的任何改变都不影响到原对象,相关的任何操作都会生成新的对象。
String 不可变的表现就是当我们试图对一个已有的对象 "abcd" 赋值为 "abcde",String 会新创建一个对象。
注意点
这个无法被修改仅仅是指引用地址不可被修改(也就是说栈里面的这个叫 value 的引用地址不可变,编译器不允许我们把 value 指向堆中的另一个地址),并不代表存储在堆中的这个数组本身的内容不可变。
那既然我们说 String 是不可变的,那显然仅仅靠 final 是远远不够的:
char 数组是 private 的,并且 String 类没有对外提供修改这个数组的方法,所以它初始化之后外界没有有效的手段去改变它;
String 类被 final 修饰的,首先要讲 final 修饰类的作用,被 final 修饰的类不能被继承,类中的所有成员方法都会被隐式地指定为 final 方法。也就是不能拥有子类,成员方法也不能被重写。;
String 的所有方法里面,都很小心地避免去修改了 char 数组中的数据,涉及到对 char 数组中数据进行修改的操作全部都会重新创建一个 String 对象。
比如 substring 方法:
为什么要设计成不可变的呢?
String 被设计成不可变就是为了字符串常量池。
字符串常量池的定义
大量频繁的创建字符串,将会极大程度地影响程序的性能,字符串的分配和其他对象分配一样,是需要消耗高昂的时间和空间的,而且字符串我们使用的非常多。JVM 为了提高性能和减少内存的开销,所以在实例化字符串的时候使用字符串常量池进行优化。
JVM 为了提高性能和减少内存开销,在实例化字符串常量的时候进行了一些优化:
字符串开辟了一个字符串常量池 String Pool(HashSet 的 StringTable),可以理解为缓存区创建字符串常量时,首先检查字符串常量池中是否存在该字符串。
池化思想其实在 Java 中并不少见,字符串常量池也是类似的思想,当创建字符串时,JVM 会首先检查字符串常量池,如果该字符串已经存在常量池中,那么就直接返回常量池中的实例引用。如果字符串不存在常量池中,就会实例化该字符串并且将其放到常量池中。
堆内存中只会创建一个 String 对象:
String 允许被改变,那如果我们修改了 str2 的内容为 good,那么 str1 也会被修改,显然这不是我们想要看见的结果。
new String(“abc”)创建了几个对象?
如果之前"abc"字符串没有使用过,毫无疑问是创建两个对象,堆中创建了一个 String 对象,字符串常量池创建了一个,一共两个。
如果之前已经使用过了"abc"字符串,则不会再在字符串常量池创建对象,而是从字符串常量缓冲区中获取,只会在堆中创建一个 String 对象。
String 被设计成不可变就是为了安全
作为最基础最常用的数据类型,String 被许多 Java 类库用来作为参数,如果 String 不是固定不变的,安全性考虑。字符串应用场景众多,设计成不可变性可以有效防止字符串被有意篡改。。
String 被许多的 Java 类(库)用来当做参数,比如网络连接地址 URL,文件路径 path,还有反射机制所需要的 String 参数等,假若 String 不是固定不变的,将会引起各种安全隐患。
在多线程环境下,众所周知,多个线程同时想要修改同一个资源,是存在危险的,而 String 作为不可变对象,不能被修改,并且多个线程同时读同一个资源,是完全没有问题的,所以 String 是线程安全的。
String 被设计成不可变就是为了效率
字符串不变性保证了 hash 码的唯一性,因此可以放心的进行缓存,这也是一种性能优化手段,意味着不必每次都取计算新的哈希码。
String 真的不可变吗?
String 无非就是改变 char 数组 value 的内容,而 value 是私有属性,那么在 Java 中有没有某种手段可以访问类的私有属性呢?
反射,使用反射可以直接修改 char 数组中的内容,当然,一般来说我们不这么做。
看下面代码
字符串的 replace
其他方法也是一样,无论是 sub、concat 还是 replace 操作都不是在原有的字符串上进行的,而是重新生成了一个新的字符串对象。
字符串拼接
字符串的拼接在 Java 中是很常见的操作,但是拼接字符串并不是简简单单地使用"+"号即可,还有一些要注意的点,否则会造成效率低下。
在循环内使用+=拼接字符串会有什么问题呢?我们反编译一下看看就知道了。
其实反编译后,我们可以看到 String 类使用"+="拼接的底层其实是使用 StringBuilder,先初始化一个 StringBuilder 对象,然后使用 append()方法拼接,最后使用 toString()方法得到结果。
问题在于如果在循环体内使用+=拼接,会创建很多临时的 StringBuilder 对象,拼接后再调用 toString()赋给原 String 对象。这会生成大量临时对象,严重影响性能。
所以在循环体内进行字符串拼接时,建议使用 StringBuilder 或者 StringBuffer 类,例子如下:
StringBuilder 和 StringBuffer 的区别在于,StringBuffer 的方法都被 sync 关键字修饰,所以是线程安全的,而 StringBuilder 则是线程不安全的(效率高)。
总结
并不是因为 char 数组是 final 才导致 String 的不可变,而是为了把 String 设计成不可变才把 char 数组设置为 final 的。
所有不可变类都完全遵守这些规则:
不要提供 setter 方法(包括修改字段的方法和修改字段引用对象的方法);
将类的所有字段定义为 final、private 的;
不允许子类重写方法。简单的办法是将类声明为 final,更好的方法是将构造函数声明为私有的,通过工厂方法创建对象;
如果类的字段是对可变对象的引用,不允许修改被引用对象。
版权声明: 本文为 InfoQ 作者【李浩宇/Alex】的原创文章。
原文链接:【http://xie.infoq.cn/article/01b344e657ae27eb6d4287c58】。文章转载请联系作者。
评论