Effective Java——类和接口(上)

                     目录
十三、使类和成员的可访问性最小化
十四、在公有类中使用访问方法而非公有域
十五、使可变性最小化toString
十六、复合优先于继承
十七、要么为继承而设计,并提供文档说明,要么就禁止继承


十三、使类和成员的可访问性最小化

        信息隐藏是软件程序设计的基本原则之一,面向对象又为这一设计原则提供了有力的支持和保障。这里我们简要列出几项受益于该原则的优势:
        1.更好的解除各个模块之间的耦合关系。由于模块间的相互调用是基于接口契约的,每个模块只是负责完成自己内部既定的功能目标和单元测试,一旦今后出现性能优化或需求变更时,我们首先需要做的便是定位需要变动的单个模块或一组模块,然后再针对各个模块提出各自的解决方案,分别予以改动和内部测试。这样便大大降低了因代码无规则交叉而带来的潜在风险,同时也缩减了开发周期。
        2.最大化并行开发。由于各个模块之间保持着较好的独立性,因此可以分配更多的开发人员同时实现更多的模块,由于每个人都是将精力完全集中在自己负责和擅长的专一领域,这样不仅提高了软件的质量,也大大加快了开发的进度。
        3.性能优化和后期维护。一般来说,局部优化的难度和可行性总是要好于来自整体的优化,事虽如此,然而我们首先需要做的却是如何定位需要优化的局部,在设计良好的系统中,完成这样的工作并非难事,我们只需针对每个涉及的模块做性能和压力测试,之后再针对测试的结果进行分析并拿到相对合理的解决方案。
        4.代码的高可复用性。在软件开发的世界中,提出了众多的设计理论,设计原则和设计模式,之所以这样,一个非常现实的目标之一就是消除重复代码,记得《重构》中有这样的一句话:“重复代码,万恶之源”。可见提高可用代码的复用性不仅对编程效率和产品质量有着非常重要的意义,对日后产品的升级和维护也是至关重要的。说一句比较现实的话,一个设计良好的产品,即使因为某些原因导致失败,那么产品中应用到的一个个独立、可用和高效的模块也为今后的东山再起提供了一个很好的技术基础。

        类具有公有的静态final数组域,或者返回这种域的访问方法,这几乎总是错误的,如:

public static final Thing[] VALUES = { ... };
        即便Thing数组对象本身是final的,不能再被赋值给其他对象,然而数组内的元素是可以改变的,这样便给外部提供了一个机会来修改内部数据的状态,从而在主类未知的情况下破坏了对象内部的状态或数据的一致性。其修改方式如下:
        1.使公有数组变成私有的,并增加一个公有的不可变列表。
private static final Thing[] PRIVATE_VALUES = { ... };
public static final List<Thing> VALUES = Collection.unmodifiableList(Arrays.asList(PRIVATE_VALUES));
        2.使公有数组变成私有的,并添加一个公有方法,它返回私有数组的一个备份。
private static final Thing[] PRIVATE_VALUES = { ... };
public static final Thing[] values() {
    return PRIVATE_VALUES.clone();
}

        总而言之,你应该尽可能地降低可访问性。你在仔细地设计了一个最小的公有API之后,应该防止把任何散乱的类、接口和成员变成API的一部分。除了公有静态final域的特殊情形之外,公有类都不应该包含公有域。并且要确保公有静态final域所引用的对象都是不可变的。


十四、在公有类中使用访问方法而非公有域

        1.对于公有类而言,由于存在大量的使用者,因此修改API接口将会给使用者带来极大的不便,他们的代码也需要随之改变。如果公有类直接暴露了域字段,一旦今后需要针对该域字段添加必要的约束逻辑时,唯一的方法就是为该字段添加访问器接口,而已有的使用者也将不得不更新其代码,以避免破坏该类的内部逻辑。
        2.对于包级类和嵌套类,公有的域方法由于只能在包内可以被访问,因而修改接口不会给包的使用者带来任何影响。
        3.对于公有类中的final域字段,提供直接访问方法也会带来负面的影响,只是和非final对象相
比可能会稍微好些,如final的数组对象,即便数组对象本身不能被修改,但是他所包含的数组成员还是可以被外部改动的,针对该情况建议提供API接口,在该接口中可以添加必要的验证逻辑,以避免非法数据的插入,如:

public <T> boolean setXxx(int index, T value) {
    if (index > myArray.length)
        return false;
    if (!(value instanceof LegalClass))
        return false;
    ...
    return true;
}


十五、使可变性最小化

        不可变类只是实例不能被修改的类。每个实例中包含的所有信息都必须在创建该实例的时候就提供,并在对象的整个生命周期内都会固定不变,如String、Integer等。不可变类比可变类更加易于设计、实现和使用,而且线程安全。

        使类成为不可变类应遵循以下五条规则:
        1.不要提供任何会修改对象状态的方法
        2.保证类不会被扩展,即声明为final类,或将构造函数定义为私有后加入静态工厂函数
        3.使所有的域都是final的
        4.使所有的域都成为私有的
        5.确保在返回任何可变域时,返回该域的deep copy

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 add(Complex c) {
        return new Complex(re + c.re,im + c.im);
    }
    public Complex substract(Complex c) {
        return new Complex(re - c.re, im - c.im);
    }
    ... ...
}
        不可变对象还有一个对象重用的优势,这样可以避免创建多余的新对象,如:
public static final Complex ZERO = new Complex(0,0);
public static final Complex ONE = new Complex(1,0);
        使用者可以重复使用上面定义的两个静态final类,而不需要在每次使用时都创建新的对象。

        对于不可变对象还有比较重要的优化技巧,既某些关键值的计算,如hashCode,可以在对象构造时或留待某特定方法(Lazy Initialization)第一次调用时进行计算并缓存到私有域字段中,之后再获取该值时,可以直接从该域字段获取,避免每次都重新计算。这样的优化主要是依赖于不可变对象的域字段在构造后即保持不变的特征。


十六、复合优先于继承

        由于继承需要透露一部分实现细节,因此不仅需要超类本身提供良好的继承机制,同时也需要提供更好的说明文档,以便子类在覆盖超类方法时,不会引起未知破坏行为的发生。需要特别指出的是对于跨越包边界的继承,很可能超类和子类的实现者并非同一开发人员或同一开发团队,因此对于某些依赖实现细节的覆盖方法极有可能会导致预料之外的结果,还需要指出的是,这些细节对于超类的普通用户来说往往是不看见的,因此在未来的升级中,该实现细节仍然存在变化的可能,这样对于子类的实现者而言,在该细节变化时,子类的相关实现也需要做出必要的调整,见如下代码:

//这里我们需要扩展HashSet类,提供新的功能用于统计当前集合中元素的数量
//实现方法是新增一个私有域变量用于保存元素数量,并每次添加新元素的方法中
//更新该值,再提供一个公有的方法返回该值
public class InstrumentedHashSet<E> extends HashSet<E> {
    private int addCount = 0;

    public InstrumentedHashSet() {}
    public InstrumentedHashSet(int initCap, float loadFactor) {
        super(initCap, loadFactor);
    }
    @Override
    public boolean add(E e) {
        ++addCount;
        return super.add(e);
    }
    @Override
    public boolean addAll(Collection<? extends E> c) {
        addCount += c.size();
        return super.addAll(c);
    }
    public int getAddCount() {
        return addCount;
    }
}
        该子类覆盖了HashSet中的两个方法add和addAll,而且从表面上看也非常合理,然而他却不能正常的工作,见下面的测试代码:
public static void main(String[] args) {
    InstrumentedHashSet<String> s = new InstrumentedHashSet<String>();
    s.addAll(Arrays.asList("Snap", "Crackle", "Pop"));
    System.out.println("The count of InstrumentedHashSet is " + s.getAddCount());
}
//The count of InstrumentedHashSet is 6
        从输出结果中可以非常清楚的看出,我们得到的结果并不是我们期望的3,而是6。这是什么原因所致呢?在HashSet的内部,addAll方法是基于add方法来实现的,而HashSet的文档中也并未列出这样的细节说明。因此我们用另外一种办法实现:在新的类中增加一个私有域,它引用现有类的一个实例。这种设计被称作“复合”,因为现有的类变成了新类的一个组件。新类中的每个实例方法都可以调用被包含的现有类实例中对应的方法,并返回它的结果,这被称为转发,新类中的方法被称为转发方法。下面的例子用复合/转发的方法代替InstrumentedHashSet类。注意这个实现分为两部分:类本身和可重用的转发类。
//转发类
class ForwardingSet<E> implements Set<E> {
    private final Set<E> s;

    public ForwardingSet(Set<E> s) {
        this.s = s;
    }
    public int size() {
        return s.size();
    }
    public void clear() {
        s.clear();
    }
    public boolean add(E e) {
        return s.add(e);
    }
    public boolean addAll(Collection<? extends E> c) {
        return s.addAll(c);
    }
    ... ...
    @Override
    pubic boolean equals(Object o) {
        return s.equals(o);
    }
    @Override
    pubic int hashCode() {
        return s.hashCode();
    }
    @Override
    pubic String toString() {
        return s.toString();
    }
}

//包装类
class InstrumentedSet<E> extends ForwardingSet<E> {
    private int addCount = 0;

    public InstrumentedSet(Set<E> s) {
        super(s);
    }
    @Override
    public boolean add(E e) {
        ++addCount;
        return super.add(e);
    }
    @Override
    public boolean addAll(Collection<? extends E> c) {
        addCount += c.size();
        return super.addAll(c);
    }
    public int getAddCount() {
        return addCount;
    }
}
        Set接口的存在使InstrumentedSet类的设计成为可能,因为Set接口保存了HashSet类的功能特性。除了获得健壮性之外,这种设计也带来了格外的灵活性。这个包装类nstrumentedSet<E>可以用来包装任何Set实现,如TreeSet和HashSet。

        继承的功能非常强大,但是也存在许多问题,因为它违背了封装原则。只有当子类真正是超类的子类型时,即两者之间确实存在“is-a”关系时,才适合用继承。即便如此,如果子类和超类处在不同的包中,并且超类并不是为了继承而设计的,那么继承将会导致脆弱性。为了避免这种脆弱性,可以用复合和转发机制来代替继承,尤其是当存在适当的接口可以实现包装类的时候。包装类不仅比子类更加健壮,而且功能也更加强大。


十七、要么为继承而设计,并提供文档说明,要么就禁止继承

        对于专门为了继承而设计的类,该类的文档必须精确地描述覆盖每个方法所带来的影响,即该类必须由文档说明它可覆盖的方法的自用性。对于每个公有的或受保护的方法或者构造器,它的文档必须指明该方法或者构造器调用了哪些可覆盖的方法,是以说明顺序调用的,每个调用的结果又是如何影响后续的处理过程的。

        类还必须遵守其他一些约束。构造器决不能调用可被覆盖的方法,无论是直接调用还是间接调用。如果违反了这条规则,很有可能导致程序失败。超类的构造器在子类的构造器之前运行,所以,子类中覆盖版本的方法将会再子类的构造器运行之前就先被调用。如果该覆盖版本的方法依赖于子类构造器所执行的任何初始化工作,该方法将不会如预期般地执行。如下面的例子:

public class SuperClass {
    public SuperClass() {
        overrideMe();
    }
    public void overrideMe() {
    }
}

public final class SubClass extends SuperClass {
    private final Date d;

    SubClass() {
        d = new Date();
    }

    @Override
    public void overrideMe() {
        System.out.println(d);
    }

    public static void main(String[] args) {
        SubClass sub = new SubClass();
        sub.overrideMe();
    }
}
        程序会打印日期两次,但是第一次打印出的是null,因为overrideMe方法被SuperClass构造器调用的时候,构造器SubClass还没有机会初始化d变量。要注意,如果overrideMe已经调用了d的任何方法,当SuperClass构造器调用overrideMe的时候,调用就好抛出NullPointerException异常。该程序没有抛出NullPointerException异常是因为println方法对于处理null参数有着特殊的规定。

        如果类是为了继承而被设计的,Cloneable和Serializable接口无论实现哪一个都不是好主意。若非要实现,则因为clone和readObject方法在行为上非常类似于构造器,所以在这些方法中也不能调用可覆盖的方法。具体的实现手段在第11条和第74条。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值