第九章 异常
第五十七条:只针对异常的情况才使用异常
1.异常应该只用在异常的情形下。
他们永远不应该用在正常的控制流中。
第五十八条: 对可恢复的情况使用受检异常,对编程错误使用运行时异常
1.Java语言有三种可抛出的结构:
受检异常、错误和运行时异常
(1)决定使用何种异常时主要原则是:如果期望调用者能够从异常中适当的恢复,那么就使用受检异常强迫用户处理或者再抛出。运行时异常属于不可恢复的情形
(2)用运行时异常来表明编程错误。大多数运行时异常都表示前提违例,比如空指针异常。所有未受检的抛出结构都应该是RuntimeException的子类
(3)现实情况应该仔细判断,但原则是能否从异常中恢复。受检的异常应该提供一些辅助方法或者在文档中写明如何恢复
第五十九条: 避免不必要地使用受检的异常
1.注意
过分使用受检异常会让API使用起来格外不方便。如果正确的使用API不能阻止这种异常条件的产生,并且一旦产生异常,使用API的程序员就可以立即采取有用的动作,才是正当使用受检异常的情形
第六十条: 优先使用标准的异常
1.重用异常有多方面好处
(1)让你的API更加易于学习和使用,因为它和程序员已经熟悉的习惯是一致的
(2)可读性会更好
(3)内存印记会更小,提高装载速度
2.常用异常及其适用情况
(1)IllegalArgumentException
场合:非null的参数值不正确
(2)IllegalStateException
场合:对于方法调用而言,对象状态不适合
(3)NullPointerException
场合:在禁止使用null的情况下参数值为null
(4)IndexOutOfBoundsException
场合:下标参数值越界
(5)ConcurrentModificationException
场合:在禁止并发修改的情况下,检测到对象的并发修改
(6)ClassCastException
场合:类型转换时出错
(7)UnsupportedOperationException
场合:对象不支持用户请求的方法
第六十一条: 抛出与抽象相对应的异常
1.异常转译的概念
更高层的实现应该捕获低级的实现,同时抛出可以按照高层抽象进行解释的异常,这种做法叫做异常转译
另外一种异常转译的方法叫做异常链。
2.异常转译不能被滥用
最好的做法是:调用底层方法之前,通过检查参数等手段确定调用底层方法会成功。次好的方法是让高层方法绕开这些异常,隔离高层方法的调用者和底层的问题,使用Log记录这些异常。
第六十二条: 每个方法抛出的异常都必须有文档
1.注意
(1)始终要单独声明受检的异常,并且利用JavaDoc的@throws标记准确的记录下抛出每个异常的条件。如果抛出多个异常,不要使用快捷方式声明抛出的超类。不要抛出Exception,不要把未受检异常放在方法声明的throws子句中
(2)对于未受检异常,也应该仔细地为他们建立文档。这样可以有效描述方法的前置条件。对于接口中的方法,在文档中描述可能抛出的未受检异常格外重要,因为这是通用约定的一部分。
(3)如果某个类的很多方法因为同一个原因抛出同一个异常,那么应该在这个类的文档注释中进行说明
第六十三条: 在细节信息里包含能捕获失败的信息
1.注意
(1)为了捕获异常,异常的细节信息里应该包含所有“对此异常有贡献“的参数和域的值。例:数组越界异常应包含没有落在界内的下标值,数组的上界和下界。
(2)异常的细节不应该同”用户层面的错误信息“相提并论。后者对于最终用户必须是可以理解的。而异常的细节里,信息的内容比可理解性要重要得多。
第六十四条:努力使失败保持原子性
1.注意
(1)原子性:失败的方法调用应该使对象保持调用前的状态
(2)对于在可变对象上执行的操作方法, 要获得失败原子性, 最常见的做法就是操作
之前检查参数的有效性.
(3)调整方法执行的顺序, 将任何可能导致导致失败的部分都在对象状态被修改之前发生.
第六十五条:不要忽略异常
1.注意
(1)空的catch块会让异常达不到应有的目的。至少应该在catch块里说明为什么可以忽略这个异常。
(2)有一种情形可以忽略:关闭FileInputStream。即使这样也应该记录异常
(3)正确的异常处理能够彻底挽回失败,必须将异常传递出去。
第十章 并发
第六十六条: 同步访问共享的可变数据
1.同步的概念:
(1)当一个对象被一个线程修改的时候,可以阻止另一个线程观察到对象内部不一致的状态
(2)保证进入同步方法或者同步代码块的每个线程,都能看到由同一个锁保护的之前所有的修改效果。
2.为了在线程之间进行可靠通信,也为了互斥访问,同步时必须的
3.最佳方法:
共享不可变的数据,或者不共享可变的数据。把可变数据限制在单个线程里。
第六十七条: 避免过度同步
1.注意
(1)在一个被同步的方法或者代码块里,永远不要放弃对客户端的控制。
(2)Java的锁都是可重入的,在一次迭代中列表已经被锁定了,再进入函数修改这个这个列表就会抛出异常。
(3)可重入的锁简化了面向对象程序构造,但是会把活性失败变成了安全失败
通常,在同步区域内应该做尽可能少的工作。多核时代的实际成本并不是只获得锁所花费的CPU时间,而是失去了并行的机会,以及需要保证每个核都有一个一致的内存视图而导致的延迟。
(4)另一项潜在开销在于它会阻碍VM对代码的优化。
(5)如果一个类要并发使用,应该把这个类变成线程安全的,。不确定的时候就不要同步类,而是建立文档并说明它不是线程安全的。
第六十八条: Executor和task优先于线程
1.注意
(1)Java.util.concurrent包含了一个Executor Framework,完成了一个简单的工
作队列。
(2)Executor service还可以完成更多的任务
可以等待完成一项特殊的任务
可以等待一个任务集合中的任何任务或所有任务完成(invokeAny和invokeAll方法)
可以等待executor service优雅地完成(awaitTermination方法)
„在任务完成时逐个获得任务的结果(ExecutorCompletionService方法)
(3)如果想让不止一个线程来处理这个队列的请求,只要调用一个不同的静态工厂,这个工厂创建了一种不同的executor service,称作线程池thread pool。你可以直接使用ThreadPoolExecutor类控制整个线程池的每个方面。
(4)Executor Framework的工作是执行,而Collection Framework的工作是聚集
(5)Executor Framework也有可以代替Timer的东西叫做ScheduledThreadPool类。
Timer虽然更容易,但是executor更加灵活。
Timer只用一个线程执行任务,长期运行的任务会影响定时的准确性
Timer唯一的线程抛出为捕获异常,timer就会停止执行。被调度的线程池executor支持多个线程,并且优雅的从异常中恢复
第六十九条: 并发工具优先于wait和notify
1.注意
(1)Java的新版本提供了更高级的并发工具,就不要使用过去的wait和notify。
(2)Java.util.concurrent中更高级的工具分三类:Executor Framework、并发集合以及同步器
第七十条: 线程安全性的文档化
1.注意
(1)如果没有适当的文档,程序员有可能对类的线程安全性做出错误的假设。从而导致过度同步或者缺乏同步,引发严重错误
(2)一个类为了可被多个线程安全使用,必须在文档中清楚地说明它支持的线程安全级别
不可变的immutable。这个类是不变的,不需要同步。例子:String,Long等
无条件的线程安全unconditionally thread-safe。这个类的实例是可变的,但是在内部做了足够的同步。它可以被并发使用,不需要外部同步。
例:ConcurrentHashMap
有条件的线程安全conditionally thread-safe。这个类的实例的某些方法需要外部同步,其他与无条件的类相同。例子:Collection.synchronized集合,iterator需要同步。
„非线程安全not thread-safe。这个类的实例是可变的。为了并发使用,必须用外部同步包围每个方法调用。例子:ArrayList和HashMap
…线程对立的thread-hostile。这个类即使被外部同步全部包围,也不能被并发使用。
第七十一条:慎用延迟初始化
1.注意
(1)延迟初始化是等到需要时在初始化的行为。除非绝对必要,否则不要这么做。大多数情况下,正常的初始化优先于延迟初始化。
(2)适用延迟初始化:如果域只在类的实例部分被访问,而且初始化这个域的开销很高。应该测量性能
第七十二条:不要依赖线程调度器
1.注意
(1)任何依赖线程调度器来达到正确性或者性能要求的程序,很有可能是不可移植的。
(2)要编写健壮的可移植的多线程程序,应该确保可运行线程的平均数量不明显多于处理器数量。注意是可运行线程,而不是线程。
(3)减少可运行线程的方法是:如果线程没有在做有意义的事,就不应该运行。任务应该有合适的大小。
(4)线程不应该一直处于忙等待状态:增加CPU负载
(5)如果一个程序不能工作是因为某些线程无法像其他线程一样得到充足的CPU时间,那么不要企图通过调用Thread.yield来修正。最好的做法是重构程序,减少并发线程数量
(6)线程优先级是最不可移植的特征。不要调整线程优先级。
(7)不要使用Thread.yield来进行调试。应该使用Thread.sleep(1)来进行测试
第七十三条: 避免使用线程组
1.注意
(1)线程组的初衷是隔离applet的机制,但是他们的安全价值已经差到根本不在Java安全模式的标准工作中提及的底部。
(2)从线程安全性的角度来看,ThreadGroup API非常弱。线程组已经过时了,实际上根本没必要修正。如果你正设计的类需要处理线程的逻辑组,那么就应该使用线程executor。
第十一章 序列化
第七十四条: 谨慎的实现Serializable接口
1.注意
(1)实现Serializable接口而付出的最大代价是, 一旦一个类被发布,就大大降低了“改变这个类的实现“的灵活性。改变可能导致序列化形式的不兼容。一定要仔细设计高质量的序列化方案
(2)序列化会使类的演变受限,这种限制与类的序列版本serial version UID有关。如果增加了一个公有方法,UID也会变化。如果没有声明UID,兼容性会被破坏
(3)实现Serializable的第二个代价是,它增加了出现bug和安全漏洞的可能性。反序列化机制是一种语言之外的对象创建机制,因此也必须保证所有“有真正的构造其建立起来的构造关系”,而且不允许攻击者访问对象的内部信息。
(4)实现Serializable的第三个代价是,随着类发行新的版本,相关的测试负担也增加了。应该测试在新/旧版本实例化一个对象,在另一方版本反序列化。这些测试不可能自动构建,因为除了二进制兼容性,还要测试语义兼容性,保证结果对象是原始对象的真正复制。
(5)实现Serializable接口的优劣
- 优点:
如果一个类将要加入某个依赖序列化来实现对象传输或持久化的框架,就该实现这个接口。
如果这个类要成为一个类的组件,且后者实现了Serializable接口,那么前者也应该实现这个接口。值类或者大多数集合类都应该实现Serializable接口
- 缺点
为了继承而设计的类/用户接口尽可能少实现Serializable接口,除非是必须的情况(以上)。实现了Serializable接口的有Throwable类,component类或者HttpServlet抽象类。
如果类有一些约束条件,当类的实例域被初始化成默认值时,就会违背这些约束条件,这时候就必须readObjectNoData()函数
如果一个为继承而设计的类不可序列化,就不可能编写可序列化的子类。如果超类没有提供可访问的无参构造器,子类也不可能可序列化。因此,为继承而设计的不可序列化的类,都应该考虑一个无参构造器。
„最好在所有约束关系都已经建立的情况下在创建对象。盲目为一个类增加无参构造器,而它的约束关系由其他构造器来建立,就会更加复杂易出错。
(6)内部类不应该实现Serializable。内部类的默认序列化形式是定义不清楚,静态成员类可以实现Serializable接口。
第七十五条: 考虑使用自定义的序列化形式
1.使用默认的序列化类型的缺点
(1)它使这个类的导出API永远束缚在该类的内部表示法。
(2)它会消耗过多的空间
(3)它会消耗过多的时间。图遍历非常耗时
(4)它会引起栈溢出。
第七十六条:保护性地编写readObject方法
1.注意
(1)readObject实际相当于一个公有的构造器,接受字节流作为唯一的输入。所以readObject也必须做保护性拷贝以及检查参数的有效性。
(2)应该对所有的实例域做保护性拷贝,并且去掉final修饰符
(3)应该提供一个显式的readObject并且做所有的参数有效性检查以及保护性拷贝。或者使用序列化代理模式。
(4)readObject不可以调用可覆盖的方法,
第七十七条:对于实例控制,枚举类型优先于readResolve
1.注意
(1)readResolve特性允许你用readObject创建的实例代替另一个实例。
(2)如果将一个可序列化的实例受控的类编写成枚举,就可以绝对保证除了所声明的常量外不会有别的实例。
(3)readResolve的可访问性很重要。
第七十八条:考虑用序列化代理模式
1.序列化代理模式的概念
首先为可序列化的类设计一个私有的静态嵌套类,精确地表示外围类的实例的逻辑状态。它应该有一个单独的构造器,以外围类实例作为参数。这个构造器只从它的参数中复制数据,不需要一致性检查/保护性拷贝。外围类和它的序列化代理都必须实现Serializable接口。
2.序列化代理模式有三个缺点
(1)不可以与可以被客户端拓展的类兼容
(2)不能与对象图中包含循环的某些类兼容
(3)开销比较大
本人才疏学浅,若有错,请指出
谢谢!