关于不可变类,你真的了解了吗

提到不可变类,大家的第一反应一定就是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类是不可变类

自定义不可变类

如果需要创建自定义的不可变类,可遵守如下规则。

  1. 使用private和final修饰符来修饰该类的Field。
  2. 提供带参数构造器,用于根据传入参数来初始化类里的Field。
  3. 仅为该类的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对象

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章