为什么Java里面的String对象是不可变的

这篇文章是Java里面的String对象到底神奇在什么地方的姊妹篇。

先来说说java对象不可变。

所谓的对象不可变,可以简单的理解为,对象一旦创建后,其状态不可修改。所谓不可修改就是对象里面所有的状态属性都不能修改。
举个例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
public 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;
}
}

这个对象一旦被创建,就没有办法修改内部状态。因为没有对外提供任何方法修改属性值。而且pricename属性被声明为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关键字进行了修饰,还有就是字符数组valuefinal修饰,并且访问权限是private的。如果不清楚Java访问权限的,可以看我这篇关于Java访问权限的介绍。

String类用final关键字修饰,证明该类是不可继承的。

String类是用字符数组表示字符串对象的。属性valuefinal修饰,证明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
2
String str = “java”;
str = str.substring(1,2);

要想知道str有没有被改变我们可以看一眼substring的源码。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
public 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类搞成不可变的有什么好处吗?当然有,主要体现在两个方面。

安全:

  1. 在加载数据库用户名密码,以及类加载的时候,都是以String类型传递参数的。如果字符串是可变的,这些信息很容易被篡改,那么将会造成很大的安全问题。
  2. 因为字符串是不可变的,同一个字符串实例可以被多个线程共享。所以天生就是线程安全的。

性能:

  1. 只有当字符串是不可变的,字符串常量池才有可能实现。字符串常量池可以节省JVM的内存空间(多份相同的String字符实际只占同一个空间)
  2. 因为字符串不可变,所以在它创建的时候可以把 hashcode值缓存起来。当使用的时候就不用再重新计算了,这就使得字符串很适合作为Map中的键,字符串的处理速度要快过其它的键对象。
-------------本文结束-------------
坚持原创技术分享,您的支持将鼓励我继续创作!
0%