String在Java中是个不可更改的类。一个不可更改的类简单来说就是这个类的所有实例是不可以更改的。所有的实例信息在创建的时候被初始化而且信息是不可以更改的。不可更改的类有很多好处。
这篇文章总结了为什么String被设计成不可以改变的。一个好的回答需要深入理解内存、同步和数据结构等。
1、 字符串池的需要
字符串池(字符串内部池) 是在方法区域的特殊区域。当一个string被创建如果这个string已经在内存里面存在了,那个存在的string的引用被返回,而不是创建个新的对象和返回它的引用。
下面的代码将在堆上创建一个string对象。
String string1 = "abcd";
String string2 = "abcd";
如果这个string是可以改变的,通过一个引用改变一个string将导致另一引用指向错误的值。
2、 缓存哈希值
在Java中,string的哈希值经常被用。举个例子,在HashMap中。保持不变,可以保证总是返回相同的哈希值。所以它可以被缓存而不用担心被改变。 这意味这不需要在使用的时候每次都计算哈希值。
这将更高效。
在String类中,它有以下的代码:
private int hash;//this is used to cache hash code.
3、 使其他类的使用更加容易。
为了更具体,想想下面的程序:
HashSet<String> set = new HashSet<String>();
set.add(new String("a"));
set.add(new String("b"));
set.add(new String("c"));
for(String a: set)
a.value = "a";
在这个例子中,如果String是可以改变的,如果它的值被改变这将违反了Set的设计(Set不可以包含重复的元素)。这个例子是为了简化设计的,在实际的String类中没有value 属性。
4、 安全
String 在很多java的类中,网络连接中,打开的文件中经常被作为参数使用。如果String是可以改变的,一个连接或者文件将有可能被改变,这将导致严肃的安全威胁。这个方法认为它正连接到一个机器,但是实际上不是。易变的strings将在反射或者作为参数将导致安全问题。
下面是代码实例:
boolean connect(string s){
if (!isSecure(s)) {
throw new SecurityException();
}
//here will cause problem, if s is changed before this by using other references.
causeProblem(s);
}
5、 不变对象是自然线程安全
因为不可变对象是不可以改变的,它能够被多个线程自由的共享。这消除了同步。
总结,String被设计成不可以更改的是为了效率和安全。这也是为什么现在很多不可以改变的类。
、///
一.原理(为什么说String类是不可变的)
1.什么是不可变对象
如果一个对象在创建之后就不能再改变它的状态,那么这个对象是不可变的(Immutable)。不能改变状态的意思是,不能改变对象内的成员变量,包括基本数据类型变量的值不能改变,引用类型的变量不能指向其他的对象,引用类型指向的对象的状态也不能改变。
2.final关键字的作用
如果要创建一个不可变对象,关键一步就是要将所有的成员变量声明为final类型。所以下面简单回顾一下final关键字的作用:
- final修饰类,表示该类不能被继承,俗称断子绝孙类,该类的所有方法自动地成为final方法
- final修饰方法,表示子类不可重写该方法
- final修饰基本数据类型变量,表示该变量为常量,值不能再修改
- final修饰引用类型变量,表示该引用在构造对象之后不能指向其他的对象,但该引用指向的对象的状态可以改变
3.String类不可变性的分析
先看下面这段代码:
String s = "abc"; //(1)
System.out.println("s = " + s);
s = "123"; //(2)
System.out.println("s = " + s);
打印结果为:
s = abc
s = 123
看到这里,你可能对String是不可变对象产生了疑惑,因为从打印结果可以看出,s的值的确改变了。其实不然,因为s只是一个String对象的引用,并不是String对象本身。
当执行(1)处这行代码之后,会先在方法区的运行时常量池创建一个String对象"abc",然后在Java栈中创建一个String对象的引用s,并让s指向"abc",如下图所示:
图1
当执行完(2)处这行代码之后,会在方法区的运行时常量池创建一个新的String对象"123",然后让引用s重新指向这个新的对象,而原来的对象"abc"还在内存中,并没有改变,如下图所示:
图2
4.String类不可变性的原理
要理解String类的不可变性,首先看一下String类中都有哪些成员变量。在JDK1.8中,String的成员变量主要有以下几个:
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];
首先可以看到,String类使用了final修饰符,表明String类是不可继承的。
然后,我们主要关注String类的成员变量value,value是char[]类型,因此String对象实际上是用这个字符数组进行封装的。再看value的修饰符,使用了private,也没有提供setter方法,所以在String类的外部不能修改value,同时value也使用了final进行修饰,那么在String类的内部也不能修改value,但是上面final修饰引用类型变量的内容提到,这只能保证value不能指向其他的对象,但value指向的对象的状态是可以改变的。通过查看String类源码可以发现,String类不可变,关键是因为SUN公司的工程师,在后面所有String的方法里都很小心的没有去动字符数组里的元素。所以String类不可变的关键都在底层的实现,而不仅仅是一个final。
5.String对象真的不可变吗
上面提到,value虽然使用了final进行修饰,但是只能保证vaue不能指向其他的对象,但value指向的对象的状态是可以改变的,也就是说,可以修改value指向的字符数组里面的元素。因为value是private类型的,所以只能使用反射来获取String对象的value属性,再去修改value指向的字符数组里面的元素。通过下面的代码进行验证:
String s = "Hello World";
System.out.println("s = " + s);
//获取String类中的value属性
Field valueField = String.class.getDeclaredField("value");
//改变value属性的访问权限
valueField.setAccessible(true);
//获取s对象上的value属性的值
char[] value = (char[]) valueField.get(s);
//改变value所引用的数组中的第6个字符
value[5] = '_';
System.out.println("s = " + s);
打印结果为:
s = Hello World
s = Hello_World
在上述代码中,s始终指向同一个String对象,但是在反射操作之后,这个String对象的内容发生了变化。也就是说,通过反射是可以修改String这种不可变对象的。
二.设计目标(为什么String要设计成不可变的)
在Java中,将String设计成不可变的是综合考虑到内存、同步、数据结构及安全等各种因素的结果,下文将为各种因素做一个小结。
1.运行时常量池的需要
String s = "abc";
执行上述代码时,JVM首先在运行时常量池中查看是否存在String对象“abc”,如果已存在该对象,则不用创建新的String对象“abc”,而是将引用s直接指向运行时常量池中已存在的String对象“abc”;如果不存在该对象,则先在运行时常量池中创建一个新的String对象“abc”,然后将引用s指向运行时常量池中创建的新String对象。
String s1 = "abc";
String s2 = "abc";
执行上述代码时,在运行时常量池中只会创建一个String对象"abc",这样就节省了内存空间。示意图如下所示:
图3
2.同步
因为String对象是不可变的,所以是多线程安全的,同一个String实例可以被多个线程共享。这样就不用因为线程安全问题而使用同步。
3.允许String对象缓存hashcode
查看上文JDK1.8中String类源码,可以发现其中有一个字段hash,String类的不可变性保证了hashcode的唯一性,所以可以用hash字段对String对象的hashcode进行缓存,就不需要每次重新计算hashcode。所以Java中String对象经常被用来作为HashMap等容器的键。
4.安全性
如果String对象是可变的,那么会引起很严重的安全问题。比如,数据库的用户名、密码都是以字符串的形式传入来获得数据库的连接,或者在socket编程中,主机名和端口都是以字符串的形式传入。因为String对象是不可变的,所以它的值是不可改变的,否则黑客们可以钻到空子,改变String引用指向的对象的值,造成安全漏洞。
参考:
Java中的String为什么是不可变的? -- String源码分析
为什么String类是不可变的?