Effective Java 阅读笔记——类和接口
《Effective Java中文版》第二版的读书笔记。
13:使类和成员的可访问性最小化
好处:降低系统模块之间(包之间、类之间)的耦合,使得模块可以独立测试开发,提高可重用性和易维护性
终极目标:尽可能的使每个类或者类的成员不被外接访问。
保护级别和公有的都是类导出的API的一部分,必须永远得到支持,应尽量少用。
注意:
- 子类中的访问级别不容许低于超类中的访问级别,以确保任何可以使用超类的地方都可以使用之类
- 如果一个类实现了某个接口,那么接口中所有的方法在这个类中必须申明为公有的,因为接口中所有的方法都隐含着公有的访问级别
- 除了包含静态的final的,指向基本类型或者不可变对象的引用的公有域之外,不能有任何公有域
14:在公有类中使用访问方法而非公有域
注意,是在公有类中,不应该暴露数据域,但是对于一些包级私有或者私有的嵌套类,合适的暴露数据域是合理的,而且更加清晰
15:使可变性最小化
目的:降低对象可能存在的状态数,可以更容易的分析该对象的行为,同时降低出错的可能性
不可变类简单、线程安全,可以被自由的共享,但是对于每一个值都需要一个单独的对象,
成为不可变类的要求:
- 不提供任何可以修改对象状态的方法:大多使用“函数的”做法,即不会修改对象内部状态的方法
- 保证类不会被扩展:成为final类或者只有私有构造函数且含有静态工厂方法
- 使所有的域都是final的
- 使所有的域都是私有的
- 确保对于任何可变组件的互斥访问:客户端不可访问可变组件或者只能拿到可变组件的拷贝
但是,为了性能上的需求,可以把要求放松为:没有一个方法能够对对象的状态产生外部可见的改变
16:复合优于继承
这里说的继承是指实现继承(一个类扩展另一个类),而不是接口继承
对于包内部的继承或者继承专门为了继承而设计并且有很好的文档的类,是安全的。
不推荐的是:跨越包的界限继承普通类,除非你确认子类确实是超类的子类型
跨包继承可能存在的风险可能有:
- 继承打破了封装性,子类的实现依赖于超类中特定功能的实现细节,但是超类可能在后续版本中获得更新,子类所要求的特定细节就无法保证
- 超类可能添加新的方法,而使得子类的某些实现依赖的特定条件被打破
- 超类中可能添加一个与子类中签名相同但返回值不同的方法,从而使得子类不可通过编译
复合:不扩展现有类,而是在新的类中增加一个引用现有类实例的引用。新类的每个实例都可以调用被包含的现有类实例中对应的方法,并返回它的结果,这样的方式叫做“转发”,新的类也叫做“包装类”,除了编码稍有点琐碎,包装类在性能与内存占用上都不会带来很大的影响,但是安全性有了很大提高。
17:要么为继承而设计,并提供文档说明,要么就禁止继承
好的API文档应该描述一个方法做了什么,而不是描述它是如何做到的。
编写为继承而设计的类,有如下建议:
- 编写子类进行测试,一般3个子类就足以测试
- 构造器决不能调用可被覆盖的方法,无论是间接还是直接调用:因为超类的构造器先于子类的构造器运行,如果在超类的构造器中调用了可覆盖的方法,那么子类中的覆盖方法就会在子类构造器被调用之前被超类构造器提前调用,如若该覆盖方法的执行依赖于子类构造器的某些初始化条件,则难以预期执行。
- 最好不要实现Cloneable和Serializable接口,因为会把一些实质性的负担转嫁给扩展这个类的程序员,若需如此:由于clone与readObject方法与构造器方法类似,所以他们都不可直接或间接调用可覆盖的方法;如果有readResolve或者writeReplace方法,那应该使他们成为受保护的方法,以避免被扩展该类的程序员忽略。
18:接口优于抽象类
接口优于抽象类的地方:接口可以多重继承,从而
- 现有的类可以很容易被更新,以实现新的接口
- 接口是定义混合类型的理想选择:比如可比较行为
- 利用接口的继承可以构造非层次结构的类型框架:比如同时继承歌唱家和作曲家
对于接口一般提供骨架实现类,即对接口部分实现的abstract类,程序员继承接口时可以考虑继承抽象类以降低工作量。
模拟多重继承:类似于C同时继承了D和B
1 interface B;
2 abstract class B implements A; 3 4 class C extends D{ 5 //... 6 class E extends B{ 7 } 8 }
抽象类较于接口的优势:演变更加方便,即如需添加新的方法,那么只需在抽象类中增加方法并给出默认的实现,那么所有继承他的子类也就可以提供这个方法,而对于接口则不行。
总之:接口通常是定义容许多个实现的最佳途径,除非演变的容易性比灵活性和功能更加重要的时候;若设计了重要的接口,则应坚决考虑提供骨架类
19:接口只用于定义类型
接口应该只被用来定义类型,而不应该被用来导出常量
错误用法:常量接口,即在接口中只定义一些静态final域
弊端:
- 类在内部使用某些常量,纯粹属于实现细节,而通过常量接口,则暴露了这些细节
- 易使用户迷惑
- 如果将来更新,不需要这些常量了,但也必须实现该常量接口
导出常量的几种正确做法:
- 如若常量与类或接口密切相关,就把常量写入该类或接口
- 使用枚举类型
- 使用不可实例化的工具类(Utility)
20:类层次优于标签类
标签类:即一些使用比如type域来区别某个实力的类型。比如使用使用一个标签来区分形状类的实例是圆形还是矩形
坏处:
- 代码可读性变差:会有很多样板代码,即在类中对不同的type会有不同的代码、type的判断以及枚举等。
- 增加内存占用:比如矩形实例中也会保存圆形type的东西。
- 不能定义final域:比如在矩形中,长宽域理应在final的,即应该在构造函数中初始化。但是在圆形中这两个域却又是无用的,但因为它是final又必须初始化,这样就需要借助对type的判断来分别初始化。
- 构造器不能借助编译器来设置标签域并初始化正确的域:无论你的type是矩形还是圆形,对于编译器来说都是属于同一类的实例,编译器才不知道你的type是啥呢
- 无法增加type:除非能修改源码,且增加type必须在判断type的所有地方都做相应修改
使用类层次来代替标签类:
- 把依赖type值的方法写成abstract方法放到抽象类中。
- 所有type类型都有的数据域放到上述抽象类中。
- 不依赖于type标签值的方法,则在上述抽象类中实现。
总之:标签类很少有适用的场合
21:用函数对象表示策略
即相当于对某个策略(用接口表示)提供不同的实现实现方式。
How:
- 声明一个接口表示该策略
- 当某个具体的策略只被适用一次时,通常适用匿名类来声明和实例化这个具体策略类
- 当某个具体的策略被设计用来重复使用时候,它的类通常被实现为某个类的私有静态成员类,并通过公有的静态final域导出,其类型为该策略的接口
22:优先考虑静态成员类
嵌套类有四种:静态成员类、非静态成员类、匿名类、局部类,其中前三种称为内部类
- 使用成员类:如果一个嵌套类需要在方法之外仍然是可见的,或者太长以至于不适合放在方法内部,就应该使用成员类
- 使用静态成员类:当成员类的每个实例不需要一个指向其外围实例的引用时
- 使用非静态成员类:当成员类的每个实例需要一个指向其外围实例的引用时
- 使用匿名类:当嵌套类属于一个方法的内部,且只需在一个地方创建实例且有一个预置类型可以说明这个类的特征(比如21条中的2),就使用匿名类
- 使用成员类:接4,否则使用成员类
匿名类和成员类一般少于十行
这里说的优先并不是很强的优先,只是静态成员类有如下优点:
- 相比与匿名类和成员类,可以在方法外的地方被访问到
- 相比与非静态成员类,由于不持有指向外围类实例的引用,所以能耗时更少,占用空间更少,或者导致某些内存泄露

浙公网安备 33010602011771号