Effective Javax学习笔记--使可变性最小

这里正式进入第4章“类与接口”,其中第15和16条主要涉及类的封装,相关内容在Code Complete的第六章已经有了较为详细的描述,因此就不再重复了,直接从第17条开始。首先说一下为什么类要保证可变性最小。

为什么要使得类的可变性最小

确保类的可变性最小是面向对象设计中的一项重要原则,它不仅有助于提升代码的质量,还能增强系统的稳定性和可维护性。主要体现在下面几方面:

  • 线性安全性:可变类在多线程的环境下是线程不安全的,需要设置一定的线程安全措施
  • 类的可变性越大其被错误篡改的可能性越大,而不可变类没有这个问题可以被自由的共享。
  • 减少错误提高可靠性:类的可变性越大,其状态就越容易受到外部因素的影响,从而增加了出错的机会。不可变类由于其状态固定,可以避免意外的修改,减少了潜在的错误来源。此外,不可变对象的行为更加可预测,因为它们不会因为外部环境的变化而改变,这使得系统整体更加可靠和稳定。
  • 提升封装性:最小化可变性意味着更严格的封装。通过限制对对象状态的直接访问和修改,可以更好地控制对象的生命周期和行为。私有成员变量和只读访问(如通过getter方法)有助于保护对象的内部状态,防止不当的修改。这种封装性进一步增强了对象的独立性,也简化了对象间的交互。
  • 适用于缓存和共用:不可变类非常适合缓存和重用,因为它们的状态永远不会改变。这意味着一旦创建了不可变对象,就可以安全地在不同的上下文中多次使用,而不用担心数据的时效性。这种特性在性能敏感的应用场景中尤其有价值,如在高并发系统中,可以减少不必要的对象创建和垃圾回收开销。

所以只要没有理由,就需要把类的各项元素尽可能地设置为不可变,而这方面的极致就是不可变类(类中所有成员在初始化后都不可变)。

什么是不可变类

对于被判断无需进行不可变类是指其实例在初始化以后不可以被修改的类。不可变类的每一个实例的所有信息在创建该实例的时候都应该被提供,并在整个生命周期中固定不变。那要符合什么原则才能够真正保证类的不可变呢?文中列出了5点:

  • 不要提供任何会修改对象状态的方法:重点就是setter方法。
  • 保证类不会被扩展:如果类允许被扩展,则子类可以通过改写父类的相关方法破坏其不可变性。
  • 声明所有域都是final的:所有内部成员的引用在指向成员的初始化值后不能再被更改。
  • 声明所有的域都是私有的:外部无法访问修改成员变量
  • 确保对于任何可变组件的访问有且只有一个(在类的内部):不能直接向外部的引用返回内部私有类实例的引用,这样会使得内部可变实例的引用有内外部两个。

这几条原则里,有几条需要重点说明一下:

  • 第2条其实有两种实现方式,一是直接添加final关键字,二是将构造函数设为私有,通过静态工厂方法来初始化类,这样由于子类无法通过父类的构造方法来初始化父类的属性,所以也可以实现禁止扩展的目的。
  • 第3条其实有些过于强硬了,实际的要求应该是没有一个方法可以对对象的状态产生外部可见的改变。这里可以允许一些开销昂贵的域设置为非final,当第一次请求执行相关计算的时候将结果缓存在这些域中,等到后续再次计算时就不需要重新计算而是可以直接返回缓存的值,从而节约了计算的开销。由于不可变对象整体是不可变的,这也保证了相关的非final域无法被改变。
  • 第5条是对于类方法提的要求,需要通过在子程序设计中不对实例进行修改而是返回新的对象来实现。这里举一个例子:
public final class complexNumber{
    private final double real;
    private final double imaginary;

    public double getReal(){
        return this.real;
    }

    public double getImaginary(){
        return this.imaginary;
    }

    public complexNumber(double real, double imaginary){
        this.real = real;
        this.imaginary = imaginary;
    }

        public complexNumber add(complexNumber o){
            return new complexNumber(this.real + o.real, this.imaginary + o.imaginary);
        }

        public complexNumber subTract(complexNumber o){
            return new complexNumber(this.real-o.real, this.imaginary-o.imaginary);
        }

        public complexNumber multiply(complexNumber o){
            return new complexNumber(this.real*o.real - this.imaginary*o.imaginary, this.real*o.imaginary + this.imaginary*o.real);
        }

        
        public complexNumber divide(complexNumber o){
            if(o.real==0||o.imaginary==0){
                throw new IllegalArgumentException("Cannot divide by zero");
            }
            return new complexNumber(this.real/o.real, this.imaginary/o.imaginary);
        }
}

这里对于ComplexNumber的四则运算并未对原有实例的成员变量进行修改,同时也未直接返回内部成员变量的引用,而是返回一个新的ComplexNumber,这就维护了类的不可变属性。

不可变类的优势

在文章开头已经初步说明了不可变性的优势比较抽象,这里会具体说说使用不可变类带来的好处。

  • 不可变对象本质上是线性安全的,不要求做同步:这个上文已经说了。
  • 不可变对象可以自由的共享,甚至可以共享他们的内部信息:前半句话没有问题,主要是后半句话我认为作者的意思是在新建不可变类的其他实例时,可以安全的复用原实例中的内部信息(如作者举的BigInteger的negate方法例子),而不是可以对外直接共享内部信息(这与上文不可变类的第5个原则冲突)。
  • 不可变对象为其他对象提供了大量的构件:我们知道对象的不可变性除了通过final关键字来限制引用不变以外,更为重要的是要限制实例的可变性。不可变对象就可以保证实例的不可变性,所以对于各种集合类等包含了其他对象的类就可以把不可变对象作为基础的构件。
  • 不可变对象无偿提供了失败的原子性:不可变类的状态永远不变,因此不存在临时不一致的可能性。

不可变类的劣势

不可变类的唯一一个缺点就是对于每一个不同的值都需要一个单独的对象。这对于一些重量级类来说是非常浪费资源和性能的。这里作者提出了一种解决方法--可变配套类。

可变配套类

可变配套类顾名思义就是不可变类的一个辅助类,它具有可变的属性,协助匹配的不可变类提升运行效率。

我们知道当不可变类的类方法是一个多步骤操作的时候,如果完全通过不可变类的方法来执行,理论上每一个步骤都会产生一个新的不可变对象,但是在这个操作结束以后,所有过程步骤中的不可变对象又会被删去,这就极大的影响了操作的执行效率。这里如果可以将过程步骤的产出改为基本类型输出则最好,如果不行的话就需要新设置一个包级私有的可变配套类来执行这些过程步骤,最后如果相关的过程步骤完全不可预测时,可以所幸设置一个公有的可变配套类,在需要执行多步骤操作的时候直接通过可变类来实现而非相匹配的不可变类。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值