《Effective Java》第4章 类和接口

来源:互联网 发布:激战2战场实时数据 编辑:程序博客网 时间:2024/06/05 02:55

类和接口是Java程序设计语言的核心,那么类和接口的设计上必然会有一些大师的指导性原则,能够指导我们设计出有用,健壮和灵活的类和接口。

  1. 使类和成员的可访问性最小化【Item 13】
    1) 设计良好的模块会隐藏所有的实现细节,把它的API和实现清晰的隔离开,模块之间只通过API进行通信,一个模块不需要知道其它模块的内部工作情况,这叫做信息隐藏,在类的角度来说就是保持好的封装性
    2)封装的优点:
    a.有效的解除组成系统的各模块之间的耦合关系,使得这些模块可以独立进行开发,测试,优化,使用,理解和优化。
    b.信息隐藏对于开发速度,可维护性,系统性能分模块剖析便利性的提升有很大的优势,可以保证在调整某个模块不影响其它模块
    c.提高了软件的可重用性,因为模块比较独立,如其它系统有需要可以直接挪过去复用
    d.信息隐藏降低了构建大型系统的风险,即使整个系统不可用,但是这些独立的模块却可能是有用的
    3)Java语言中关于支持封装性实现的机制
    a.访问控制机制决定了类、接口和成员的可访问性
    b.实例的可访问性由该实例所声明的位置,以及该实例声明中所出现的访问修饰符共同决定
    4)封装实现的一些注意点
    a.尽可能地使每个类或者成员不被外界访问
    复习下访问级别,详细的还得参考java语言定义:
    private:只在声明该成员的顶层类内部才可以访问这个成员
    package:缺省(default)访问级别,声明该成员的包内部任何类都可以访问这个成员
    protected:声明该成员的类的子类可以访问这个成员,并且该成员的包内部的任何类都可以访问这个成员
    public:在任何地方都可以访问该成员
    public和protected的成员将被作为API导出,一旦发布必须永久支持,慎重
    b.protected成员少用
    c.子类override超类的方法时,只能扩大访问范围,不能缩小,这样确保任何使用超类的实例的地方都可以使用子类的实例,保证继承is-a的特性
    d.实例域局不能是公有的,包含公有可变域的类并不是线程安全的,一旦把实例域设计为公有的,就失去了对该实例成员的控制权限,虽然你可能将该成员定义为final,但是只是引用不可变,里面的值可能被修改
    e.静态域也不建议作为公有的,常量除外,引用类型的实例域不建议作为公有,因为即使final显示引用不能修改,但是引用指向的对象是可能被修改的
    f.长度非零的数组总是可变的,因为数组也是引用类型,类具有公有的静态final数组域或者返回这种域的访问方法,这几乎总是错误的
    g.尽可能的降低可访问性,访问级别尽量从最低开始设计,如果非得升级访问权限需要有充分的原因
    h.防止把任何散乱的类、接口和成员变成API的一部分
  2. 在公有类中使用访问方法而非公有域【Item 14】
    1)公有类不应该直接暴露数据域,如果类可以在它所在的包的外部进行访问,就提供访问方法,以保留将来改变该类的内部表示法的灵活性
    2)如果类是包级私有的,或者是私有的嵌套类,直接暴露它的数据域并没有本质的错误,因为访问范围本身比较小,也不需要遵循API的延续不可更改性
  3. 使可变性最小化【Item 15】
    1)遵循如下规则使类成为不可变,不可变类只是其实例不能被修改的类,每个实例包含的信息必须在创建时提供,并在实例整个生命周期内固定不变
    a.不要提供任何会修改对象状态的方法
    b.保证类不会被扩展:可以用final修改类,也可以通过静态工厂方法来做,private/protected修改构造器,通过静态工厂方法获取实例,因为不可能把来自另一个包的类,缺少公有或者受保护的构造器的类进行扩展
    c.使所有的域都成为私有的,不提供任何修改途径
    e.确保对于任何可变组件的互斥访问,如果类有指向可变对象的域,确保客户端无法获得指向这些对象的引用,并且永远不要用客户端提供的对象引用来初始化这样的域,也不要从任何访问方法中返回该对象的引用,一旦是公有引用,哪怕是final修改的引用域,因为final只能保证引用指向的对象不变,但是对象的值控制不了,引用指向的对象的值就不可控了。
    2)不可变对象的优势
    a.不可变对象本质上是线程安全的,它们不要求同步,不可变对象可以被自由的共享
    b.不仅可以共享不可变对象,甚至也可以共享它们的内部信息
    e.不可变对象为其它对象(可变或者不可变)提供了大量的构件
    3)不可变类的唯一缺点是,存在潜在的性能问题,对于每个不同的值都需要一个单独的对象,创建这种对象的代价可能很高,特别是对于大型对象的情形。
    针对此缺点,
    a.可以通过预测客户端将要在不可变类上执行哪些复杂的多阶段操作,如果可以预测出来,那么使用包级私有的可变配套类就可以避免多次对象的创建,而如果无法预测,那么可以通过提供公有可变配套类来进行可变部分的处理,例如String的可变配套类StringBuilder
    b.可以考虑将一些下的值对象的类设计为不可变
    c.也可以考虑将一些较大的值对象做成不可变的
    d.总之当你确认有必要实现令人满意的性能时,才应该为不可变的类提供公有的可变配套类
    4)其它注意事项
    a.不可变类,可以通过每次操作返回一个新创建的对象的方式(函数方式)实现,这样做的缺点是创建的实例会越来越多,而通过静态工厂方法则可以规避该缺陷,创建,从而降低内存占用和垃圾回收的成本
    b.不需要也不应该为不可变的类提供clone方法或者拷贝构造器
    c.坚决不要为每个get方法编写一个相应的set方法,除非有很好的理由让类成为可变类,否则就应该是不可变的
    d.如果类不能被做成是不可变的,仍然应该尽可能的限制它的可变性,降低对象可以存在的状态数,可以更加容易的分析该对象的行为,并且降低出错的可能性。
    e.除非有令人信服的理由要使域变成非final的,否则要使每个域都是final的
    f.构造器应该创建完全初始化的对象,并建立起约束条件,不能在构造器以及静态工厂方法之外提供初始化方法,或者重新初始化方法
  4. 复合优先于继承(针对类非接口)【Item 16】
    1)继承提高了代码的复用性,同时也破坏了封装性,子类和父类耦合在一起,父类的改动直接影响子类,因此在如下两种场景下使用继承是安全的:
    a.在包的内部使用继承
    b.对于专门为继承而设计,并且具有很好的文档说明的类来说
    2)子类在复写父类的方法时,需要遵守父类设计该方法的约定,因为在父类中可能会根据该方法设计的约定进行自用,如果子类破坏了该约定,相关功能方法就会运行不正确,出现出乎意料的结果
    3)那么如果不是非得使用继承,可以通过复合(组合)来达到代码的复用,复合是指不扩展现有的类,而是在新的类中增加一个私有域,它引用现有类的一个实例
    4)包装类(Wrapper)是复合的一种典型的用法,包装类不需要像类继承那样,子类必须对于父类中的每一个构造器都要求有一个单独的构造器与之对应,包装类可以用来包装被包装的类的任何实现,并结合任何先前存在的构造器一起工作,这也是Decorator模式的实现方式
    5)包装类不适合用在回调框架中,因为在回调框架中,对象把自身的引用传递给其它对象,用于后续的调用(回调),因为被包装起来的对象并不知道它外面的包装对象,因此就它传递一个自身的引用(this),那么回调时就避开了外面的包装对象。
    6)只有当子类和父类真正存在一种is-a的关系,才适合使用继承
    7)如果类之间是has-a的关系,一定要使用复合
    8)在适合使用复合的地方使用的继承,则会不必要的暴露实现细节,这样得到的API会把你限制在原始的实现上,永远限定了类的性能,更严重的是暴露了内部细节,客户端就有可能直接访问这些内部细节,这样至少会导致语义上的混淆
    9)继承机制会把超类API中的所有缺陷传播到子类中,而复合则允许设计新的API来隐藏这些缺陷
    10)如果超类和子类不同包,而超类并非为继承而设计,那么继承就会导致脆弱性,超类的修改并不会顾忌子类,子类的行为就不确定了
  5. 要么为继承而设计,并提供文档说明,要么就禁止继承【Item 17】
    1)如果类为继承而设计,那么必须对可覆盖的方法进行详细的说明,尤其是可被覆盖的自用方法,类必须有文档说明它可覆盖的方法的自用性,也就说该类的内部怎么用的该方法,以便子类不未被超类的设计初衷,或者取消自用方法的可覆盖性
    2)好的API文档应该描述一个给定的方法做了什么,而不是描述它是如何做到的,但是对于自用可被覆盖的方法就应该说明如何做到的,确保其被安全的子类化
    3)为继承而设计的类除了添加合适的文档说明外,也可以为程序员编写更加有效的子类,避免子类化的危险。这种有效的子类必须通过某种形式提供适当的钩子(hook),以便能够进入到它的内部工作流程中,这种形式可以是精心选择的受保护的(protected)的方法,也可以是受保护的域。
    4)对于为继承设计的类,唯一的测试方式就是编写子类
    a.如果遗漏了关键的受保护成员,尝试编写子类就会使遗漏所带来的痛苦更加明显
    b.如果编写了多个子类,并且无一使用受保护的成员,或许就应该将其访问级别降为私有的
    5)为了版本延续性,一旦API公开,类就必须保持public与protected的域,成员等做了永久的承诺,所以必须在发布前做好充分的验证,因为一旦发布,你的API就遍布各个地方,后续不能随意修改了
    6)为继承而设计的类还应该遵守构造器决不能调用可被覆盖的方法,无论是直接调用还是间接调用。因为超类的构造器在子类构造器之前调用,这个时候是调用不到被子类覆盖的方法的,这个时候子类还未初始化,可能出现final域出来两个值的情况
    7)为继承而设计的类不建议实现Cloneable和Serializable接口,因为这样会把实质性的负担转给了扩展这个类的程序员身上,clone和readObjetc两个方法在行为上非常类似于构造器,所以无论clone还是readObject方法都不可以调用可被覆盖的方法,不管直接还是间接的
    8)消除可覆盖方法的自用性,可以通过将该方法的代码移到一个私有的“辅助方法”中,并且每个可覆盖方法调用它的私有辅助方法,然后,用“直接调用可覆盖方法的私有辅助方法”来代替“可覆盖方法的每个自用调用”,从而避免了可覆盖方法在超类中的自用,提升子类化的安全性
    9)那么如果不是为继承儿设计的类,就应该禁止子类化
    a.把类声明为final
    b.把所有的构造器变成私有的,或者包级私有的,并增加一些公有的静态工厂方法来替代构造器
  6. 接口优于抽象类【Item 18】
    1)接口的优势
    a.现有的类可以很容易被更新,以实现新的接口,通过implements接口就可以啦
    b.接口就是定义mixin(混合类型)的理想选择,可以模拟实现多继承
    c.接口允许我们构造非层次结构的类型框架,就一个类的多面性,如 一个人可以是软件工程师,也可以是硬件工程师
    2)接口使得安全的增加类的功能成为可能,避免了继承抽象类代码了限制
    3)接口不允许有具体的实现,为了降低使用接口程序员的工作量,可以为接口提供一个抽象的骨架实现类,把接口和抽象类的优点结合起来,接口的作用是定义类型,而骨架实现类接管了所有与接口实现相关的工作
    。如Collections Framework为每个重要集合接口都提供了一个骨架实现,包括AbstractCollection,AbstractSet,AbstractList和AbstractMap
    4)骨架实现的美妙之处在于它们为抽象类提供了实现上的帮助,但有不强加“抽象类被用作类型定义时”所持有的严格限制。
    5)抽象类可增加具体方法和域,而接口只能增加域,不能增加方法,因为接口中增加的方法都没有具体实现,一旦公开接口增加接口随意增加方法,所有implements的类都必须实现该方法,不然无法编译通过,所以在抽象类的演变比接口的演变要容易得多
    6)如上原因所述,接口一旦被公开发行,并且已被广泛实现,再想修改这个接口几乎是不可能的
    7)抽象类和接口的选择
    a.当演变容易性比灵活性和功能更为重要的时候,在这种情况下,应该使用抽象类来定义类型,但前提是必须理解并且可以接受这些局限性
    b.如果你导出了一个重要的接口,就应该坚决考虑同时提供骨架实现类
    c.应该尽可能谨慎地设计所有的公开接口,并通过编写多个实现来对它们进行全面的测试
  7. 接口只用于定义类型【Item 19】
    1)为了增加类型而设计接口
    2)常量接口模式是针对接口的不良使用,即接口仅有常量并无接口方法
    3)导出常量的几种合适的方式:
    a.如果这些常量与某个现有的类或者接口紧密相关,就应该把这些常量加到这些类和接口中,如JDK中的Integer和Double中的MAX_VALUE和MIN_VALUE
    b.如果常量最好被看作枚举类型的成员,那么就应该使用枚举类型
    c.如上都不合适,可以使用不可实例化的工具类来导出这些常量
  8. 类层次优于标签类【Item 20】
    1)标签类即是一个类中包含多个类型,增加所有类型的域以及方法,多个类混合在一起
    2)标签类过于冗长,容易出错,并且效率低下
    3)类层次的好处在于其反应了类型之间本质上的层次关系,有助于增强灵活性,并进行更好的编译时类型检查
  9. 用函数对象表示策略【Item 21】
    1)允许函数调用者通过传入第二个函数来指定自己的行为,java中通过传入策略对象决定行为,即某一个对象的方法执行其他对象上的操作,这种机制叫做策略模式
    2)在设计具体的策略类时,还需要定义一个策略接口,策略接口被用作所有具体策略实例的类型
    3)java中实现策略模式方式
    a.要声明一个接口来表示该策略,并且为每个具体策略生命一个实现了该接口的类
    b.当一个具体策略只被使用一次时,通常使用匿名类来声明和实例化这个具体策略类
    c.当一个具体策略是设计用来重复使用的时候,它的类通常就要被实现为私有静态成员类,并通过公有的静态final域被导出,且类型为该策略接口
  10. 优先考虑静态成员类【Item 22】
    1)嵌套类(nested class)是指被定义在一个类的内部的类,它的目的是为其外围类(enclosing class)提供服务
    2)嵌套类有静态成员类、非静态成员类、匿名类、局部类四种,其中静态成员类为非内部类,其无异于外围类,仅仅是位置位于外围类内部,即使没有外围类的实例,静态成员类仍可以被正常使用,其它三种被成为内部类
    3)静态成员类一种常见用法是作为公有的辅助类,仅当与它的外部类一起使用才有意义
    4)非静态成员类的每个实例都隐含着与外围类的一个外围实例相关联,没有外围类的实例的情况下,要想创建非静态成员类的实例是不可能的
    5)非静态成员类的一种常见用法是定义一个Adapter,它允许外部类的实例被看作是另一个不相关的类的实例
    6)如果声明成员类不要求访问外围类实例,就要始终把static修饰符放在它的声明中,使它成为静态成员类,而不是非静态成员类,如果缺省了static修饰,那么其嵌套类的每个实例都包含一个外围类的实例,保存这份引用需要时间和空间,并且导致外围实例在垃圾回收时得以保留
    7)私有静态成员类的一种常见用法是用来代表外围类所代表的组件
    8)当且仅当匿名类出现在非静态环境中时,它才有外围实例,但是即使它们出现在静态环境中,也不可能拥有任何静态成员
    9)匿名类常见用法
    a.动态的创建函数对象,也就是策略对象
    b.创建过程对象,比如runnable,Thread或者TimerTask等
    c.静态工厂方法的内部进行匿名自定义类对象的创建
    10)匿名类的限制
    a.除了在匿名类声明的时候,是无法将它们实例化的,你不能执行instanceof测试,或者做任何需要命名类的其它事情
    b.无法声明一个匿名类来实现多个接口,或者扩展一个类,并同时扩展类和实现接口
    c.匿名类的客户端无法调用任何成员,除了从它的超类中继承得到之外
    d.由于匿名类出现在表达式中,其必须保持简短
    11)在任何声明局部变量的地方都可以声明局部类,并且局部类也遵守同样的作用域规则
    12)局部类同其它三种嵌套类都有一些共同属性
    a.与成员类一样,局部类有名字,可以被重复地使用
    b.与匿名类一样,只有当局部类在非静态环境中定义的时候,才有外围实例,它们不能包含静态成员
    c.与匿名类一样,它必须非常简短,以便不会影响到可读性
    13)四种嵌套类的用法:
    a.如果一个嵌套类需要在单个方法之外仍然是可见的,或者它太长了,不适合于放在方法内部,就应该使用成员类
    b.如果嵌套类的每个实例都需要一个指向其外围类实例的引用,就要把成员类做成非静态的,否则做成静态的
    c.假设这个嵌套类属于一个方法内部,如果你只需要在一个地方创建该类的实例,并且已经有了一个预置类型可以说明这个类的特征,那么将其做成匿名类
    d.以上特征都没有,那么就可以将嵌套类设计成局部类
0 0
原创粉丝点击