要了解String类创建的实例为什么不可变,首先要知道final关键字的作用:final的意思是“最终,最后”。
final关键字可以修饰类、方法、字段。 修饰类时,这个类不可以被继承; 修饰方法时,这个方法就不可以被覆盖(重写),在JVM中也就只有一个版本的方法--实方法; 修饰字段时,这个字段就是一个常量。
查看java.lang.String方法时,可以看到:
public final class String implements java.io.Serializable, Comparable<String>, CharSequence {
/** The value is used for character storage. */
private final char value[];
...
}
我们在创建一个String时候 如:String str=new String("ab");实际上是创建了final char value['a','b'],而这里的str仅仅是保存的这个char数组的引用地址,我们在修改时候,比如str="ccc";实际上是将str的引用地址给改变了,但是我们原来的被final修饰的数组并没有改变.
String一些方法诸如replace,substring等等看似改变了字符串的值,时间上只是改变了引用指向的地址,其实他们底层都是通过创建新的String对象来返回的,并不是修改以前的. 可以看下源码.
从上文可知String的成员变量是private final 的,也就是初始化之后不可改变。那么在这几个成员中, value比较特殊,因为他是一个引用变量,而不是真正的对象。value是final修饰的,也就是说final不能再指向其他数组对象,那么我能改变value指向的数组吗? 比如将数组中的某个位置上的字符变为下划线“_”。 至少在我们自己写的普通代码中不能够做到,因为我们根本不能够访问到这个value引用,更不能通过这个引用去修改数组。
那么用什么方式可以访问私有成员呢? 没错,用反射,可以反射出String对象中的value属性, 进而改变通过获得的value引用改变数组的结构。 示例代码:
public static void testReflection() throws Exception {
//创建字符串"Hello World", 并赋给引用s
String s = "Hello World";
System.out.println("s = " + s); //Hello World
//获取String类中的value字段
Field valueFieldOfString = String.class.getDeclaredField("value");
//改变value属性的访问权限
valueFieldOfString.setAccessible(true);
//获取s对象上的value属性的值
char[] value = (char[]) valueFieldOfString.get(s);
//改变value所引用的数组中的第5个字符
value[5] = '_';
System.out.println("s = " + s); //Hello_World
}
//打印结果
s = Hello World
s = Hello_World
在这个过程中,s始终引用的同一个String对象,但是再反射前后,这个String对象发生了变化, 也就是说,通过反射是可以修改所谓的“不可变”对象的。但是一般我们不这么做。这个反射的实例还可以说明一个问题:如果一个对象,他组合的其他对象的状态是可以改变的,那么这个对象很可能不是不可变对象。例如一个Car对象,它组合了一个Wheel对象,虽然这个Wheel对象声明成了private final 的,但是这个Wheel对象内部的状态可以改变, 那么就不能很好的保证Car对象不可变。
不可变类只是它的实例不能被修改的类。每个实例中包含的所有信息都必须在创建该实例时就提供,并在对象 的整个生命周期内固定不变。String、基本类型的包装类、BigInteger和BigDecimal就是不可变得类。
为了使类成为不可变,必须遵循以下5条规则: ①不要提供任何会修改对象状态的方法。 ②保证类不会被扩展。 ③使所有的域都是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>();
}
①类如果不含公有的或者受保护的构造器,就不能被子类化。对于公有的静态工厂方法所返回的非公有类也同样如此。
②它们与静态方法实际上没有什么区别。
简而言之,静态工厂方法和公有构造器都各有用处,我们需要理解它们各自的长处。结合实际情况,再做选择。