Effictive Java
最近看了一本书,叫做《Effictive Java中文版》,是Joshua Bloch著,潘爱民翻译的。其实下载这本书已经很久了,但是由于自己一直忙于其他事情,就一直没有看。有一天突然又翻到,所以就从头看了一下。
个人感觉这本书和Effictive C++非常的相似,都是从实例的角度出发,为编程者提供了很多高效,或者说规范编程的建议。刚开始看这本书的时候感觉有种醍醐灌顶的感觉,毕竟自己是个学生,实际项目经验不是很丰富,但依旧感觉收益匪浅。之后看下去的时候,感觉就是有种解读java设计思想的感觉了,因为后面提到的好多东西,其实在缺乏开发经验的前提下并不是能有很大的收获。本人平常看书有总结的习惯,总是喜欢用几句话来总结自己看过的内容,所以在这里将自己写的东西记录一下,以供后期再次回味。
第二章 创建和销毁对象
1. 静态工厂和构造函数,在设计的时候可以考虑用静态工厂方法来替代构造函数
2. singleton从大的方面可以分为饿汉式和饱汉式两种方式
饿汉式确保了该类永远是一个singleton,而饱汉式其实还保留了一点余地
singleton在序列化的时候需要提供一个readResolve,否则在每次反序列化的时候,都会创建一个新的实例
3. 有些类并不想被实例化,而是仅仅作为工具类的存在,比如Arrays,或者Collections等
这种类在编写的时候可以用私有构造函数来强化不可实例化的能力
例如:Arrays的构造函数是 private Arrays() {}
4. 避免创建重复的对象,如果一个对象是不可变的,而且创建时耗时或者消耗内存的,应该尽量去复用,而不是创建
5. 消除过期的对象引用,当一个对象不再被使用时,应该尽量清楚他内部所有对象的引用
6. 避免使用终结函数,终结函数(finalize)的执行时间以及是否执行是不可确定的
在需要释放某些资源的时候,应该尽量使用try-finally结构来处理,而不能寄希望于finalize函数
第三章 对于所有对象都通用的方法
7. 在改写equals方法的时候要注意该方法的一些特性:自反性,对称性,传递性,一致性等
8. 改写equals时总是要改写hashCode,因为Object类规定,相等的对象必须具有相等的散列码(hash code)
9. 总是要改写toString,建议所有的子类都改写这个方法,从而获取自己想要的信息
10. 谨慎地改写clone, object的clone方法是protected的,是被保护的。
实现克隆接口的时候或者进行拷贝的时候,要注意深度拷贝和浅拷贝的区别
还可以实现一个拷贝构造函数或者静态工厂方法的变形
11. 考虑实现Comparable接口,与equals方法非常相似,但也有一些不同
不需要检查实参的类型,如果类型不合适,会抛出ClassCastException异常
如果实参是null,会抛出NullPointerException异常
第四章 类和接口
12. 使类和成员的可访问能力最小化,尽可能地降低可访问性
13. 支持非可变性:一个非可变类是一个简单的类,它的实例不能被修改
坚决不要为每一个get方法编写相应的set方法,除非有很好的理由要让一个类变成可变类
即使一个类不是非可变类,那么也应该尽量限制它的可变性
14. 复合优先于继承,与方法调用不同,继承打破了封装性,子类依赖于超类的功能实现,超类变了,子类就必须要变化
继承:在一个包的内部使用继承是非常安全的
或者专门为了继承而设计,并且具有良好文档说明的类,使用继承也是很好的
复用:不是扩展一个已有的类,而是在新的类中增加一个私有域,引用了已有类的一个实例。其实类似于一个包装类
在java中,stack并不是vector,因此应该使用复用,而不是继承
同样的,属性列表也不是一个散列表,因此Properties不应该扩展Hashtable
继承会把超类中的缺陷传播到子类中,而复合可以设计新的API,从而隐藏这些缺陷
15. 要么专门为继承而设计,并给出文档说明,要么禁止继承
为了继承而设计一个类,要求对这个类有一些实质性的限制,这并不是轻松可以决定的
防止类被子类化:1——把这个类声明成final的
2——把所有的构造函数声明为私有的,或者说是包级私有的
16. 接口优于抽象类:接口使得我们可以构造出非层次结构的类型框架
接口使得安全地增强一个类的功能成为可能,通过包装类的模式
抽象类可以用来做为一个骨架实现类,然后用各种接口来进行具体功能的实现和扩展
抽象类的演化比接口的演化要容易得多,可以更加方便地增加新的方法
17. 接口只是被用于定义类型的:一个类实现了一个接口,这个接口就是一个类型,通过此类型可以引用这个类的实例
常量接口,包含了很多静态的final域,实现该接口的类都可以使用这些常量(不建议这样用)
接口应该只是被用来定义类型的,不应该被用来导出常量
18. 优先考虑静态成员类,嵌套类有四种:静态成员类,非静态成员类,匿名类,局部类
静态成员类:外围类的一个成员,可以访问外围类的所有成员
非静态成员类:每一个实例都隐含着与外围类的一个外围实例紧密关联在一起,会浪费时间和空间
局部类:在可以声明局部变量的地方,可以声明局部类。与成员类一样,局部类有名字,可被重复使用
匿名类:没有名字,不是外围类的一个成员。在被使用的点上被声明和实例化,可以出现在任何允许代码出现的地方,比如Thread,Runnable实例
第五章 C语言结构的替代
19. 用类代替结构,类可以把数据封装到一个对象中,并且只有对象的方法才可以访问这些数据。
公有类不应该直接暴露数据域,而应该使用get和set函数等
20. 用类层次来代替联合(union),可以定义一个抽象类,然后扩展其他类来实现功能
类层次提供了类型安全性,而且代码更加简洁,容易扩展,反映类型之间本质上的层次关系
21. 用类来代替enum结构,java中存在一种类型安全枚举,定义一个类来代表枚举类型的单个元素
对于任何枚举类型的需求都应该相对较少,因为随着子类化技术的推广,这些类型的主要用途已经过时了。
22. 用类和接口来代替函数指针
C语言的函数指针主要用途是实现Strategy(策略)模式。java中可以用一个接口表示该策略,并声明一个实现该接口的类
第六章 方法
23. 检查参数的有效性
应该在方法体的起始处对参数进行检查,非公有的方法通常应该使用assertions(断言)来检查它们的参数
编写方法或者构造函数的时候,应该考虑参数有哪些限制
24. 需要时使用保护性拷贝
保护性拷贝动作是在检查参数的有效性之前进行的,而且有效性检查是针对拷贝之后的对象,而不是原始的对象
25. 谨慎设计方法的原型
1——谨慎选择方法的名字
2——不要过于追求提供便利的方法:每一个方法都应该提供其应具备的功能点。不要定义太多的方法
3——避免长长的参数列表:三个参数应该被看作实践中的最大值,参数应该越少越好
4——对于参数类型,优先使用接口而不是类:这样方便后来的扩展
5——谨慎地使用函数对象
26. 谨慎地使用重载
重载方法的选择是静态的,并不能保证重载的函数执行的是自己逻辑上想要他执行的那一个
一个安全保守的策略是,永远不要导出两个具有相同参数数目的重载方法
27. 返回零长度的数组而不是null
返回null会导致程序员需要额外的代码来处理这种情况
28. 为所有导出的API元素编写文档注释
java语言环境提供了一个被称为javadoc的实用工具,可根据源代码自动产生API文档
在文档注释内部可以出现任意的HTML标签,但是HTML元字符必须要经过转义
第七章 通用程序设计
29. 局部变量的作用域最小化
c程序设计语言要求局部变量必须被声明在一个代码块的开始处
java程序语言允许在任何可以出现的地方声明变量,提高代码的可读性
30. 了解和使用库
了解标准库的函数和使用方法,会让你的代码变得更加简单
不要从头发明轮子,如果你要做的事情看起来很常见,可能库中已经封装了相应的函数
31. 如果要求精确的答案,请避免使用float和double
float和double类型的主要设计目标是科学计算和工程计算,对于货币计算非常不合适
精确计算应该更多的使用BigDecimal,int或者long等进行货币计算
32. 如果其他类型更加适合,则尽量避免使用字符串
字符串不适合代替枚举类型,聚集类型,能力表等
33. 了解字符串连接的性能
字符串连接符并不适合规模比较大的情形,尽量使用StringBuffer
34. 通过接口引用对象
例如:List subscribers = new Vector() 使用接口转变会更加的灵活,特别是工厂模式
35. 接口优先于反射(reflection)机制
反射机制可以通过程序来访问已装载的类的信息
复杂的应用程序需要使用反射机制,包括浏览器,对象监视器,代码分析工具,内嵌的解释器等
反射机制的缺点:1. 损失了编译时类型检查的好处,可能会调用不存在的方法
2. 要求执行映像访问的代码非常笨拙和冗长
3. 性能损失,耗时可能是普通方法的几十倍
36. 谨慎地使用本地方法(JNI)
使用本地方法主要有几种好处:1. 访问与平台相关的设施,比如注册表和文件锁等
2. 访问老式代码库以及老式数据等
3. 使用本地的方法,从而提高性能等
使用本地方法的缺点:1. 使用本地的语言可能是不安全的
2. 由于本地方法是平台相关的,因此使用了本地的方法,程序也不再是可自由移植的
3. 对于每一个平台,本地代码需要重新编译
4. 在进入和退出本地代码时,需要较高的程序开销
37. 谨慎地进行优化
在每次试图做优化之前和之后,应该对性能进行测量
38. 遵守普遍接受的命名惯例
字面的:主要涉及到包,类,接口,方法和域等的命名,如大小写习惯等
语法的:类通常用一个名词或者名词短语命名(接口类似)
执行某个动作的方法通常用一个动词或者动词短语来命名
第八章 异常
39. 只针对不正常的条件才使用异常(永远不应该被用于正常的控制流)
异常的设计初衷是用于不正常的情形。所以创建,抛出和捕获异常的开销都是很昂贵的
40. 对于可恢复的条件使用被检查的异常,对于程序错误使用运行时异常
java中有三种可抛出结构:1.被检查的异常(checked exception),可恢复的条件
2.运行时异常(run-time exception),程序错误
3.错误(error),往往是不可恢复的情形
41. 避免不必要地使用被检查的异常
42. 尽量使用标准的异常
重用现有的异常:1.使得你的API更加易于学习和使用
2.对于用到这些API的程序而言,它们的可读性更好
3.异常类越少,意味着内存占用越小,装载类的开销也越小
43. 抛出的异常要适合于相应的抽象
底层异常的处理:1.可用使用异常转译来处理
异常转译:高层的实现应该捕获底层的异常,同时抛出一个可以按照高层抽象进行解释的异常
2.如果可以的话,应该在处理来自底层异常的时候,确保它们会成功地执行
44. 每个方法抛出的异常都要有文档
使用javadoc的@throws标记,准确地记录下每个异常被抛出的条件
如果一个类中的许多方法由于同样的原因而抛出同一个异常,那么在该类的文档注释中做文档是可以的
45. 在细节消息中包含失败-捕获信息
异常类型的toString方法应该尽可能多地返回有关失败原因的信息
一个异常的字符串表示应该包含所有“对该异常有贡献”的参数和域的值
栈轨迹通常包含了异常被抛出的确切文件和行数
46. 努力使失败保持原子性
一个失败的方法调用应该使对象保持在“它被调用之前的状态”(失败原子性)
失败原子性获得的办法:1.设计一个非可变的对象,操作失败将会阻止创建新的对象
2.对计算处理过程调整顺序,使得任何可能失败的计算部分都发生在对象状态被修改之前
3.编写一段恢复代码,使对象回滚到操作开始之前的状态
4.在对象的一份临时拷贝上执行操作,操作完之后再复制给原来的对象
47. 不要忽略异常
空的catch块会使异常达不到应有的目的。忽略异常会导致程序在遇到错误的时候继续悄然地执行下去
第九章 线程
48. 对共享可变数据的同步访问
在使用多个线程共享可变数据的时候,每个读或者写数据的线程必须获得一把锁
49. 避免过多的同步
过多的同步可能会导致性能降低,死锁,甚至不确定的行为
通常,在同步区域内应该做尽量少的工作。获得锁,检查共享数据,变换数据,释放锁。
为了避免死锁和数据破坏,千万不要在同步区域内部调用外来方法
50. 永远不要再循环的外面调用wait
object.wait方法是使一个线程等待某个条件,一定是在同步区域中被调用的
在选择notify还是notifyAll的时候,总是建议使用notifyAll唤醒所有线程,让线程去检测是否满足条件
总是在一个while循环中调用wait,并且使用标准模式
51. 不要依赖于线程调度器
线程调度器决定哪个线程运行以及运行时间。但不同的JVM可能会有区别,依赖于调度器的多线程可能是不可移植的
健壮的,相应良好的,可移植的多线程调度器应该确保在任何时刻只有少量的可运行线程
让每个线程做少量的工作,然后使用object.wait等待条件或者Thread.sleep睡眠一段时间
线程优先级是java平台上最不可移植的特征,因此尽量不要使用这个方法来改善应用程序
不要试图通过Thread.yield来修正程序,因为不同的JVM对于yield有不同的反映
52. 线程安全性的文档化
一个类可能支持的线程安全性级别:1.非可变的,类的实例是不变的,如String,Integer,BigInteger等
2.线程安全的,类的实例是可变的,但所有方法都包含足够的同步机制
3.有条件的线程安全,这个类的某些方法必须被顺序调用
4.线程兼容的,在每个方法调用的外围使用外部同步
5.线程对立的,这个类不能安全地被多个线程并发使用,这样的类或者方法在java中非常少
53. 避免使用线程组
线程组允许你把Thread的基本功能直接应用到一组线程上
线程组基本上已经过时了,不建议使用
第十章 序列化
序列化是将一个对象编码成一个字节流,反序列化则是将字节流转变为对象
对象被序列化之后可以从一个正在运行的虚拟机传递到另一个虚拟机上,或者存储到磁盘中
54. 谨慎地实现Serializable
应该设计一个高质量的序列化形式,不然后期改变这个类会很麻烦
默认的序列化会导致类中私有的和包级私有的实例域都变成导出API的一部分
一个类实现了Serializable后,随着新版本的发行,相关的测试负担增加了
实现Serializable是一个很严重的承诺(除非该类一段时间后会被抛弃)
55. 考虑使用自定义的序列化形式
理想的序列化形式应该只包含该对象所表示的逻辑数据。
如果没有认真考虑默认序列化形式是否合适,则不要接受这种形式
即使确认了默认序列化形式合适,也需要一个readObject方法以保证约束关系和安全性
不管选用了哪种序列化形式,都要为每个可序列化的类声明一个显式的序列版本UID
一个对象的物理表示与他的逻辑数据内容有实质差别时,默认的序列化有4个缺点:
1. 使这个类的导出API永远束缚在该类的内部表示上
2. 消耗过多的空间,物理表示和逻辑数据都要存储起来
3. 消耗过多的时间,序列化逻辑需要昂贵的图遍历
4. 有可能引起栈的溢出,默认的序列化过程会对对象图进行一次递归遍历
56. 保护性地编写readObject方法
readObject方法相当于一个构造函数。因此也需要检查参数的有效性以及必要时对参数进行保护性拷贝
保护性拷贝应该是在有效性检查之前进行的
57. 必要时提供一个readResolve方法
如果一个类定了readResolve方法,在反序列化的时候,新创建的对象会调用这个方法,而这个新创建的对象不再有用
readResolve方法不仅对singleton对象是必要的,对于其他的实例受控的类也是必须的
readResolve方法可以替代保护性的readObject方法