美文网首页
第十七条:使可变性最小化

第十七条:使可变性最小化

作者: Js_Gavin | 来源:发表于2021-02-06 15:08 被阅读0次

    不可变类是指其实例不可被修改的类。每个实例中包含的所有信息都必须在创建该实例的时候就提供,并在对象的整个声明周期内固定不变。Java平台类库种包含许多不可变的类,其中有String、基本类型的包装类、BigInteger和BigDecimal。存在不可变类有许多理由:不可变的类比可变类更加易于设计、实现和使用。它们不容易出错,且更加安全

    为了使类成为不可变、要遵循下面五条规则:
    1、不要提供任何会修改对象状态的方法(也称为设值方法)。

    2、保证类不会被扩展。这样就可以防止粗心或者恶意的子类假装对象的状态已经改变,从而破坏该类的不可变类。为了防止子类化,一般的做法是声明这个类成为final的,但是后面的我们还会讨论其他的做法。

    3、声明所有的域都是final的。通过系统的强制方式可以清楚地表明你的意图。而且,如果一个指向新创建实例地引用在缺乏同步机制地情况下,从一个线程被传递到另一个线程,就必须确保正确的行为,正如内存模型中所述。

    4、声明所有的域都为私有的这样可以防止客户端获得访问被域引用的可变对象的权限,并防止客户端直接修改这些对象。虽然从技术上讲,允许不可变的类具有公有的final域,只要这些域包含基本类型的值或者指向不可变对象的引用,但是不建议这样做,因为这样会使得再以后的版本中无法再改变内部的表示法(详见第15条和第16条)。

    5、确保对于任何可变组件的互斥访问。如果类具有指向可变对象的域,则必须确保该类的客户端无法获得指向这些对象的引用,并且,永远不要用客户端提供的对象引用来初始化这样的域,也不要从任何方法中返回该对象。在构造器、访问方法和readObject方法(详见第88条)中请使用保护性拷贝技术(详见第50条)。

    前面条目中的许多例子都是不可变的,其中一个例子是第11条中的PhoneNumber,他针对每个属性都有访问方法,但是没有对应的设置方法。下面稍微复杂点的例子:

    // Immutable complex number class 
    public final class Complex {
        private final double re; 
        private final double im;
    
        public Complex(double re, double im) { 
          this.re = re;
          this.im = im;
        }
        public double realPart() { return re; } 
        public double imaginaryPart() { return im; }
    
        public Complex plus(Complex c) {
          return new Complex(re + c.re, im + c.im);
        }
        public Complex minus(Complex c) { 
          return new Complex(re - c.re, im - c.im);
        }
        public Complex times(Complex c) {
          return new Complex(re * c.re - im * c.im, re * c.im + im * c.re); 
        }
        public Complex dividedBy(Complex c) {
          double tmp = c.re * c.re + c.im * c.im;
          return new Complex((re * c.re + im * c.im) / tmp, (im * c.re - re * c.im) / tmp);
        }
    
        @Override
        public boolean equals(Object o) { 
          if (o == this)
             return true;
          if (!(o instanceof Complex))
             return false;
           Complex c = (Complex) o;
          // See page 47 to find out why we use     compare instead of == return 
           Double.compare(c.re, re) == 0 && Double.compare(c.im, im) == 0; 
        }
    
        @Override 
        public int hashCode() {
          return 31 * Double.hashCode(re) + Double.hashCode(im);
        }
    
       @Override 
       public String toString() { 
          return "(" + re + " + " + im + "i)";
        } 
    }
    

    这个类表示一个复数(complex number,具有实部和虚部)。除了标准的Object方法之外,它还提供了针对实部和虚部的访问方法,以及4种基本的算术运算:加法、减法、乘法和除法。注意这些算术运算如何创建并返回新的Complex实例,而不是修改这个实例。大多数重要的不可变都使用了这种模式。他被称之为函数的方法,因为这些方法返回了一个函数的结果,这些函数对操作数进行运算但并不修改它。与之相对应的更常见的是过程或者命令式的方法,使用这些方法时,将一个过程作用在它们的操作数上,会导致它的状态发生改变。注意,这些方法名称都是介词(如plus),而不是动词(如add)。这是为了强调该方法不会改变对象的值。BigInteger类的BigDecimal类由于没有遵守这一命名习惯,就导致了许多用法错误。

    如果你对函数方式的做法还不太熟悉,可能会觉得它显得不太自然,但是它带来了不可变性,具有许多优点。不可变对象比较简单。不可变对象可以只有一种状态,即被创建时的状态。如果你能够确保所有的构造器都建立了这个类的约束关系,就可以确保这些约束关系在整个生命周期内永远不再发生变化,你和使用这个类的程序员都无须再做额外的工作来维护这些约束关系。另一方面,可变对象可以有任意复杂的状态空间。如果文档中没有为设值方法所执行的状态转换提供精确的描述,要可靠地使用可变类时非常困难地,甚至是不可能的。

    不可变对象本质上是线程安全的,它们不要求同步。当多个线程并发访问这样的对象时,它们不会遭到破坏。这无疑是获得了线程安全最容易的办法。实际上,没有任何线程会注意到其他线程对不可变对象的影响。所以,不可变对象可以被自由地共享。不可变类应该充分利用这种优势,鼓励客户端尽可能地重用现有地实例。要做到这一点,一个很简单地办法就是:对于频繁用到的值,为它们提供公有地静态final常量。例如,Complex类会提供下面地常量:

    public static final Complex ZERO = new Complex(0, 0); 
    public static final Complex ONE = new Complex(1, 0); 
    public static final Complex I = new Complex(0, 1);
    

    这种方法可以进一步扩展。不可变的类可以提供静态工厂(详见第1条),它们把频繁被请求的实例缓存起来,从而当现有的实例可以符合请求的时候。就不必创建新的实例。所有基本类型的包装类和BigInteger都有这样的静态工厂。使用静态工厂也使得客户端之间可以共享现有的实例,而不是创建新的实例,从而降低内存占用和垃圾回收器的成本,在设计新的类时,选择用静态工厂代替公有的构造器可以让你以后有添加缓存的灵活性,而不必影响客户端。

    ”不可变对象可以被自由的共享“导致的结果是,永远不需要进行保护性拷贝(详见第50条)。实际上,你根本无须做任何拷贝,因为这些拷贝始终等于原始的对象。因此,你不需要,也不应该为不可变的类提供clone方法或者拷贝构造器(详见第13条)。这一点再Java平台的早期并不好理解,所以String类仍然具有拷贝构造器,但是应该尽量少用它(详见第6条)。

    不仅可以共享不可变对象,甚至也可以共享它们的内部信息。例如,BigInteger类内部使用了符号数值表示法。符号用一个int类型的值来表示,数值则用一个int数组来表示。negate方法产生一个新的BigInteger,其中数值是一样的,符号则是相反的。它并不需要拷贝数组,新建的BigInetger也指向原始实例中的同一个内部数组。

    不可变对象为其他对象提供了大量的构件,无论是可变的还是不可变的对象。如果知道一个复杂对象内部的组件对象不会改变,要维护它的不变性约束是比较容易的。这条原则的一种特例在于,不可变对象构成了大量的映射值(map key)和集合元素(set element);一旦不可变对象进入映射(map)或者集合(set)中,尽管这破坏了映射值或者集合的不变性约束,但是也不用担心它们的值会发生变化。

    不可变对象无偿的提供了失败的原子性(详见第76条),它们的状态永远不变,因此不存在临时不一致的可能性。

    不可变对象真正唯一的缺点是,对于每个不同的值都需要一个单独的对象。创建这些对象的单价可能很高,特别是大型的对象。例如,假设你有一个上百万为的BigInteger,想要改变它的低位:

    BigInteger moby  = ...;
    moby = moby.flipBit(0);
    

    flipBit方法创建了一个新的BigInteger实例,也有上百位长,它与原来的对象只差一位。这项操作所消耗的时间和空间与BigInteger的成正比。我们拿它与java.util.BitSet比较。与BigInteger类似,BitSet代表一个任意长度的位序列,但是与BigInteger不同的是,BitSet是可变的。BitSet类提供了一个方法,允许在固定的时间内改变”百万位“实例中单个位的状态:

    BitSet moby  = ...;
    moby.flip(0);
    

    如果你执行一个多步骤的操作,并且每一个步骤都会产生一个新的对象,除了最后的结果之外,其他的对象最终都会被丢弃,此时性能问题就会显露出来。处理这种问题有两种办法。第一种,先猜测一下经常会用到哪些多步骤的操作,然后将它们作为基本类型提供。如果某个多步骤操作已经作为基本类型提供。例如:BigInteger有包级私有的可变”配套类“,它的 用途是加速诸如”模指数“这样的多步骤操作。由于前面提到的诸多原因,使用可变的配套类比使用BigInteger要困难得多,但幸运的是,你并不需要这样做。因为BigInteger的实现者已经替你完成了所有的困难工作。

    如果能够精确的预测出客户端将要在不可变的类上执行哪些复杂的多阶段操作,这种包级私有的可变配套类的方法就可以工作地很好,如果无法预测,最好地办法是提供一个公有地可配套类。在Java平台类库中,这种方法地主要例子是String类,它的可配套类是StringBuilder(及其已经被废弃地祖先StringBuffer)。

    现在你已经知道了如何构建不可变的类,并且了解了不可变性的优点和缺点,现在我们来讨论其他的一些设计方案。前面提到过,为了确保不可变性,类绝对不允许自生被子类化。除了使类成为final的这种方式之外,还有另一种更加灵活的办法可以做到这一点。不可变的类变成final的另一种办法是,让类的所有构造器都变成私有的或者包级私有的,并添加公有的静态工厂来代替公有的构造器(详见第1条)。为了具体说明这种办法,下面以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);
      }
    }
    

    这种方法虽然并不常用,但它通常是最好的替代方法。它最灵活,因为它允许使用多个包级私有的实现类。对于处在包外的客户端而言,不可变的类实际上是final的,因为不可能对来自另一个包的类、缺少公有的或者受保护的构造器的类进行扩展。除了允许多个实现类的灵活性之外,这种方法还是得有可能通过改善静态工厂的对象缓存能力,在后续的发行版本中改进该类的性能。

    当BigIntege和BigDecimal刚被编写出来的时候,对此“不可变的类必须为final”的说法还没有得到广泛的理解,所以它们的所有方法都有可能被覆盖。遗憾的是,为了保持向后兼容,这个问题一直无法得以修正。如果你正在编写一个类,它的安全性依赖于来自不可信任客户端的BigInteger或者BigDecimal参数的不可变性,就必须进行检查,以确保这个参数是否为"真正的"BigInteger或者BigDecimal,而不是不可信任子类的实例。如果是后者,就必须在假设它可能是可变的前提下对它进行保护性拷贝(详见第50条):

    public static BigInteger safeInstance(BigInteger val) { 
      return val.getClass() == BigInteger.class ?
               val : new BigInteger(val.toByteArray()); 
    }
    

    本条开头关于不可变类的诸多规则指出,没有方法会修改对象,并且它的所有域都必须是final的。实际上,这些规则比真正的要求更强硬了一点,为了提高性能可以有所放松。事实上应该是这样:没有一个方法能够对对象的状态产生外部可见的改变。然而,许多不可变的类拥有一个或者多个非final的域,它们在第一次被请求执行这些计算的时候,把一些开销昂贵的计算结果缓存在这些域中。如果将来再次请求同样的计算,就直接返回这些缓存的值,从而节约了重新计算所需要的开销。这种技巧可以很好的工作,因为对象是不变的,它的不可变性保证了这些计算如果被再次执行,就会产生同样的结果

    例如,PhoneNumber类的hashCode方法(详见第11条)在第一次被调用的时候,计算出散列码,然后把它缓存起来,以备将来再次调用时使用。这种方法是延迟初始化(详见第83条)的一个例子,String类也用到了。

    有关序列化功能的一条告诫有必要在这里提出来。如果你选择让自己的不可变类实现Serializable接口,并且它包含一个或者多个指向可变对象的域,就必须提供一个显式的readObject或者readResolve方法,或者使用ObjectOutputStream.writeUnshared和ObjectOutputStream.readUnshared方法,即便默认的序列化形式是可以接受的,也是如此。否则,攻击者可能从不可变的类创建可变的实例。
    关于这个话题的详情请参考第88条。

    总之,坚决不要为每个get方法编写一个相应的set方法。除非有很好的理由要让类成为可变的类,否则它就应该是不可变的。不可变的类有许多优点,唯一的缺点是在特定的情况下存在潜在的性能问题。你应该总是使一些小的值对象,比如PhoneNumber和Complex成为不可变的。(在Java平台类库中,有几个类如java.util.Date和java.awt.Point,它们本应该是不可变得,单实际上却不是。)你也应该认真考虑把一些较大的值对象做成不可变的,例如String和BigInteger。只有当你确认有必要实现令人满意的性能(详见第67条),才应该为不可变的类提供公有的可变配套类。

    对于某些类而言,其不可变性是不切实际的。如果类不能被做成不可变的,仍然应该尽可能地限制它的可变性。降低对象可以存在的状态数,可以更容易地分析该对象地行为,同时降低出错地可能性。因此,除非有令人信服地理由使域变成非final的,否则让每个域都是final的。结合这条的建议和第15条的建议,你自然倾向于:除非有令人信服的理由要使域变成非final的,否则要使每个域都是private final 的

    构造器应该创建完全初始化的对象,并建立其所有的约束关系。不要在构造器或者静态工厂之外再提供公有的初始化方法,除非有令人信服的理由必须这么做。同样地,也不应该提供”重新初始化“地方法(它使得对象可以别重用,就好像这个对象是由另一个不同地初始化状态构造出来地一样)。与所增加地复杂性相比,“重新初始化”方法通常并没有带来太多地性能优势。

    通过CountDownLatch类地例子可以说明这些原则。它是可变的,但是它的状态空间被有意地设计得非常小。比如创建一个实例,只使用一次,它的任务就完成了:一旦定时器的计数达到零,就不能重用了。

    最后值得注意的一点与本条目中的Complex类有关。这个例子只是被用来演示不可变性的,它不是一个工业强度的复数实现。它对复数乘法和除法使用标准的计算公式,会进行不正确的四舍五入,并且对复数NaN和无穷打也没有提供很好的语义。

    相关文章

      网友评论

          本文标题:第十七条:使可变性最小化

          本文链接:https://www.haomeiwen.com/subject/klzytltx.html