Effective Java读书笔记-使可变性最小化

存在不可变类的原因:不可变的类比可变的类更加易于设计、实现和使用。它们不容易出错且更加安全。

使类变为不可变需要遵守的五项原则:

  1. 不要提供任何会修改对象状态的方法。
  2. 保证类不会被拓展。这样可以防止粗心或者恶意的子类假装对象的状态已经改变,从而破坏类的不可变行为。为了防止子类化,一般的做法是使类变为final的。
  3. 使所有的域都是final的。
  4. 使所有的域都变为私有的。
  5. 确保任何可变组件的互斥访问。如果类具有指向可变对象的域,就必须确保该类的客户端无法获得指向这些对象的引用。并且,永远不要用客户端提供的对象引用来初始化这样的域,也不要从任何访问方法中返回该对象的引用。在构造器、访问方法和readObject方法中请使用保护性拷贝技术

不可变对象比较简单。不可变对象可以只有一种状态,即被创建时的状态。如果能够确保所有的构造器都建立了这个类的约束关系,就可以确保这些约束关系在整个类的生命周期中不再发生任何变化,你和使用这个类的程序员都无需进行任何动作来维护这些约束关系。

不可变对象本质上是线程安全的,它们不要求同步。不可变对象可以自由的被共享。不可变对象应当充分的利用这一优势,对于频繁应用的值,为它们提供静态的final常量。例如:

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);

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

不要给不可变的类提供拷贝:

“不可变的对象可以被自由的共享”导致的结果是,永远不需要进行保护性拷贝。实际上根本不需要做任何拷贝,因为这些拷贝始终等于原对象。因此不需要也不应该为不可变的对象提供clone方法或者拷贝构造器。这一点在Java的早期并不好理解,所以String类仍然具有拷贝构造器。

不仅可以共享不可变的对象,甚至可以共享它们的内部信息。
不可变对象为其它对象提供了大量的构件。不可变对象构成了大量的映射键和集合元素,一旦不可变对象进入到映射或者集合中,尽管破坏了映射或者集合的不变性约束,但是也不用担心它们的值会发生变化。

不可变类的缺点:

对每一个不同的值都需要一个单独的对象。创建这种对象的代价可能很高,特别对于大型对象的情形。
如果执行一个多步骤的操作,并且每个步骤都会产生一个新的对象,除了最后的结果之外其它的对象最后都会被丢弃,此时性能问题就显露出来了。处理这种问题有两种方法:

  • 猜测一下经常会用到的多步骤操作,然后将它们作为基本类型提供。如果某个多步骤操作已经作为基本类型提供了,不可变类就不必在每个步骤单独创建一个对象,不可变的类在内部可以更加的灵活。
  • 如果能够精准的预测出客户端将在不可变的类上执行那些复杂的多阶段操作,这种包级私有的可变配套类方法就可以工作的更好。如果无法预测,最好的办法就是提供一个公有的可变配套类。在Java平台类库中,这种方法的主要例子是String类,它的可变配套类是StringBuilder类。

确保类不变性的方法:

为了使类保持不变性,类不允许自身被子类化。除了使用final方法之外,还有一种更为灵活的方法可以做到这一点,就是让类的构造器变为私有的或者是包级私有的,添加静态工厂来代替公有的构造器。例如:

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);
    }
}

虽然这种方法不常用,但却是最好的替代方法。它是灵活的,因为允许多个包级私有类的实现。除了允许多个实现类的灵活性之外,这种方法还能够改善静态工厂的对象缓存能力,在后续的发行版本中改进该类的性能。

如果你在编写一个类的时候,它的安全性依赖于BigInteger或BigDecimal参数的不可变性,就必须进行检查,以确定这个参数是否为真正的BigInteger或BigDecimal,而不是不可信任子类的实例。如果是后者的话,就必须假设它可能是可变的前提下对它进行拷贝性保护。

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

不可变类的好处:

没有方法会修改对象,并且它的所有域都必须是final的。没有一个方法能够对对象的状态产生外部可见的改变。许多的不可变的类拥有一个或多个非final的域,它们在第一次被请求执行这些计算的时候,把一些开销昂贵的计算结果缓存在这些域中。如果将来再次请求同样的计算,就直接返回这些缓存的值,从而节约了重新计算所需要的开销。这种技巧可以很好的工作,因为对象是不可变的。它的不可变确保了这些计算如果再次被执行,就会产生同样的效果。

延迟初始化:

类的对象在第一次调用的时候,计算出相应的值,之后把它缓存起来,以备将来再次被调用的时候使用。

序列化时应当注意的问题:

如果你选择让自己不可变的类实现Serializable接口,并且包含一个或者多个指向可变对象的数据域,就必须提供一个显式的readObject或者readResolve方法,或者使用ObjectOutputStream.writeUnshared和ObjectInputStream.readUnshared方法。即使默认的序列化形式是可以接受的,也是如此。否则攻击者可能从不可变的类创建可变的实例。

注意的问题:

  • 总之,不要为每一个get方法编写一个相应的set方法。除非有很好的理由让类成为可变的类,否则就应该是不可变的。不可变的类有很多的优点,唯一的缺点是在特定的情况下存在潜在的性能问题。
  • 如果类不能被做成是不可变的,仍然应该尽可能的限制它的可变性。降低对象可以存在的状态数,可以更容易的分析该对象的行为,同时降低出错的可能性。除非有令人信服的理由要使域变成非final的,否则要使每个域都是final的。
  • 构造器应该创建完全初始化的对象,并建立起所有的约束关系。不要再构造器或者静态工厂之外再提供公有的初始化方法,除非有令人信服的理由必须这么做。同样的也不应该提供“重新初始化”的方法。“重新初始化”的方法通常没有带来太多的性能优势。
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值