这篇文章是Java里面的String对象到底神奇在什么地方的姊妹篇。
先来说说java对象不可变。
所谓的对象不可变,可以简单的理解为,对象一旦创建后,其状态不可修改。所谓不可修改就是对象里面所有的状态属性都不能修改。
举个例子:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19public class Book {
//书价
private final double price;
//书名
private final String name;
public Book(double price, String name) {
this.price = price;
this.name = name;
}
public double getPrice() {
return price;
}
public String getName() {
return name;
}
}
这个对象一旦被创建,就没有办法修改内部状态。因为没有对外提供任何方法修改属性值。而且price
和name
属性被声明为final
在来看看String类是如何定义的。以下源码摘自JDK1.8
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
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
/** use serialVersionUID from JDK 1.0.2 for interoperability */
private static final long serialVersionUID = -6849794470754667710L;
/**
* Class String is special cased within the Serialization Stream Protocol.
*
* A String instance is written into an ObjectOutputStream according to
* <a href="{@docRoot}/../platform/serialization/spec/output.html">
* Object Serialization Specification, Section 6.2, "Stream Elements"</a>
*/
private static final ObjectStreamField[] serialPersistentFields =
new ObjectStreamField[0];
/**
* Initializes a newly created {@code String} object so that it represents
* an empty character sequence. Note that use of this constructor is
* unnecessary since Strings are immutable.
*/
public String() {
this.value = "".value;
}
}
这段代码最关键的两个部分就是,String
类用final
关键字进行了修饰,还有就是字符数组value
用final
修饰,并且访问权限是private
的。如果不清楚Java访问权限的,可以看我这篇关于Java访问权限的介绍。
String
类用final
关键字修饰,证明该类是不可继承的。
String
类是用字符数组表示字符串对象的。属性value
被final
修饰,证明value
是不可变的。等等,这里有个小问题,value
是个字符数组,数组是引用类型,value
不可变只能说明字符数组的引用不可变,字符数组里面的内容是可变的。
举个例子 String str = “java”;
value
里面的值是这样的 [‘j’,’a’,’v’,’a’]
,我们虽然不能改变value
到数组['j','a','v','a']
的引用,但是我们很容易改变数组内的值,比如 value[2]=’s’
;
那为什么我们还说String
是不可变的呢。关键就在于private
关键字。用private
关键字修饰之后,在java的访问权限规则里面,除了在类内部可以修改value
的值以外,没有任何地方可以修改它了。而我们的JDK工程师,又很小心的设计了该类,在String
类内部也没有任何一处可以修改value
的值,真是连自己人都不信任啊。所以String
对象一旦被创建,就再也不能被修改了。
有人说貌似不是,比如你看下面的代码1
2String str = “java”;
str = str.substring(1,2);
要想知道str有没有被改变我们可以看一眼substring的源码。1
2
3
4
5
6
7
8
9
10
11
12
13
14public String substring(int beginIndex, int endIndex) {
if (beginIndex < 0) {
throw new StringIndexOutOfBoundsException(beginIndex);
}
if (endIndex > value.length) {
throw new StringIndexOutOfBoundsException(endIndex);
}
int subLen = endIndex - beginIndex;
if (subLen < 0) {
throw new StringIndexOutOfBoundsException(subLen);
}
return ((beginIndex == 0) && (endIndex == value.length)) ? this
: new String(value, beginIndex, subLen);
}
关健的代码就是最后一行,new String(value, beginIndex, subLen)
看到了吗,不是在原来的字符上进行修改,而是创建了一个新的对象,然后把变量str指向新的对象。 原来的字符串就被回收了。
费了这么大力气,把String
类搞成不可变的有什么好处吗?当然有,主要体现在两个方面。
安全:
- 在加载数据库用户名密码,以及类加载的时候,都是以String类型传递参数的。如果字符串是可变的,这些信息很容易被篡改,那么将会造成很大的安全问题。
- 因为字符串是不可变的,同一个字符串实例可以被多个线程共享。所以天生就是线程安全的。
性能:
- 只有当字符串是不可变的,字符串常量池才有可能实现。字符串常量池可以节省JVM的内存空间(多份相同的String字符实际只占同一个空间)
- 因为字符串不可变,所以在它创建的时候可以把 hashcode值缓存起来。当使用的时候就不用再重新计算了,这就使得字符串很适合作为Map中的键,字符串的处理速度要快过其它的键对象。