Effective Java
1. 考虑用静态工厂方法代替构造器
优点:
- 拥有名称,更容易识别
- 不必每次调用时,都创建一个新的对象
- 可以返回返回类型的任何子类型对象
- 使代码更简洁
缺点:
- 类如果不含共有的或者受保护的构造器,就不能被子类化。
- 它们与其他的静态方法实际上没有任何区别。
应用:
- 服务提供者框架
2. 遇到多个构造器参数时要考虑用构建器
- 当有很多参数的时候,重叠构造器模式可行,客户端代码会很难编写,并且仍然较难以阅读
- 使用javabeans模式,在构造过程中JavaBean可能处于不一定的状态
- 使用Builder 模式的客户端代码将更易于阅读与编写,构建器也比JavaBeans更加安全。
3. 用私有构造器或者枚举类型强化Singleton属性
- 使类成为Singleton会是它的客户端测试变得十分困难,因为无法给Singleton替换模拟实现,除非它实现一个充当其类型的接口。
- 序列化Singleton类使用implement Serializable是不够的,为了维护并保证Singleton,必须声明所有实例域都是瞬时(transient)的,并提供一个readResolve方法,否则每次反序列化一个序列化的实例时,都会创建一个新的实例。
单元素的枚举类型已经成为Singleton的最佳方法
例子:public enum Elvis { INSTANCE; public void leaveTheBuilding(){}; }
4. 通过私有构造器强化不可实例化的能力
导致一个类不能被子类化。所有的构造器都必须显示或隐式的调用超类的构造器。如此一来子类就没有可访问的超类构造器可用了
5. 避免创建不必要的对象
- 使用静态工厂方法而不是构造器,以避免创建不必要的对象
- 要优先使用基本类型而不是装箱基本类型,要当心无意识的自动装箱。
6. 消除过期的对象引用
- 只要类是自己管理内存,就应该警惕内存泄露问题
- 内存泄露的另一个常见来源是缓存
- 内存泄露的第三个常见来源是监听器和其他回调
7. 避免使用终结方法
- 不应该依赖终结方法来更新重要的持久状态
- 使用终结方法有一个非常严重的性能损失
- 如果终结方法发现资源还未被终止,则应该在日志中记录一条警告
- 如果类有终结方法,并且子类覆盖了终结方法,子类的终结方法就必须手工调用超类的终结方法,应该在try块中终结子类,并在相应的finally块中调用超类的终结方法
8. 覆盖equals时请遵守通用约定
- 类的每个实例本质上都是唯一的
- 不关心类是否提供了”逻辑相等”的测试功能
- 超类已经覆盖了equals,从超类继承过来的行为对于子类也是合适的
- 类是私有的或是包级私有的,可以确定它的equals方法永远不会被调用
equals方法实现了等价关系
- 自反性。对于任何非null的引用值x,x.equals(x)必须返回true
- 对称性。对于任何非null的引用值x和y,当且仅当y.equals(x)返回true时,x.equals(y)必须返回true.
- 传递性。对于任何非null的引用值x、y和z,如果x.equals(y)返回true,并且y.equals(z)也返回true,那么x.equals(z)也必须返回true。
- 一致性。对于任何非null的引用值x和y,只要equals的比较操作在对象中所用的信息没有被修改,多次调用x.equals(y)就会一致地返回true,或者一致的返回false。
- 非空性。对于任何非null的引用值x,x.equals(null)必须返回false。
- 无法在扩展可实例化的类的同时,即增加新的组件,同时又保留equals约定
实现高质量equals方法的诀窍
- 使用==操作符检查”参数是否为这个对象的引用”
- 使用instanceof操作符检查”参数是否为正确的类型”
- 把参数转换成正确的类型
- 对于该类中的每个”关键””域,检查参数中的域是否与该对象中对应的域相匹配
- 当编写完equals方法之后,应该问自己三个问题,它是否是对称的,传递的,一致的?
告诫:
- 覆盖equals时总要覆盖hashCode。
- 不要企图让equals方法过于智能
- 不要将equals声明中的Object对象替换为其他的类型
9. 覆盖equals时总要覆盖hashCode
- 相等的对象必须具有相等的散列码
- 不要试图从散列码计算中排除掉一个对象的关键部分来提高性能
10. 始终覆盖toString
- toString()方法应该返回对象中包含的所有值得关注的信息
11. 谨慎的覆盖clone
- 永远不要让客户去做类库能够替客户完成的事情
- 必须确保它不会伤害到原始的对象,并确保正确地创建被克隆对象中的约束条件
12. 考虑实现Comparable接口
13. 使类和成员的可访问性最小化
- 尽可能地使每个类或者成员不被外界访问
14. 在公有类中使用访问方法而非公有域
- 如果类可以在它所在的包的外部进行访问,就提供访问方法。
15. 使可变性最小化
使类成为不可变,要遵循下面五条规则
- 不要提供改变对象属性的方法
- 保证类不会被扩展
- 使所有的域都是final的
- 使所有的域都成为私有的
- 确保对于任何可变组件的互斥访问
优点
- 不可变对象比较简单
- 不可变对象本质上是线程安全的,它们不要求同步
- 不可变对象可以被自由地共享
- 不仅可以共享不可变对象,甚至也可以共享它们的内部信息。
- 不可变对象为其他对象提供了大量的构件
缺点
- 对于每个不同的值都需要一个单独的对象、
16. 复合优先于继承
17. 要么为继承而设计,并提供文档说明,要么就禁止继承
- 构造器决不能调用可被覆盖的方法
18. 接口优于抽象类
- 现有的类可以很容易被更新,以实现新的接口
- 接口是定义混合类型的理想选择
- 接口允许我们构造非层次结构的类型框架
- 接口使得安全的增强类的功能成为可能
- 抽象类的演变比接口的演变要容易的多
- 接口一旦被公开发行,并且已被广泛实现,再想改变这个接口几乎是不可能的
19. 接口只用于定义类型
20. 类层次优于标签类
- 标签类过于冗长、容易出错、并且效率低下
21. 用函数对象表示策略
22. 优先考虑静态成员类
23. 请不要在新代码中使用原生态类型
- 如果使用原生态类型。就失掉了泛型在安全性和表述性方面的所有优势
- 如果使用像List这样的原生态类型,就会失掉类型安全性,但是如果使用像
List
24. 消除非受检警告
- 要尽可能地消除每一个非受检警告
- 如果无法消除警告,同时可以证明引起警告的代码是类型安全的,(只有在这种情况下才)可以用一个人@SuppressWarnings(“unchecked”)注解来禁止这条警告
- 应该始终在尽可能小的范围中使用@SuppressWarnings注解
- 每当使用SuppressWarnings(“unchecked”)注解时,都要添加一条注释,说明为什么这么做是安全的
25. 列表优先于数组
- 数组是协变的(convariant)。相反泛型则是不可变的(invariant)。即如果Sub为Super的子类型,那么数组类型Sub[]就是Super[]的子类型;
- 数组是具体化的(reified)。因此数组会在运行时才知道并检查他们的元素类型约束。泛型是通过擦除来实现的。因此泛型只在编译时强化他们的类型信息,并在运行时丢弃(或者擦除)他们元素的类型信息。
- 创建泛型数组是非法的:
26. 优先考虑泛型
- 使用泛型比使用需要在客户端代码中进行转换的类型来得更加安全,也更加容易。
- 再设计新类型的时候,要确保他们不需要这种转换就可以使用
27. 优先考虑泛型方法
28. 利用有限制通配符来提升API的灵活性
- 为了获得最大限度的灵活性,要在表示生产者或者消费者的输入参数上使用通配符类型。
- 如果参数化类型表示一个T生产者,就使用《? extends T>,如果它表示一个T消费者,就使用<? super T>
- 不要使用通配符类型作为返回类型
29. 优先考虑类型安全的异构容器
30. 用enum代替int常量
- 枚举提供了编译时的类型安全。
31. 用实例域代替序数
- 永远不要根据枚举的序数导出与它关联的值,而是要将它保存在一个实例域中
32. 用EnumSet代替位域
33. 用EnumMap代替序数索引
34. 用接口模拟可伸缩的枚举
35. 注解优先于命名模式
命名模式的缺点:
- 无法处理命名失误的情况。
- 无法确保它们只用于响应的程序元素上
- 他们没有提供将参数值与程序元素关联起来的好方法。
36. 坚持使用Override注解
37. 用标记接口定义类型
- 标记接口定义的类型是由被标记类的实例实现的;标记注解则没有定义这样的类型
- 使用标记接口的方法能够更加精确的对实现它的类型进行锁定。
- 标记注解胜过标记结构的最大优点在于,他可以通过默认的方式添加一个或多个注解类型元素,给已被使用的注解类型添加更多的信息
38. 检查参数的有效性
39. 必要时进行保护性拷贝
- 假设类的客户端会尽其所能来破坏这个类的约束条件,因此你必须保护性地设计程序
- 保护性拷贝是在检查参数的有效性之前进行的,并且有效性检查是针对拷贝之后的对象,而不是针对原始的对象
- 对于参数类型可以被不信任方子类化的参数,请不要使用clone方法进行保护性拷贝
40. 谨慎设计方法签名
- 谨慎的选择方法的名称
- 不要过于追求提供便利的方法
- 避免过长的参数列表
- 分解成多个方法
- 创建辅助类
- 对于参数类型,要优先使用接口而不是类
- 对于boolean参数,要优先使用两个元素的枚举类型
41. 慎用重载
- 永远不要导出两个具有相同参数数目的重载方法。
- 必须要保证当传递同样当参数时,所有的重载方法的行为必须一致。
42. 慎用可变参数
- 不必改造具有final数组参数的每个方法,只当确定是在数量不定的值上执行调用时才使用可变参数
43. 返回零长度的数组或者集合,而不是null
44. 为所有导出的API元素编写文档注释
45. 将局部变量的作用域最小化
- 要使用局部变量的作用域最小化,最有力的方法就是在第一次使用它的地方声明
- 几乎每个局部变量的声明都应该包含一个初始化表达式
46. for-each循环优先于传统的for循环
三种常见的情况无法使用for-each循环
- 过滤–如果需要遍历集合,并删除选定的元素,就需要使用显示的迭代器,以便可以调用它的remove方法。
- 转换–如果需要遍历列表或者数组,并取代它部分或者全部的元素值,就需要列表迭代器或者数组索引,以便设定元素的值
- 平行迭代–如果需要并行地遍历多个集合,就需要显示地控制迭代器或者索引变量,以便所有迭代器或者索引变量都可以得到同步前移
47. 了解和使用类库
48. 如果需要精确的答案,请避免使用float和double
49. 基本类型优先于装箱基本类型
- 对装箱基本类型运行==操作符几乎总是错误的
- 当在一项操作中混合使用基本类型和装箱基本类型时,装箱基本类型就会自动拆箱
- 自动装箱减少了使用装箱基本类型的繁琐性,但是并没有减少它的风险
基本类型和装箱基本类型有三个主要区别:
- 基本类型只有值,装箱基本类型则具有与它们的值不同的同一性。
- 基本类型只有功能完备的值,而每个装箱基本类型除了它对应基本类型的所有功能值之外,还有个非功能值null
- 基本类型通常比装箱基本类型更节省时间和空间。
什么时候应该使用装箱基本类型:
- 作为集合中的元素,键和值
- 在参数化类型中,必须使用装箱基本类型作为类型参数
- 反射的方法调用时,必须使用装箱基本类型
50. 如果其他类型更适合,则尽量避免使用字符串
- 字符串不适合代替其他的值类型
- 字符串不适合代替枚举类型
- 字符串不适合代替聚集类型
- 字符串不适合代替能力表
51. 当心字符串连接的性能
- 为连接n个字符串而重复第使用字符串连接操作符,需要n的平方级的时间。
52. 通过接口引用对象
- 如果有合适的接口类型存在,那么对于参数、返回值、变量和域来说,就都应该使用接口类型进行声明
53. 接口优先于反射机制
反射机制的缺点:
- 丧失了编译时类型检查的好处
- 执行反射访问所需要的代码非常笨拙与冗长
- 性能损失
54. 谨慎地使用本地方法
- 使用本地方法来提高性能的做法不值得提倡
本地方法的三种用途:
- 它们提供了“访问特定于平台的机制”的能力
- 提供了访问遗留代码库的能力,从而访问遗留数据。
- 可以通过本地语言,编写应用程序中注重性能的部分,以提高系统的性能。
55. 谨慎地进行优化
- 要努力编写好的程序而不是快的程序
- 努力避免那些限制性能的设计决策
- 要考虑API设计决策的性能后果
- 在每次试图做优化之前和之后,要对性能进行测量
56. 遵守普遍接受的命名惯例
57. 只针对异常的情况才使用异常
58. 对可恢复的情况使用受检异常,对编程错误使用运行时异常
- 如果期望调用者能够适当地恢复,对于这种情况就应该使用受检的异常
- 用运行时异常来表面编程错误
- 实现的所有未受检的抛出结构都应该是RuntimeExecption的子类
59. 避免不必要地使用受检的异常
60. 优先使用标准的异常
61. 抛出与抽象相对应的异常
- 更高层的实现应该捕获低层的异常,同时抛出可以按照高层抽象进行解释的异常,这种做法称为异常转译
- 尽管异常转译与不加选择的从低层传递异常的做法相比有所改进,但是它也不能被滥用
62. 每个方法抛出的异常都要有文档
- 始终要单独地声明受检的异常,并且利用Javadoc的@throws标记,准确地记录下抛出每个异常的条件
63. 在细节消息中包含能捕获失败的而信息
- 异常的toString方法应该尽可能多地返回有关失败原因的信息
- 为了捕获失败,异常的细节消息应该包含所有“对该异常有贡献”的参数和域的值
64. 努力使失败保持原子性
65. 不要忽略异常
- 空的catch块会使异常达不到应有的目的,至少应当包含一条说明,解释为什么可以忽略这个异常
66. 同步访问共享的可变数据
- 为了在线程之间进行可靠的通信,也为了互斥访问,同步是必要的
- 如果读和写操作没有都被同步,同步就不会起作用
- 将可变数据限制在单个线程中
67. 避免过度同步
68. executor和task优先于线程
69. 并发工具优先于wait和notify
70. 线程安全性的文档化
71. 慎用延迟初始化
72. 不要依赖于线程调度器
73. 避免使用线程组
74. 谨慎地实现Serializable接口
- 为了继承而设计的类应该尽可能少的实现Serializable接口,用户的接口也应该尽可能少地继承Serializable接口
- 对于为继承而设计的不可序列化的类,你应该考虑提供一个无参构造器
序列化的缺点:
- 一旦一个类被发布,就大大降低了”改变这个类的实现”的灵活性
- 它增加了出现Bug和安全漏洞的可能性
- 随着类发行新的版本,相关的测试负担也增加了
75. 考虑使用自定义的序列化形式
76. 保护性地编写readObject方法
77. 对于实例控制,枚举类型优先于readResolve
78. 考虑用序列化代理代替序列化实例