Effective.Java第67-77条(异常相关)
67. 明智审慎地进行优化
有三条优化的格言是每个人都应该知道的:
(1)比起其他任何单一的原因(包括盲目的愚钝),很多计算上的过失都被归咎于效率(不一定能实现)
(2)不要去计算效率上的一些小小的得失,在97%的情况下,不成熟的优化才是一切问题的根源。
(3)在优化方面,我们要遵守两条规则:
规则一:不要进行优化
规则二(仅针对专家):还是不要进行优化,也就是说,在你还没有绝对清晰的优化方案之前,请不要进行优化。
努力编写好的程序,而不是快的程序;速度自然会提高。但是在设计系统时一定要考虑性能,特别是在设计API、线路层协议和持久数据格式时。当你完成了系统的构建之后度量性能,如果足够快,就完成了;如果没有利用分析器找到问题并进行相关部分优化。
68. 遵守被广泛认可的命名约定
Java有一组完善的命名约定,不严格地讲,分为两类:排版和语法。
排版约定如下:
任何在你的组织外部使用的包,名称都应该以你的组织的 Internet 域名开头,并将组件颠倒过来,例如,edu.cmu、com.google、org.ew。以 java 和 javax 开头的标准库和可选包是这个规则的例外。用户不能创建名称以 java 或 javax 开头的包或模块。
解释一下TypeParameter,这个是类型参数,通常由单个字母组成。最常见的是五种:T表示任意类型、E表示集合的元素类型、K和V表示Map的键值、X表示异常。函数的返回类型通常为R。任意类型的序列可以是T、U、V或T1、T2、T3。
阿里规约如下:
1. 【强制】 代码中的命名均不能以下划线或美元符号开始,也不能以下划线或美元符号结束。
2. 【强制】 代码中的命名严禁使用拼音与英文混合的方式,更不允许直接使用中文的方式。
正例: alibaba / taobao / youku / hangzhou 等国际通用的名称, 可视同英文。
3. 【强制】类名使用 UpperCamelCase 风格,必须遵从驼峰形式,但以下情形例外: DO / BO /DTO / VO / AO
4. 【强制】方法名、参数名、成员变量、局部变量都统一使用 lowerCamelCase 风格,必须遵从驼峰形式。
5. 【强制】常量命名全部大写,单词间用下划线隔开,力求语义表达完整清楚,不要嫌名字长。
6. 【强制】抽象类命名使用 Abstract 或 Base 开头; 异常类命名使用 Exception 结尾; 测试类命名以它要测试的类的名称开始,以 Test 结尾。
7. 【强制】中括号是数组类型的一部分,数组定义如下: String[] args;
8. 【强制】 POJO 类中布尔类型的变量,都不要加 is,否则部分框架解析会引起序列化错误。
9. 【强制】包名统一使用小写,点分隔符之间有且仅有一个自然语义的英语单词。包名统一使用
10. 【强制】杜绝完全不规范的缩写, 避免望文不知义。
11. 【推荐】为了达到代码自解释的目标,任何自定义编程元素在命名时,使用尽量完整的单词组合来表达其意。
12. 【推荐】如果模块、 接口、类、方法使用了设计模式,在命名时体现出具体模式。
13. 【推荐】接口类中的方法和属性不要加任何修饰符号(public 也不要加) ,保持代码的简洁性,并加上有效的 Javadoc 注释。尽量不要在接口里定义变量,如果一定要定义变量,肯定是与接口方法相关,并且是整个应用的基础常量。
14. 接口和实现类的命名有两套规则:
1) 【强制】对于 Service 和 DAO 类,基于 SOA 的理念,暴露出来的服务一定是接口,内部的实现类用 Impl 的后缀与接口区别。
2)【推荐】 如果是形容能力的接口名称,取对应的形容词做接口名(通常是–able 的形式)。
15. 【参考】枚举类名建议带上 Enum 后缀,枚举成员名称需要全大写,单词间用下划线隔开。
16. 【参考】各层命名规约:
A) Service/DAO 层方法命名规约
1) 获取单个对象的方法用 get 做前缀。
2) 获取多个对象的方法用 list 做前缀。
3) 获取统计值的方法用 count 做前缀。
4) 插入的方法用 save/insert 做前缀。
5) 删除的方法用 remove/delete 做前缀。
6) 修改的方法用 update 做前缀。
B) 领域模型命名规约
1) 数据对象: xxxDO, xxx 即为数据表名。
2) 数据传输对象: xxxDTO, xxx 为业务领域相关的名称。
3) 展示对象: xxxVO, xxx 一般为网页名称。
4) POJO 是 DO/DTO/BO/VO 的统称,禁止命名成 xxxPOJO。
语法命名比排版更加灵活:
包和字段没有语法命名约定。
可实例化的类,包括枚举类通常 使用一个或多个名词短语类命名,例如:Thread、PriorityQueue;不可实例化的类通常使用复数名词来命名,例如:Collections;接口的名称类似于类,例如集合或比较器,或者以able或者以ible结尾的形容词,例如Runnable、Iterable或Accessible。
因为注解类型有很多用途,所以没有哪部分占主导地位。名词、形容词、介词和动词都很常见,例如 BindAnnotation、Inject、Singleton。
方法约定:执行某些操作的方法以动词或动词短语命名(包括对象)命名,例如append、drawImage;返回布尔值的方法通常以单词is或has开头,后面跟一次名词或名词短语或者任何用作形容词的短语,例如 isDigit、isEmpty、isEnabled等;返回被调用对象的非布尔函数或属性的方法通常使用以get开头的名词或名词短语,例如size、hasCode、getTime。
一些方法名词值得特别注意。转换对象类型的实例方法通常称为toType,例如toString;返回与接收对象类型不同的试图的方法称为asType,例如asList;返回与调用他们的对象具有相同值的基本类型的方法通常称为类型值,例如intValue;静态工厂的常见名称包括from、of、valueOf、instance、getInstance、newInstance、getType、newType。
69. 只针对异常的情况下使用异常
异常应该只用于异常的情况下;他们永远不应该用于正常的程序控制流程。
设计良好的API不应该强迫它的客户端为了正常的控制流程而使用异常。
总而言之,异常是为了在异常情况下被设计和使用的。不要将它们用于普通的控制流程,也不要编写迫使它们这么做的API。
70. 对可恢复的情况使用受检异常,对编程错误使用运行时异常
Java中有三种异常:受检异常(checked exceptions==继承自Exception)、运行时异常(runtime exceptions==继承自Runtime)、和错误(errors)。
在决定使用受检异常还是非受检异常时,主要的原则是:如果期望调用者能够合理的恢复程序运行,对于这种情况应该使用受检异常。你实现的所有非受检的throwable都应该是RuntimeException子类,或者直接抛出RuntimeException。不仅不应该定义Error的子类,也不应该抛出AssertionError异常。
用运行时异常来表名编程错误。大多数运行时异常都是违例。比如数组越界等。
总之,对于可恢复的情况,要抛出受检异常;对于程序错误,就要抛出运行时异常。不确定是否可恢复,就抛出受检异常。不要定义任何既不是受检异常也不是运行异常的抛出类型。要在受检异常提供方法以便远程协助恢复。
71. 避免不必要的使用受检异常
Java程序员不喜欢使用受检异常,但是如果使用得当,它们可以改善API和程序。过分使用受检异常会使API使用起来非常不方便,如果方法抛出受检异常,调用该方法的代码就必须在一个或多个catch中处理这些异常,或者它必须声明抛出这些异常并将它们传播出去。
72. 优先使用标准的异常
Java库中已经自带了许多异常,一般可以重用。
最常见的可重用异常如下:(都是允许时异常)
非法参数异常:当调用者传递的参数值不合适时就会抛出,比如:参数希望是正数,调用者传了一个负数。
非法状态异常:因为接收对象的状态而调用非法,比如某个对象被正确地初始化前,调用者就企图使用这个对就会抛出这个异常。
并发修改异常:如果检测到一个专门设计用于单线程的对象,或者与外部同步机制配合使用的对象正在或已经被并发地修改,就应该抛出这个异常。这个异常顶多是一个提示,因为不可能可靠地侦测到并发的修改。
73. 抛出与抽象对应的异常
更高层的实现应该捕获底层的异常,同时抛出可以按照高层进行解释的异常。这种做法称为异常转译。
try { } catch (LowerLevelException e) { throw new HeigherLevelException(.args.); }
一种特殊的异常转译形式称为异常链,如果低层的异常对于调试导致高层异常的问题非常有帮助,使用异常链就很合适。低层的异常原因被传到高层的异常,高层的异常提供访问方法来获得低层的异常:
try { } catch (LowerLevelException cause) { throw new HeigherLevelException(cause); }
大多数标准的异常都支持链的构造器。对于没有支持链的异常,可以用Throwable的initCause方法设置原因。
尽管异常转译与不加选择地从低层传递异常的做法相比有所改进,但是也不能滥用它。处理低层异常的最好做法是:在调用低层方法之前确保他们会成功执行,从而避免它们抛出异常。有时候,可以在给低层传递参数之前,检查更高层方法的参数的有效性,从而避免低层抛出异常。
如果没有办法阻止来自低层的异常,其次的做法是,让高层来悄悄地处理这些异常,从而将高层方法的调用者与低层的问题隔离开来。在这种情况下,可以用某种日志记录机制将日志记录下来。这样有助于管理员调查问题,同时又将客户端代码和最终用户与问题隔离开来。
总而言之,如果不能阻止或者处理来自更底层的异常,一般的做法是使用异常转译,只有在低层方法的规范碰巧可以保证"它所抛出的所有异常对于更高层也适用"的情况下,才可以将异常从低层传播到高层。
74. 每个方法抛出的异常都需要创建文档
始终要单独地声明受检异常,并且利用javadoc的@throws标签,准确地记录下抛出每个异常的条件。永远不要直接抛出Exception,更不能抛出Throwable。
使用javadoc的@throws标签记录下可能抛出的每个受检异常,但是不要将未受检的异常包含在方法声明中。
如果一个类中的许多方法出于同一个原因而抛出同一个异常,在该类的文档注释中对这个异常建立文档,这是可以接受的。
75. 在细节消息中包含失败-捕获信息
当程序由于未被捕获的异常而失败的时候,系统会自动打印该异常的堆栈轨迹。在轨迹中包含该异常的toString方法的结果。通常是类名后跟细节消息。
为了捕获失败,异常的细节信息应该包含有价值的参数和字段的值。例如:IndexOutOfBoundsException异常的细节消息应该包含上边界、下边界以及没有落在界内的值。
76. 保持失败原子性
一般而言,失败的方法调用应该使对象保持在被调用之前的状态。具有这种属性的方法被称为具有失败原子性(failure atomic)。
有几种办法可以实现失败原子性:
(1)设计不可变对象。如果对象不可变,失败原子性是显然的。如果一个操作失败了,它会阻止创建新的对象,但是永远也不会使已有的对象保存在不一致的状态之中,因为当每个对象被创建之后它就处于一致的状态不会变化。
(2)对于可变对象,办法是在执行操作之前检查参数有效性。这可以使得在对象的状态被修改之前,先抛出适当的异常。
(3)在对象的一份临时拷贝上进行操作,当操作完成只会再用临时拷贝的结果代替原对象的内容。可能有性能方面的影响。
77. 不要忽略异常
忽略一个异常很简单,在catch代码块中啥都不处理即可。空的catch代码块会使异常达不到应有的目的。但是一般情况不应该忽略异常,否则可能会造成无法排查的错误。
但是有时候如果就不需要处理,catch代码块应有一条注释,说明为什么这么做,并且变量命名为ignored:
public static void closeQuietly(final Closeable closeable) { try { if (closeable != null) { closeable.close(); } } catch (final IOException ignored) { // ignore } }
补充:数组的越界异常和集合越界异常不同,如下:
数组越界:
String[] strings = new String[] { "1" }; System.out.println(strings[1]);
Exception in thread "main" java.lang.ArrayIndexOutOfBoundsException: 1
at cn.qlq.Client.main(Client.java:12)
list越界:
List<String> list = new ArrayList<String>(); System.out.println(list.get(1));
结果:
Exception in thread "main" java.lang.IndexOutOfBoundsException: Index: 1, Size: 0
at java.util.ArrayList.rangeCheck(ArrayList.java:653)
at java.util.ArrayList.get(ArrayList.java:429)
at cn.qlq.Client.main(Client.java:14)