String类在java中是immutable(不可变),因为它被关键字final修饰。当String实例创建时就会被初始化,并且以后无法修改实例信息。String类是工程师精心设计的艺术品。
一、String为什么不可变?
要了解String类创建的实例为什么不可变,首先要知道final关键字的作用:final的意思是“最终,最后”。final关键字可以修饰类、方法、字段。修饰类时,这个类不可以被继承;修饰方法时,这个方法就不可以被覆盖(重写),在JVM中也就只有一个版本的方法--实方法;修饰字段时,这个字段就是一个常量。
查看java.lang.String方法时,可以看到:
/**
* The String class represents character strings. All string literals in Java programs, such as "abc",
* are implemented as instances of this class.
*/
public final class String implements java.io.Serializable, Comparable<String>, CharSequence {
/** The value is used for character storage. */
private final char value[];
...
}
"The String class represents character strings"意思是String类表示字符串;String类被关键字final修饰,可以说明它不可被继承;从"private final char value[]"可知,String本质上是一个char数组。
String类的成员字段value是一个char[]数组,而且也是用final关键字修饰,被final关键字修饰的字段在创建后其值是不可变的,但也只是value这个引用地址不可变,可是Array数组的值却是可变的,Array数组数据结构如下图所示:
从图中可以看出,Array变量只是栈上(stack)的一个引用,数组中的数据存储在堆上(heap)。String类里的value是用final修饰,只能说在栈上(stack)这个value的引用地址不可变,可没说堆里的Array本身数据不可变。看下面这个例子:
final int[] value = {1,2,3,4,5};
int otherValue = {6,7,8,9,10};
value = otherValue;//编译报错
value是被final关键字修饰的,编译器不允许把value指向堆另外一个地址;但如果直接对数组元素进行赋值,就允许;如下面这个例子:
final int[] value = {1,2,3,4,5};
value[0] = 0;
所以说String是不可变,在后面所有的String方法里没有去动Array中的元素,也没有暴露内部成员字段。private final char value[],private的私有访问权限的作用都比final大。所以String是不可变的关键都是在底层实现的,而不是一个简单的final关键字。
二、String类实例不可变的内存结构图
String类实例不可变很简单,如下图所示是String类实例不可变的内存结构图:
有一个字符串s的值为"abcd",它再次被赋值为"abcdef",不是在原堆的地址上修改数据,而是重新指向一个新的对象,新的地址。
三、字符串常量池
字符串常量池是方法区(Method Area)中一个特殊的存储区域。当一个字符串被创建时,如果这个字符串的值已经存在String pool中,就返回这个已经存在的字符串引用,而不是创建一个新的对象。下面的代码只会在堆中创建一个对象:
String name="abcd";
String userName="abcd";
这样在大量使用字符串的情况下,可以节省内存空间,提高效率。但之所以能实现这个特性,String的不可变性是最基础的一个必要条件。
四、String类不可变有什么好处?
最简单的就是为了安全和效率。从安全上讲,因为不可变的对象不能被改变,他们可以在多个线程之间进行自由共享,这消除了进行同步的要求;从效率上讲,设计成final,JVM才不用对相关方法在虚函数表中查询,而是直接定位到String类的相关方法上,提高执行效率;总之,由于效率和安全问题,String被设计成不可变的,这也是一般情况下,不可变的类是首选的原因。
五、不可变类
不可变类只是它的实例不能被修改的类。每个实例中包含的所有信息都必须在创建该实例时就提供,并在对象 的整个生命周期内固定不变。String、基本类型的包装类、BigInteger和BigDecimal就是不可变得类。
为了使类成为不可变,必须遵循以下5条规则:①不要提供任何会修改对象状态的方法。②保证类不会被扩展。③使所有的域都是final。④使所有的域都成为私有的。⑤确保 对于任何可变组件的互斥访问。如果类具有指向可变对象的域,则必须确保该类的客户端无法获得指向这些对象的引用。
六、不可变类的优点和缺点
不可变类实例不可变性,具有很多优点。①不可变类对象比较简单。不可变对象可以只有一种状态,即被创建时的状态。②不可变对象本质上是线程安全的,它们不要求同步。当多个线程并发访问这样的对象时,它们不会遭到破坏。实际上,没有任何线程会注意到其他线程对于不可变对象的影响。所以,不可变对象可以被自由地分配。“不可变对象可以被自由地分配”导致的结果是:永远不需要进行保护性拷贝。③不仅可以共享不可变对象,甚至也可以共享它们的内部信息。④不可变对象为其他对象提供了大量的构件。如果知道一个复杂对象内部的组件不会改变,要维护它的不变性约束是比较容易的。
不可变类真正唯一的缺点是,对于每个不同的值都需要一个单独的对象。创建这种对象的代价很高。
七、如何构建不可变类?
构建不可变类有两种方式:用关键字final修饰类和让类的所有构造器都变成私有的或者包级私有的,并添加公有的静态工厂来替代公有的构造器。
为了具体说明用静态工厂方法来替代公有的构造器,下面以Complex为例:
//复数类
public class Complex{
//实数部分
private final double re;
//虚数部分
private final double im;
//私有构造器
private Complex(double re,double im){
this.re = re;
this.im = im;
}
//静态工厂方法,返回对象唯一实例
public static Complex valueOf(double re,double im){
return new Complex(re,im);
}
...
}
不可变的类提供一些静态工厂方法,它们把频繁请求的实例缓存起来,从而当现在实例符合请求的时候,就不必创建新的实例。使用这样的静态工厂方法使得客户端之间可以共享现有的实例,而不是创建新的实例,从而减低内存占用和垃圾回收的成本。
总之,使类的可变性最小化。不要为每个get方法编写一个相对应的set方法,除非有很好的理由要让类成为可变的类,否则就应该是不可变的。如果有些类不能被做成是不可变的,仍然应该尽可能地限制它的可变性。不可变的类有很多优点,但唯一的缺点就是在特定的情况下存在潜在的性能问题。
PS:静态工厂方法是什么?
静态工厂方法只是一个返回类的实例的静态方法,如下面是一个Boolean的简单实例。这个方法将boolean基本类型值转换成一个Boolean对象引用。
public static Boolean valueOf(boolean b){
return b?Boolean.TRUE?Boolean.FALSE;
}
静态工厂方法相对于构造器来说,具有很多优势:
①创建的方法有名字;
②不必在每次调用它们的时候都创建一个新的对象;
③可以返回原返回类型的任何子类的对象。这样我们在选择返回对象的类时就有更大的灵活性,这种灵活性的一种应用是API可以返回对象,同时又不会使对象的类变成公有的。以这种方式隐藏实现类会使API变得非常简洁,这项技术适用于基于接口的框架。
④在创建参数化类型实例时,它们使代码变得更加简洁。编译器可以替你找到类型参数,这被称为类型推导。如下面这个例子
public static<k,v> HashMap<k,v> newInstance(){
return new HashMap<k,v>();
}
静态工厂方法也有缺点:
①类如果不含公有的或者受保护的构造器,就不能被子类化。对于公有的静态工厂方法所返回的非公有类也同样如此。
②它们与静态方法实际上没有什么区别。
简而言之,静态工厂方法和公有构造器都各有用处,我们需要理解它们各自的长处。结合实际情况,再做选择。