java 什么是不可变对象

JavaScript016

java 什么是不可变对象,第1张

不可变对象是指一个对象的状态在对象被创建之后就不再变化。不可变对象对于缓存是非常好的选择,因为你不需要担心它的值会被更改。

创建一个不可变类:

将类声明为final,所以它不能被继承;

将所有的成员声明为私有的,这样就不允许直接访问这些成员;

对变量不要提供setter方法;

将所有可变的成员声明为final,这样只能对它们赋值一次;

通过构造器初始化所有成员,进行深拷贝(deep copy);

在getter方法中,不要直接返回对象本身,而是克隆对象,并返回对象的拷贝;

示例:

public final class Immutable {  

    private final int id  

    private final String name  

    private final HashMap map  

   

    public int getId() {  

        return id  

    }  

   

    public String getName() {  

        return name  

    }  

   

    /** 

     * 可变对象的访问方法 

     */  

    public HashMap getMap() {  

        return (HashMap) testMap.clone()  

    }  

   

    /** 

     * 实现深拷贝的构造器*/  

    public Immutable(int i, String n, HashMap hm){this.id=i  

        this.name=n  

        HashMap tempMap=new HashMap()  

        String key  

        Iterator it = hm.keySet().iterator()  

        while(it.hasNext()){  

            key=it.next()  

            tempMap.put(key, hm.get(key))  

        }  

        this.map = tempMap  

    }  

  

}

不变模式有两种形式:弱不变模式和强不变模式。

弱不变模式,指类实例的状态是不可改变,但是这个类的子类的实例具有可能会变化的状态,要实现弱不变模式,一个类必须满足下面条件:

对象没有任何会修改对象状态的方法 ,这样一来当对象的构造函数将对象的状态初始化之后,对象的状态便不再改变;

属性都是私有的,以防客户端对象直接修改内部状态;

这个对象所引用的其他对象如果是可变的话,必须设法限制外界对这些可变对象的访问,以防止外界修改这些对象,尽量在不可变对象内部初始化这些被引用的对象,而不要在客户端初始化再传入到不可变对象内部来,如果某个可变对象必须在客户端初始化,然后再传入到不变对象里的话,就应当考虑在不可变对象初始化的时候,将这个可变对象进行拷贝。

弱不变模式的缺点是,一个弱不变对象的子对象可能是可变的,这个可变的子对象可能可以修改父对象的状态,从而可能允许外界修改父对象的状态。

强不变模式:一个类的实例不会改变,同时它的子类的实例也具有不可变化的状态。一个类必须首先满足弱不变模式所要求的所有条件,并且还有满足下面条件之一:

类所有的方法都应当是final,这样这个类的子类不能够重写此类的方法;

此类本身就是final的,那么这个类就不可能会有子类,从而也就不可能有被子类修改的问题;

不变和只读的区别:当一个变量是只读时,变量的值不能直接改变,但是可以在其他变量发生改变的时候改变;比如生日是不变属性,而年龄是只读的,年龄会随着时间发生变化,生日则不会变化。(用final声明的变量是只读的)

不可变对象的好处:

不可变对象更容易构造,测试与使用;

真正不可变对象都是线程安全的;

不可变对象的使用没有副作用(没有保护性拷贝);

对象变化的问题得到了避免;

不可变对象的失败都是原子性的;

不可变对象更容易缓存,且可以避免null引用;

不可变对象可以避免时间上的耦合;

String,StringBuffer,StringBuilder,都是final类,不允许被继承,在本质上都是字符数组,不同的是,String的长度是不可变的而后两者长度可变,在进行连接操作时,String每次返回一个新的String实例,而StringBuffer和StringBuilder的append方法直接返回this,所以当进行大量的字符串连接操作时,不推荐使用String,因为它会产生大量的中间String对象。

StringBuffer和StringBuilder的一个区别是,StringBuffer在append方法前增加了一个synchronized修饰符,以起到同步的作用,为此也降低了执行效率;若要在toString方法中使用循环,使用StringBuilder。

不可变类是指实例不能被修改的类。每个实例中包含的信息都必须在创建该实例的时候就提供,并在对象的整个生命周期内固定不变。你也许时常听说,String就是一个典型的不可变类。

让我们思考一下,如果一个类的实例从创建后就不能再修改,它有什么好处?

最容易想到的是,它用起来 简单 。它不会提供过多的方法,大概只会提供一些访问方法,这对于用户是件非常愉快的事情。

其次,当一个类不可变的时候,我们就可以 复用 它。就像String对象池那样,相同的对象只需要创建一个,这样可以极大地节省空间。

不仅可以复用,还可以 共享 。不可变对象本质上是线程安全的,多个线程并发访问同一个对象不会造成任何线程安全性问题。

最后,不可变类可以很方便地作为其它不可变类的成员。这将在后面的叙述中给出原因。

不可变类唯一的 缺点 是,对于每个不同的值都需要一个单独的对象。如果对象很大,对它做一点点的修改就重新生成另一个对象,在性能上是无法接受的。即使是String类也存在这样的问题,但好在Java平台给出了解决方案,当频繁改变字符串时使用StringBuilder代替String,可以获得较好的性能。

要想使一个类成为不可变类,需要遵循下面五条规则:

让我们根据这些规则实现一个Period类,该类用来表示一段不可变的时间周期。

在Period中,我们只提供了一个构造方法和两个访问方法,并没有提供任何会修改对象状态的方法,因此满足第一条规则。

通过在类名称前添加 final 关键字,保证了该类不会被扩展,满足第二条规则。

所有的域都是final并且私有的,满足三四条规则。

最关键的地方在于我们是如何满足第五条规则的。由于 Date 是一个可变类,因此在构造方法中,并没有直接把用户传入的 Date 对象赋值给私有域,而是各拷贝了一份相同的对象再赋值给私有域。同样的,在访问方法 start 和 end 中,也没有直接把私有域指向的对象返回给客户端,而是各拷贝了一份副本返回。这种做法就叫做 保护性拷贝 ,客户端永远无法拿到不可变对象私有域的引用,而只能拿到相应的副本,因此也就无法改变不可变对象。

这段程序还有几处需要注意的地方:

其实,Java API的设计者早已把这些规则应用到了String等不可变类上,大家可以查看String类的源码,它的构造方法、静态工厂方法以及其它很多方法都使用了保护性拷贝技术。

关注 作者 或 文集《Effective Java》 ,第一时间获取最新发布文章。

immutable,副作用,纯函数关键词解释:

js 中的对象一般是可变的,因为使用了引用赋值,虽然可以节约内存,但是会有一些隐患。一般做法使用浅拷贝和深拷贝来避免修改,但是这样会造成 CPU 和内存浪费。

为了解决这个问题,出现了 immutable.js 和 immer

immutable.js 实现原理: Persistent Data Structure (持久化数据结构)。

内部实现了一套 Persistent Data Structure,还有很多易用的数据类型像 Collection 、 List 、 Map 、 Set 、 Record 、 Seq 。有非常全面的 map 、 filter 、 groupBy 、 reduce``find 函数式操作方法。同时 API 也尽量与 Object 或 Array 类似。

缺点:

和 redux 的配合使用,redux 简化了 Flux 中多个 store 的概念,只有一个 store,数据操作通过 reducer 使用。reducer 就是要接受一个纯函数。

mobx 作者写的一个 immutable 库,核心实现利用 es6 的 proxy。

immer 使用原生数据结构的 api,而不像 immutable-js 转化为内置对象的 api。

学习成本低,就是把之前的操作放置到 produce 函数的第二参数函数中去执行。

原理:使用了一个 ES6 的新特性 Proxy 对象。深层嵌套对象的结构化共享的处理

proxy 具体可以看 Proxy

Proxy 无法 polyfill,所以 immer 在不支持 Proxy 的环境中,使用 Object.defineProperty 来进行一个兼容。

immer 维护一份 state 在内部,劫持所有操作,内部来判断是否有变化从而最终决定如何返回。