Effective Java

第二章:创建和销毁对象

1. 使用静态工厂方法代替构造器

好处

  1. 有名称,可以根据不同的目的给静态工厂起不同的名称。

  2. 可以根据需要来决定是否需要创建对象。

  3. 可以返回原类型的任何子类型的对象。 这些子类型可以是非公有的,可以根据不同的情况返回不同的类型。 允许通过注册的方式增加新的类,返回新的类的对象。

  4. 可以利用类型推导,使代码更加简单。

    Map<String List<String>> m = HashMap.newInstance();
    

缺点

  1. 没有公有构造函数的类不能被继承
  2. 静态工厂方法和其他的方法没有特别的区分, 不能绝对地判断一个方法是不是静态工厂方法。

2. 使用构建器来处理多个可选参数

为了处理多个可选参数,有三种做法:

写多个构造器

不足之处在于调用这需要熟悉多个函数, 然后参数之间的顺序也不够灵活, 如果参数多了,调用起来不是很方便。

JavaBeans模式

先通过无参构造器来构建对象,然后调用setter来设置属性。 无法维持对象的一致性,很难做到线程安全。

Builder模式

先调用setter来构建builder,然后使用builder来构建对象。

builder模式可以很方便地实现抽象工厂。相比于newInstance更加类型安全。

builder模式的不足在于构建builder的开销。 所以只在参数较多的情况下才使用builder模式

3. 使用Enum来实现Singleton

这是最佳方法,相对于private构造函数, 不会有序列化的问题。 不过很少有单例能够序列化吧。

4. 通过private构造器是类不可实例化

通过不可实例化的类来把一些静态方法组织在一起。

如果类没有提供公有构造器,它也不能被继承。

如果类的构造器是private的,应该通过注释说明为什么这样做。

5. 避免创建不必要的对象

防止自动装箱时创建的大量对象。

但是有时需要创建一些对象来进行保护性拷贝,这是必要的。

6. 消除对过期对象的引用

以便于垃圾回收。如果自己管理内存,就需要担心内存泄漏问题。

注意使用Weak Ref来释放过期的引用。

可以使用性能分析工具来查看内存泄漏。

7. 避免使用终结方法

不要依赖于终结方法来释放资源,使用try...finally的形式。

提供一个显式的终止方法,要求类的客户端必须调用此方法。 此时,终结方法可以充当‘安全网’的作用,确保客户端调用了显示的终止方法。

如果调用了终结方法,记得调用 super.finalize

第三章:对于所有对象都通用的方法

8. 覆盖equals

编写好的 equals 函数:

  1. 使用==操作符检查是否为当前对象的引用。
  2. 使用instanceof操作符检查参数的类型。我的感觉是只匹配当前的类型, 不要试图实现两个不同类型的对象相等。这样没什么坏处,而且不会造成不一致。
  3. 把参数转化成正确的类型,传入的参数是object。
  4. 利用“关键域”来检查两个对象是否相等。

9. 覆盖equals时总要覆盖hashCode

这条没什么好说的,使用关键域来生成hashCode。

10. 始终覆盖toString

覆盖toString来使一个类的字符串表示更有意义。

11. 谨慎覆盖clone

覆盖clone时,要调用 super.clone 返回一个新的对象, 然后给需要拷贝的域进行拷贝,注意,这些域不能是final的。

另外一种实现对象拷贝的方式是提供一个拷贝构造器或静态工厂。 这样更加方便。

12. 实现Comparable接口

这是一个泛型接口,能够被泛型算法使用。 如果一个类实现了Comparable接口,如果要对它扩展新的域, 推荐的做法是使用组合和不是继承,这样能够满足对于Compare的对称性和传递性。

第四章:类和接口

13. 使类和成员的可访问性最小化

信息隐藏的目的在于可以使客户端不用关心这些东西, 一方面使得接口更加简单,容易使用。 另一方面,可以更改接口的实现, 而客户端不用做任何修改。

新的访问级别:包级私有,它是默认的访问级别。

受保护的成员属于导出API的一部分,它是在为继承而设计的类中存在的。

不要public出类的不可变域。虽然Java的setter和getter写起来有点麻烦。

14. 在公有类中使用访问方法而非公有域

如果类是包级公有的,或者是私有的嵌套类, 直接暴露它的数据域并没有本质的错误。

15. 使可变性最小化

使类不可变,要遵循下面五条原则:

  1. 不要提供任何会修改对象的方法
  2. 保证类不会被扩展
  3. 使所有的域都是final的
  4. 所有的域都是私有的
  5. 确保对于任何组件的互斥访问,利用保护性拷贝技术。

16. 复合优先于继承

对于继承,子类依赖于其超类中特定功能的实现细节。 特别是,如果子类覆盖了超类的某个方法, 而超类的某个方法调用了这个方法, 此时就应该特别小心。

只有当子类是超类的子类型时,才适合用继承。 确定两个类之间确实有“is-a”关系, 这个原则不太好判断啊,就拿这条的例子来说, 可以算作是“is-a”关系,可以还不适合继承。 可能这个地方的原因在于原来的类不是用来扩展的, 没有用文档注明细节,如果细节被文档标明了, 这些细节就不应该被修改了。

如果一个类的API有缺陷,应该尽量使用组合而不是继承, 这样会防止把这种缺陷扩展到客户端。

17. 要么为继承而设计,并提供文档说明,要么就禁止继承

类的文档必须精确地描述覆盖每个方法锁带来的影响。特别是可覆盖的方法。

好的API文档应该描述一个给定的方法做了什么工作, 而不是描述它如何做到的。对于继承类来说,这点需要修改。

对于为了继承而设计的类,唯一的测试方法就是编写子类。

构造器决不能调用可悲覆盖的方法。

无论是clone还是readObject,都不可以调用可覆盖的方法。

对于一个API的设计者,可以这么来设计,也很有道理, 导出的类只有两种,一种是专门用来继承的,一种是禁止继承的。 可是作为内部实现来说,又应该怎么做呢?

为了实现某个功能,应该会有一个类的层次, 通过这些类完成整个功能。 这时每一个类是否允许继承能够被确定吗? 而且,有些类是能被继承,但是只允许被当前包继承, 这应该怎么实现?把类声明成包级私有的? 现在主要的问题是看Java代码太少了,否则应该可以实现的。

至少对于后一种情况,文档说明的情况应该要改一下吧。

18. 接口优先于抽象类

接口比抽象类灵活。为了使用抽象类的优点, 可以实现一个骨架类作为一个接口的抽象类用于继承。

现在还不是很明白骨架类的用处在哪, 它的本质还是一个抽象类。 既然接口优于抽象类, 那就直接使用接口就好了, 还需要骨架类干嘛呢?

抽象类相对于接口,演变相对容易一些(可以增加新的方法), 如果演变的容易性比灵活和功能更为重要的时候可以考虑使用抽象类。

19. 接口只用于定义类型

不应该使用接口来导出常量, 直接使用一个不能初始化的类来导出就可以了。

20. 类层次优先于标签类

类层次相对于标签类来说更加清晰,灵活。

21. 用函数对象表示策略

通过策略接口来模拟函数指针,把一个具体的函数作为参数。 具体的策略实现类可以不公开,而只要提供一个这个类的对象在宿主类中, 通过公有的静态final域被导出。

22. 优先考虑静态成员类

静态成员类相对于普通成员类不需要引用外围对象,能够减少开销。 如果一个成员类不需要访问外围类, 那么就把它声明为静态成员类。

匿名类只有当它出现在非静态的环境中时,才有外围实例。

第五章:泛型

23. 尽量使用泛型

使用泛型机制能够很好地利用类型检查,能够尽快地暴露程序的错误。

如果不确定具体元素的类型,可以使用类型通配符, 而不要使用原生态类型。

注意 Set<Object>Set<?> 的区别。 Set<Object> 可以包含任何对象的一个集合, Set<?> 可以包含某未知类型对象的一个集合。

24. 消除unchecked warnings

Unchecked warnings 能够指明程序中的类型不匹配, 除非足够确认, 不要消除unchecked warnings。 如果消除了unchecked warnings, 需要添加注释表明为什么要这样做是安全的。

25. 列表优先于数组

列表能够使用泛型的特性,可以依靠类型检查来做到类型安全。

在泛型类或者泛型函数的实现中,使用列表更数组更好。 但是列表操作起来有点麻烦,远不及数组方便, 这应该算是Java语言的一个不好的地方吧。

26. 优先考虑泛型

使用泛型能够更加类型安全,但是泛型里如果要使用数组也是可以的 但是需要手动进行类型转换,同时SuppressWarnings. 这就是Java泛型系统本身的局限性, 不知道以后的版本中会不会对它进行修改。

27. 优先考虑泛型方法

泛型方法能够自动的进行类型推导, 这与泛型类不一样, 所以对于调用着来说很方便。

28. 利用有限制通配符来提升API的灵活性

这样就可以把子类型的变量放入容器了, 更加灵活。

PECS: producer-extends, consumer-super.

如果类型推导无法正确地推导出类型, 可以显式地设置参数的类型。

29. 优先考虑类型安全的异构容器

对于异构容器,可以在它的函数中传入类型参数, 其他的参数可以利用这个参数, 这样能够保证类型安全。

public class Favorites {
    public <T> void putFavorite(Class<T> type, T instane);
    public <T> T getFavorite(Class<T> type);
}

第六章:枚举和注解

30. 用enum组织int常量

一个好处在于可以遍历,或者获取整个组的大小。

枚举的本质是int值,所以不会有性能问题。

可以给枚举类型定义域和方法,可以实现任意的接口。 与普通类的区别在于这个类只能导出特定的instance.

可以给每个instance定义同一个函数的不同实现。

可以使用枚举策略来给将一组函数组织在一起, 不用对于每个instance定义一个实现, 而且,可以防止对于新增instance而漏写case的问题。

31. 用实例域代替序数

不要依赖于 ordinal() 这个方法, 如果需要给每个实例一个序数,通过一个实例域来模拟。 对于C++,有一个默认的实现。

32. 用EnumSet代替位域

在C语言中,经常看到利用位操作来组织一些flag, 这样可以直接用与和或操作来提取、添加flag。 在Java中,推荐使用EnumSet来完成这样的功能。

33. 用EnumMap代替序数索引

通过EnumMap来把data和enum关联起来, EnumMap的构造器需要传入一个Class对象, 感觉有点重复了啊,因为前面泛型的类型参数指定了Enum的类型, 不知道为什么要这样设计。

34. 用接口模拟可伸缩的枚举

感觉这点不是很重要,客户端很少需要去扩展一个枚举类吧。

35. 注解优先于命名模式

利用注解,能够利用编译器的检查尽早发现错误, 而且可以传递注解参数,比命名模式方便很多。

37. 坚持使用Override注解

使用 @Override 注解,能够利用编译器的检查防止不必要的错误。

38. 标记接口

待续,没怎么看明白。

第七章:方法

38. 检查参数的有效性

对于有些参数,方法本身没有用到,却被保存起来供以后使用, 检查这类参数的有效性显得尤为重要。

如果检查有效性的代价特别昂贵, 或者需要等方法的运行才能检查方法的有效性, 这时可以不用检查参数的有效性, 但需要注意必要的时候使用异常转译。

39. 必要的时候进行保护性拷贝

保护性拷贝在构造函数(传入数据)时和返回对象的域(传出数据)时。 对于传入数据,保护性拷贝在检查参数的有效性之前进行的。

只要用可能,都使用不可变对象作为对象的组件(域)。这一点比较关键。

如果信任客户端不会不适当地修改组件, 就可以不用进行保护性拷贝来增加性能。

40. 谨慎设计方法的签名

避免过长的参数列表,可以把一个方法分解成多个方法, 或者通过类类组织相关参数,或者使用Builder模式。

对于参数类型,使用接口而不是类。

41. 慎用重载

注意重载不要引起冲突, 防止混淆。重载方法的选择是在编译是决定的。 而对于被覆盖方法的选择是在运行时决定的。

42. 慎用可变参数

慎用可变参数,尤其是类型是Object类型,或者是T类型。

::
ReturnType1 suspect1(Object...args) {} <T> Returntype2 suspect2(T...args) {}

43. 返回零长度的数组或者集合,而不是null

这样客户端就不需要专门为了null而进行一次处理。

至于性能问题,因为零长度的数组都是不可变的, 所以可以返回同一个对象,避免重复新建数组的开销。

44. 为所有导出的API元素编写文档注释

How to write Doc Comments 这个规范.

第八章:通用程序设计

45. 将局部变量的作用域最小化

每个局部变量的声明都应该包含一个初始化表达式, 必须使用try来初始化的变量例外。

for 循环优先于 while 循环。

::
for (int i = 0, n = expensiveComputator(); i < n; ++i) {
doSomething(i);

} // 避免每次迭代都计算n

方法小而集中。

46. for-each循环优先于for循环

使用for-each循环,代码更加简短,也不容易出错。

47. 使用类库

不要重复造轮子。

48. 如果需要精确的答案,请避免使用float和double

如果需要精确的答案,使用BigDecimal。

49. 基本类型优先于装箱基本类型

不要使用使用装箱类型来进行 == 比较。

50. 不要使用字符串来组织数据

使用其他的类来组织数据。

51. 当心字符串连接的性能

如果需要把很多的字符串连接到一起, 使用StringBuilder更好, 能够减少字符串拷贝。

52. 使用接口引用对象

好处在于可以把对象改成其他实现了该接口的类型的对象, 而且保证不会依赖于具体的对象的其他方法。

53. 接口优先于反射机制

接口是类型安全的, 代码更简短,性能更好。

54. 谨慎地使用本地方法

尽量不要使用本地方法。

55. 谨慎地进行优化

努力编写好的程序,性能会随之而来。

56. 遵守普遍接受的命名惯例

如果命名是一门学问,Java中有一些惯用法。

第九章:异常

57. 只针对异常情况才使用异常

JVM没有对异常进行优化,异常控制的性能没有正常代码的性能好。

为了获知对象的状态,才能执行相应的操作, 有两种做法:

  1. 提供状态测试方法
  2. 提供一个可识别的返回值

如果状态测试和状态相关方法是重复同样的工作, 出于性能考虑,使用返回值方法。 否则使用状态测试方法, 因为它更好理解。

58-65. 如何使用异常

只在可恢复的情况下使用受检异常。

对于受检异常,客户端catch住受检异常,然后执行恢复操作, 实现这注意发生异常时的回滚操作,使失败保持原子性。

对于非受检异常,实现者注释异常,异常的原因, 同时实现异常转译,异常中带有具体信息。 客户端根据异常的原因修改代码, 如果忽略异常,解释为什么要这样做。

第十章:并发

待续。

第十一章:序列化

要将一个类的对象序列化, 需要实现 readObjectwriteObject 两个函数, 同时必要的时候需要实现 readResolve 这个方法。 readObject 注意实时保护性的拷贝, 对参数进行检查。 使用序列化代理来实现这种可能更加简单, 实现 writeReplace 这个方法就可以了。

Table Of Contents

Previous topic

Java基础语法

Next topic

Mining of Massive Data Sets

This Page