提到不可变类,大家的第一反应一定就是String类了,没错,String类就是不可变类,可大家真的理解了不可变类的意义了吗,还是说final class String 就代表了不可变类了?非也,今天我们就一起来看看何为不可变类
概念
不可变类的意思是创建该类的实例后,该实例的Field是不可改变的。
也就是说是该类的实例Field不可变才说明该类是不可变类,并不是说final修饰的类就是不可变类,final修饰的类只保证了该类不可被继承而已
java提供的8个包装类和java.lang.String类都是不可变类,当创建它们的实例后,其实例的Field不可改变
拿Double类来说
Double d = new Double(6.5);
查看Double类源码
private final double value;
/**
* Constructs a newly allocated {@code Double} object that
* represents the primitive {@code double} argument.
*
* @param value the value to be represented by the {@code Double}.
*/
public Double(double value) {
this.value = value;
}
如上图Double源码,当创建Double实例的时候实际上是在Double构造器中初始化了其成员变量final double value
,该value成员变量被final修饰(final修饰的变量一旦被初始化了则不能再被赋值
),所以该Double实例的值是不会被改变的
由此验证Double类是不可变类
自定义不可变类
如果需要创建自定义的不可变类,可遵守如下规则。
- 使用private和final修饰符来修饰该类的Field。
- 提供带参数构造器,用于根据传入参数来初始化类里的Field。
- 仅为该类的Field提供getter方法,不要为该类的Field提供setter方法,提供了也没法修改
(反射情况除外)
。
下面例子验证了反射可以修改final修饰的成员变量
System.out.println("修改前:" + d);
Class c = Double.class;
Field f = c.getDeclaredField("value");
f.setAccessible(true);
f.set(d, 6.6);
System.out.println("修改后:" + d);
修改前:6.5
修改后:6.6
与不可变类对应的是可变类,可变类的含义是该类的实例Field是可变的。大部分时候所创建的类都是可变类,特别是JavaBean,因为总是为其Field提供了setter和getter方法。
与可变类想比,不可变类的实例在整个生命周期中永远处于初始化状态,它的Field不可改变。因此对不可变类的实例的控制将更加简单。
缓存实例的不可变类
不可变类的实例状态不可改变(通俗点讲就是成员变量不可改变
),可以很方便地被多个对象所共享。如果程序经常需要使用相同的不可变类实例,则应该考虑缓存这种不可变类的实例。毕竟重复创建相同的对象没有太大的意义,而且加大系统开销。如果可能,应该将已经创建的不可变类的实例进行缓存。
如大家所知的字符串常量池机制就是一种缓存机制,String类是不可变类,所以字符串不可修改,那么就没必要重复创建同一个字符串,我不能修改这个对象干嘛要创建多个相同的字符串呢?这样就达到了节约内存资源的目的,也可以提高效率
class CacheImmutale{
private static int MAX_SIZE = 10;
//使用数组来缓存已有的实例
private static CacheImmutale[] cache = new CacheImmutale[MAX_SIZE];
//记录缓存实例在缓存中的位置,cache[pos-1]是最新缓存的实例
private static int pos = 0;
private final String name;
private CacheImmutale(String name){
this.name = name;
}
public String getName(){
return name;
}
public static CacheImmutale valueOf(String name){
//遍历已缓存的对象
for (int i=0; i<MAX_SIZE; i++){
//如果已有相同的实例,则直接返回该缓存的实例
if (cache[i] != null && cache[i].getName().equals(name)){
return cache[i];
}
}
//如果缓存池已满
if (pos == MAX_SIZE){
//把缓存的第一个对象覆盖,即把刚刚生成的对象放在缓存池的最开始位置
cache[0] = new CacheImmutale(name);
//把pos设为1
pos = 1;
}else {
//把新创建的对象缓存起来,pos加1
cache[pos++] = new CacheImmutale(name);
}
return cache[pos-1];
}
@Override
public boolean equals(Object obj){
if (this == obj) return true;
if (obj != null && obj.getClass() == CacheImmutale.class){
CacheImmutale ci = (CacheImmutale) obj;
return name.equals(ci.getName());
}
return false;
}
@Override
public int hashCode(){
return name.hashCode();
}
}
上述代码输出结果为true
上面CacheImmutale类使用一个数组来缓存该类的对象,这个数组长度为MAX_SIZE,即该类共可以缓存MAX_SIZE个CacheImmutale对象。当缓存池已满时,缓存池采用"先进先出"规则来决定哪个对象将被移出缓存池.
CacheImmutale类能控制系统生成CacheImmutale对象的个数,需要程序使用该类的valueOf方法来得到其对象,而且程序使用private修饰符隐藏该类的构造器,因此程序只能通过该类提供的valueOf方法来获取实例.
是否需要隐藏CacheImmutale类的构造器完全取决于系统需求。盲目乱用缓存也可能导致系统性能下降,缓存的对象会占用系统内存,如果某个对象只使用一次,重复使用的概率不大,缓存该实例就弊大于利;反之,如果某个对象需要频繁地重复使用,缓存该实例就利大于弊。
如java提供的java.lang.Integer类,它就采用了与CacheImmutale类相同的处理策略,如果采用new构造器来创建Integer对象,则每次返回全新的Integer对象;如果采用valueOf方法来创建Integer对象,则会缓存该方法创建的对象。
Integer in1 = Integer.valueOf(6);
Integer in2 = Integer.valueOf(6);
System.out.println(in1 == in2);
以上代码输出结果为true
注意:
Integer只缓存了-128~127之间的Integer对象