《Effective Java》笔记
以前看书时记的笔记,分享一下。
1 创建和销毁对象
1.1 考虑用静态工厂方法代替构造函数
静态工厂方法相比于构造函数的优点: 1、具有名字,从而易于阅读; 2、每次调用时不要求非得创建一个新的对象(如Singleton); 3、可以返回一个原返回类型的子类型对象; 缺点: 1、如果不含公有的或者受保护的构造函数,就不能被子类化 2、静态工厂方法与其他静态方法没有任何区别
1.2 使用私有构造函数强化singleton属性
1.3 通过私有构造函数强化不可实例化的能力
工具类只有静态方法,禁止继承和实例化
1.4 避免创建重复的对象
l 错例:String s = new String("silly");
l 小对象的创建和回收动作是非常廉价的。通过创建附加的对象 以使得程序更加清晰、简洁、功能强大也是一件好事
l 反之,通过维护自己的对象池来避免对象创建并不是一个好做法, 除非池中的对象是非常重量级的(如数据库连接池)
l 当你应该创建重用一个已有对象时,请不要创建新对象
1.5 消除过期的对象引用
l 只要一个类自己管理内存,程序员就应该警惕内存泄漏, 一旦一个元素被释放掉,则该元素中包含的任何引用应该被清空
l 内存泄漏的另一个常见来源是缓存。要定期清除不再使用的缓存对象。
1.6 避免使用终结函数
l 终结函数(finalizer)通常是不可预测的,也是危险的,一般情况下是不必要的。
l 执行终结函数的线程的优先级比应用程序的其他线程要低得多
l 显式的终止方法通常与try-finally结构结合起来使用,以确保及时终止。
l 除非作为安全防护网(safety net)或者为了终止非关键的本地资源,否则不要使用终结函数
2 对于所有对象都通用的方法
Object类所有非final方法(equals、hashCode、toString、clone和finalize) 都有明确的通用约定,都是为了被改写(override)而设计的。如果不做, 则其他依赖于这些约定的类就无法与之结合一起正常运作
2.1 在改写equals的时候请遵守通用约定
l 要避免问题最容易的办法是不改写equals方法 每个实例只与它自己相等
类的每个实例本质上都是唯一的
不关心为是否提供了“逻辑相等(logical equality)的测试功能
超类已经改写了equals,从超类继承过来的行为对于子类也是合适的。
l 一个类是私有的,或者是包级私有的, 并且可以确定它的equals方法永远也不会被调用, 应改写equals方法,以防万一有一天被调用。
public boolean equals(Object o){ throw new UnsupportedOperationException(); }
l 改写equals方法必须遵守的通用约定, equals方法实现了等价关系(equivalence relation)
n 自反性(reflexive)。对于任意的引用值x, x.equals(x)一定为true
n 对称性(symmetric)。对于任意的引用值x和y, 当且仅当y.equals(x)返回true时,x.equals(y)也一定返回true。
n 传递性(transitive)。对于 任意的引用值x、y和z,如果x.equals(y)返回true,并且y.equals(z)也返回true, 那么x.equals(z)也一定返回true。
n 一致性(consistent)。对于任意的引用值x和y, 如果用于equals比较的对象信息没有被修改的话, 那么多次调用x.equals(y)要么一致返回true,要么返回false。
对于任意的非空引用值x, x.equals(null)一定返回false。
2.1.1 改写equals的步骤
1、使用==操作符检查“实参是否为指向对象的一个引用”。
2、使用instanceof操作检查“实参是否为正确的类型”
3、把实参转换到正确的类型
4、对于该类由每一个"关键(significant)"域,检查实参中 的域与当前对象中对应的域值是否匹配
5、当编写完成了equals方法之后,现问自己三个问题:它是否是对称的、传递的、一致的?
2.1.2 改写equals的告诫
当你改写equals时,总是要改写hashCode
不要使equals方法过于聪明
不要使equals方法依赖于不可靠的资源
不要将eqauls声明中的Object对象替换成其他的类型 (改类型是Overload了)
2.2 改写equals时总是要改写hashCode
l 相等的对象必须具有相等的hashCode
l 不相等的对象不要求具有不同的hashCode, 但不同的值有利于提高hashTable的性能
l 一个好的hash函数通常倾向于”为不相等的对象产生不相等的散列码“
l hash生成方法
1、把某个非零常数值保存到int result中;
2、对于对象每一个关键域f (指equals方法中考虑的每一个域), 完成以下步骤:
a. 为该域int类型的hashCode c:
b. 按result=37*result+c的公式,把步骤a中 计算得到的散列值码c组合到result中;
i. if is boolean then f ? 0:1
ii. if is byte, char, short, int, then (int) f
iii. if is long then (int) (f ^ (f>>>32))
iv. if is float then Float.floatToIntBits(f)
v. if is doule then Double.doubleToLongBits(f)得到long,再按iii步骤计算
vi. if is Object,则取这个对象的hashCode,if is null then 0
vii. if is array, 则对数据所有对象计算hashCode,再按2.b计算,再组合
c. 返回result
d. 检查”是否相等的实例具有相等的散列码"
l 不要试图从散列码计算中排除掉一个对象的关键部分以提高性能。
2.3 总是要改写toString
l 提供一个好的toString实现,可以使一个类用起来更加愉快
l 当一个对象传给println、字符串连接操作符(+)以及assert时, toString方法会自动调用。
l 实际应用中,toString方法应该返回对象中包含的所有令人感兴趣的信息。
l 为toString返回值中包含的所有信息,提供一种编程访问途径,这是一个好的做法。
2.4 谨慎地改写clone
l Cloneable接口的目的是作为对象的一个mixin接口(mixin interface),表明这样的对象允许克隆。
l 如果改写一个非final类的clone方法,则应该返回一个通过调用super.clone而得到的对象。
l clone是另一个构造函数,必须保证它不会伤害到原始的对象。并建立起被克隆对象的约束关系。
l 所有实现了Cloneable接口的类都应用一个公有的方法改写clone.
l 拷贝构造函数
2.5 考虑实现Comparable接口
3 类和接口
3.1 使类和成员的可访问能力最小化
3.1.1 公共类不应有公共域
3.2 支持非可变性
l 非可变类:
是一个简单的类,它的实例不能被修改。 实例所有信息都在创建时提供 在对象整个生存期内固定不变。
l 五条规则
1、不要提供任何会修改对象的方法
2、保证没胡可被子类改写的方法
3、所有的域都是final的
4、所有域都是私有的
5、保证对于任何可变组件的互斥访问
l 非可变对象本质上是线程安全的,不要求同步
l 非可变对象为其他对象提供了大量的构件(bulding blocks)
l 非可变对象对于每一个不同的值都要求一个单独的对象
l 不要为每个get方法编写一个相应的set方法
3.3 复合优先于继承
3.3.1 原因
继承是实现代码重用的有力手段,但它并不总是完成这项工作的最佳工具。
包内部使用继承和专门为继承而设计且有很好文档说明的类,使用继承是安全的。
而普通的具体类进行跨越包边界的继承则是非常危险的
继承打破了封装性
子类依赖于超类中特定功能的实现细节
3.4 要么专门为继承而设计,并给出文档说明, 要么禁止继承
文档必须精确描述改写每一个方法所带来的影响
允许继承的类的构造函数一定不能调用可被改写的方法
3.5 接口优于抽象类
已有的类可以很容易更新以实现新接口
接口是定义mixin(混合类型)的理想选择
接口使得可以构造出非层次结构的类型框架
接口使得安全地增强一个类的功能成为可能
可以把接口和抽象类的优点结合起来,对于期望导出的每一个重要接口, 都提供一个抽象的骨架实现(skeletal implementation)类。接口的作用仍然是定义类型, 但骨架实现类负责所有与接口实现相关的工作。
骨架实现优美之处:抽象类提供了实现上的帮助, 但又没有强加“抽象类被用作类型定义时候”所特有的严格限制。
3.6 接口只是被用于定义类型
3.6.1 常量接口模式是对接口的不良使用
不要在接口中定义常量
常量定义到常量工具类中
3.7 优先考虑静态成员类
3.7.1 嵌套类nested class
l 静态成员类static member class
最简单的嵌套类,是外围类的一个静态成员
通常是公有的辅助类
l 非静态成员类nonstatic member class
每一个实例都隐含一个外围实例的引用this
实例不能在外围类实例之外独立存在。
创建时即创建了与外围实例的关系
l 匿名类anonymous class
应该非常简短,可能20行或更少,太长影响程序可读性
用作函数对象(function object) 或过程对象(process object)
l 局部类local class
必须非常精简
3.7.2 用法
如果需要在单个方法之外仍可见或太长而不适合放在方法内,那就使用成员类
如果声明的成员类不要求访问外围实例,那么就采用静态成员类,否则非静态
4 C语言结构的替代
4.1 用类代替结构
公有类不要直接访问数据域,而私有嵌套类或包私有类这样做则更简洁
4.2 用类层次来代替联合
4.3 用类来代替enum结构
4.3.1 类型安全枚举(typesafe enum)模式
定义一个类来代表枚举类型的单个元素,并且不提供任何公有的构造函数, 提供公有的静态的final域,使枚举类型中的每一个常量对应一个域。
1、好处
l 提供了编译时的类型安全性
如果声明的方法参数为这一类型,如果传入非null的对象引用一定是枚举类型之一,不可能为其他。
l 新常量加入时无需重新编译客户代码
枚举常量的公有静态对象引用域为客户和枚举类型之间提供了一层隔离层。 而int模式和String变形,常量被编译到客户代码中。
l 可改写toString实现可打印字符串和国际化
l 是类,可以扩展,可以实现接口
l 可放入集合、列表
l 可方便序列化
l 性能可与int枚举类型相媲美
l 具有普遍适用性
2、缺点
l 要把类型安全枚举聚集到一起比较困难,而int而方便。
l 不能使用switch,而只能用if
l 装载时需要一定的时间和空间开销,而使之不能用在资源很受限的设计上。
4.4 用类和接口来代替函数指针
C语言中函数指针主要用途是实现Strategy(策略)模式。
在JAVA中声明一个接口来表示该策略,并为每个具体 的策略声明一个实现该接口的类
如果具体策略只被使用一次,那常通过匿名类声明和实例化这个策略
如果要被导出以便重复使用,则这个类通常是私有静态成员类, 并通过一个公有静态final域被导出,其类型为该策略接口。
5 方法
5.1 检查参数的有效性
5.1.1 不检查的危害
l 该方法在处理过程中失败,产生一个令人迷惑的异常
l 方法正常返回,但计算出的结果错误
l 方法正常返回,但使得对象处于一种被破坏的状态, 将来在某个不确定的时候,在某个不相关的点引发出错误来
如果无效参数值传入时即检查并以适当异常指明错误原因,将易于定位错误。
5.1.2 方法
非公有方法常应该使用assertions断言来检查参数而不使用检查语句
5.1.3 例外
当有效性检查工作非常昂贵或根本不切实际, 且在计算过程中有效性检查工作也隐含完成了。
注意方法本身处理时的原子性
5.2 需要时使用保护性拷贝
l 在类的客户会尽一切手段来破坏这个类的约束条件下,必须保护性地设计程序
l 有选择性的实施这个规则
5.3 谨慎设计方法的原型
l 谨慎选择方法的名字
l 不要过于追求提供便利的方法
l 避免长长的参数列表,三个参数应该被看做实践中的最大值
类型相同的长参数序列尤其有害
减少参数个数的方法
一个方法分解成多个方法,但会导致方法过多
创建辅助类helper class,用来保存参数的聚焦aggregate
l 对于参数类型,优先使用接口而非类
l 谨慎使用函数对象
5.4 谨慎地使用重载overload
重载方法overloaded method的选择是静态的,而overridden method是动态的
一个安全而保守的策略是:永远不要导出两个具有相同参数数目的重载方法。
5.5 返回零长度的数组而不是null
l 对于一个返回null而不是零长度数组的方法, 几乎每次用到该方法的时候都需要这种曲折的处理方式
l 担心返回零长度数组会占用开销是站不住脚的
因为:1、在这个层次上担心性能问题是不明智的,除非分析表明这个方法正是造成性能问题的真正源头
2、对于不返回任何元素的调用,每次都返回一个零长度数据是有可能的,因为零长度数组是非可变的, 而非可变对象有可能被自由地共享。
l 没有理由从一个取数组值(array-valued)的方法中返回null,而不是返回一个零长度数组。
5.6 为所有导出的API元素编写文档注释
为了正确地编写API文档,你必须在每个个被导出的类、 接口、构造函数、方法和域声明之前增加一个文档注释
每一个方法的文档注释应该简洁地描述出它和客户之间的约定, 除了专门为继承而设计的类的方法是例外情况, 应该说明这个方法做了什么,而不是如何做的。 文档应该列出所有前提条件和后置条件。
6 通用程序设计
6.1 将局部变量的作用域最小化
l 使一个局部变量的作用域最小化,最有力的技术是在第一次使用它的地方声明
否则:分散读者的注意力,当变量不用时容易忘记删除
过早声明会扩大作用域
l 几乎每一个局部变量的声明都应该包含一个初始表达式
例外:当初始化方法会抛出异常时,使用try-catch时,不能带初始表达式
l 如果在循环终止之后循环变量的内容不再使用的话,for循环优先于while循环
while循环可能会因为剪切-粘贴而易出错
l 通过增加一个循环变更来提升性能
如:for (int i=0, n=list.size(); i<n; i++){doSomething(list.get(i))};
l 使方法小而集中
6.2 了解和使用库
6.2.1 使用标准库的好处
1、可以充分利用这些编写标准库的专家的知识,以及在你之前其他人的使用经验
2、不必浪费时间为工作关系不大的问题提供特别的解决方案,应重点放在应用上
3、标准库的性能会不断提高,而无需做任何努力
4、使代码融入主流,更易读,易维护,易被其他人重用
6.2.2 注意
每个程序员应该熟悉java.lang, java.util, java.io中的内容,其他按需学习
6.3 如果要求精确的答案,请避免使用float和double
6.3.1 问题
float和double类型的主要设计目标是为了科学计算和工程计算。 执行二进制浮点运算,是为了在广域的数值范围上提供较为 精确的快速近似计算而精心设计的
不适合于要求精确结果的场合,尤其不适合于货币计算
6.3.2 解决方案
使用BigDecimal、int、long替代进行计算
与原语相比,Bigdecimal不方便,而且更慢
6.4 如果其他类型更适合,则尽量避免使用字符串
l 字符串不适合代替枚举类型
l 字符串不适合代替聚集类型
l 字符串不适合代替能力表capabilities
l 常错误地用字符串来代替的类型包括原语类型、枚举类型和聚焦类型
6.5 了解字符串连接的性能
l 产生一行输出,或构造一个字符串来表示小的、大小固定的对象,使用“+”很合适,但不适合规模比较大的情形
l 在循环体内的递增的“+”赋值应用StringBuffer替代,而少数几个或在一次赋值中连接多个字符串,则可用“+”
6.6 通过接口引用对象
6.6.1 代码将更灵活,更能适应变化
6.6.2 例外
如果没有合适的接口存在,那用类而非接口更合适的
对象属于框架,而框架的基本类型是类而非接口,则用类更适合
6.7 接口优先于反射机制(reflection facility)
l 反射机制提供了“通过程序来访问关于已装载的类的信息”的能力
l 反射的代价
1、损失了编译时类型检查的好处
2、要求执行反射访问的代码非常笨拙和冗长
3、性能损失
l 在有限的情况下使用反射,付出少许代价,但可获得许多好处
l 反射机制功能强大,对于一些特定的复杂程序设计任务是必要的
l 当编写的程序要与编译时刻未知的类一起工作的话, 可仅使用反射机制来实例化对象,而用已知接口或超类来访问对象
6.8 谨慎地使用本地方法
l JDK1.3及以后,JVM的性能已经大大提高,通过本地方法提高性能已不值提倡
l 本地方法的严重缺点:
1、本地语言是不安全的,
2、平台相关的,
3、进入和退出本地代码时,也要较高的固定开销
4、本地方法编写起来单调乏味,且难以阅读
5、本地代码中的一个错误(BUG)可以破坏整个应用程序
6.9 谨慎地进行优化
6.9.1 优化格言
很多计算上的过失都被归咎于效率原因(没有获得必要的效率), 而不是其他原因——甚至包括盲目地做傻事。
不要去计较一些小的效率的得失,在97%的情况下,不成熟的优化是一切问题的根源
在优化方面,我们应该遵守两条规则: 规则1:不要做优化。 规则2(仅针对专家):还是不要做优化——也就是说, 在你还没有绝对清晰的未优化方案之前,请不要做优化。
6.9.2 优化
优化,特别是不成熟的优化,更容易带来伤害,而不是好处
不要因为性能而牺牲合理的结构,努力编写好的程序而不是快的程序
在设计过程中考虑性能,而不是在完成程序后
努力避免那些限制性能的设计决定
因素有:API、wire-level protocols、持久数据格式persistent data formats
好的API设计伴随好的性能
不要为了性能面对API进行曲改
优化之前和优化之后要对性能进行测量
程序把80%的时间花在20%的代码上
性能分析工具有助于决定应该把优化的重心放在哪里
6.10 遵守普遍接受的命名惯例
l 如果长期养成的习惯用法与此不同的话,请不要盲目遵从这些命名惯例
7 异常
7.1 只针对不正常的条件才使用异常
l 异常
因为异常机制的设计初衷是用于不正常的情形, 所以很少会有JVM实现试图对它们的性能做优化. 创建、抛出和捕获异常的开销很昂贵
把代码放在try-catch块中,反而阻止了现代JVM 实现本来可能要执行的某些优化
对数组进行遍历的标准模式并不会导致冗余的检查; 有些现代JVM实现会优化掉
基于异常的模式比标准模式要慢得多
l 异常只应被用于不正常的条件,永远不应用于正常的控制流
l 设计良好的API不应强迫客户为了正常的控制流而使用异常
l 如果类具有一个状态相关的方法,则相应的应具有一个状态检测方法
7.2 对于可恢复的条件使用被检查的异常, 对于程序错误使用运行时异常
7.2.1 异常分类
被检查的异常checked exception
运行时异常run-time exception
错误error
7.2.2 使用约定
checked exception往往是可恢复的,调用者要做处理
运行时异常用来指明程序错误
不要增加error子类
7.3 避免不必要地使用被检查的异常
l 通过增加状态检测方法减少不必要的异常
7.4 尽量使用标准的异常
7.4.1 好处
API更易于学习和使用,因为与程序员原来的习惯用法一致
使用API的程序有更好的可读性
异常越少,占用(footprint)内存越小,且装载这些类的时间开销也越小
7.4.2 常被重用的异常
IllegalArgumentException: 参数值不合适
NullPointerException: 在null被禁止的情况下参数值为null
IndexOutOfBoundsException: 下标越界
IllegalStateException: 对于这个方法调用而言,对象状态不合适
ConcurrentModificaitonException: 在禁止并发修改的情况下,对象检测到并发修改
UnsupportedOperationException: 对象不支持客户请示的方法
7.5 抛出的异常要适合于相应的抽象
7.5.1 异常转译exception translation
高层的实现应该捕获低层的异常, 同时抛出一个可以按照高层抽象进行解释的异常
异常转译也不能滥用
7.6 每个方法抛出的异常都要有文档
l 如果一个类中的许多方法出于同样的原因而抛出同一异常, 则在该类的文档注释中对该异常做文档, 而不是为每个方法单独做文档。
7.7 在细节消息中包含失败-捕获信息
l 为了捕获失败,异常字符串应包括所有”对该异常有贡献“的参数和域的值
7.8 努力使失败保持原子性
7.8.1 失败原子性(failure atomic)
一个失败的方法调用应该使对象保持”调用之前的状态“
7.8.2 方法
1、设计一个非可变的对象
方法失败后可能会阻止创建新的对象,因为永远是一致的
2、在执行操作之前检查参数的有效性
3、调整计算处理过程顺序,使得任何可能会 失败的计算部分都发生在对象状态修改之前
4、编写一段恢复代码(recovery code),由它来解释操作过程中发生的失败, 使对象回滚到开始之前的状态
适用于持久性的数据结构
5、在对象的临时拷贝上执行操作, 执行完后再将结果复制到原来的对象
7.8.3 规则
作为方法规范的一部分,任何一个异常都不应该改变对象调用该方法之前的状态, 如果违反了,则API文档应该清楚地指明对象将会处于什么样的状态
7.9 不要忽略异常
l API的设计者声明一个方法将要抛出异常时,就试图说明某些事情。
l 忽略了异常,就相当于关掉火警信号器一样,无人看到,就可能导致灾难
l 如果要忽略,至少catch块也应该包含一条说明, 用来解释为什么忽略这个异常是合适的
8 线程
8.1 对共享可变数据的同步访问
l 合用同步的唯一目的是为了通信,而不是为了互斥访问
l JAVA语言保证读或写一个除long或double类型外的变量是原子的(atomic)
l 但是不要因此而在处理多线程时不使用同步, 原子性并不能保证当一个线程写入的值另一个线程可见
l 为了线程间可靠的通信,为了互斥访问,同步是需要的。
l 延迟初始化(lazy initialization)的双重检查模式(double-check idiom)也可能导致灾难性的错误
// Double-check idiom for lazy initialization of instance fields
private volatile FieldType field;
FieldType getField() {
FieldType result = field;
if (result == null) { // First check (no locking)
synchronized(this) {
result = field;
if (result == null) // Second check (with locking)
field = result = computeFieldValue();
}
}
return result;
}
不使用延迟初始化
// 不用同步,也不用计算,getFoo方法执行最快
private static final Foo foo = new Foo();
public static Foo getFoo(){
return foo;
}
同步方法执行延迟初始化
// 可保证正常工作,但每次调用的同步开销上升。
// 在现代的JVM实现上,这个开销相对比较小,如果测试发现这个开销承受不了,则另可选用按需初始化容器类(initialize-on-demand holder class)模式非常合适
private static Foo foo = null;
public static synchronized Foo getFoo(){
if (foo==null) foo = new Foo();
return foo;
}
按需初始化容器类(initialize-on-demand holder class)模式
private static class FooHolder{
static final Foo foo = new Foo();
}
public static Foo getFoo(){ return FooHolder.foo;}
JAVA中只有当一个类被用到的时候才被初始化
l 无论何时当多个线程共享可变数据时,每个读或写数据的线程必须获得一把锁
8.2 避免过多的同步
l 为了避免死锁的危险, 在一个被同步的方法或者代码块中, 永远不要放弃对客户的控制。
也就是说:同步区域内部不要调用 可被改写的公有或受保护的方法。 以防止错误的扩展了该方法, 再创建线程而导致死锁
l 同步区域内应尽量少做工作
8.3 永远不要在循环的外面调用wait
l 总是使用wait循环模式来调用 wait方法
sychronized (obj){
while (<condition does not hold>)
obj.wait();
...// perform action appropriate to condition
}
8.4 不要依赖于线程调度器
l 任何依赖于线程调度器而达到正确性或性能要求的程序,很可能不可移植。
l 线程优先级是JAVA平台上最不可移植的特征
l 对大多数程序员来言,Thread.yield的唯一用途 是在测试期间人为地增加一个程序的并发性
8.5 线程安全性的文档化
一个类为了可被多个线程安全的使用, 必须在文档中清楚地说明它所支持的线程安全性级别
8.5.1 类的线程安全级别
非可变的immutable
线程安全的thread-safe
有条件的线程安全conditionally thread-safe
线程兼容的thread-compatible
线程对立的thread-hostile
Java中这线程对立的类和方法非常少
8.6 避免使用线程组
9 序列化
9.1 谨慎地实现Serializable
9.1.1 实现序列化的最大代价
1、一旦一个类被发布,则”改变这个的实现“的灵活性将大大降低
2、增加了错误BUG和安全漏洞的可能性
3、随着类的新版本的发行,加大了相关的测试负担
l 不要轻易的决定实现Serializable接口
l 为继承而设计的类应该很少实现Serializable接口, 接口也应该很少会扩展它
9.2 考虑使用自定义的序列化形式
l 如果没有认真考虑默认序列化是否合适,则不要接受这种形式
l 如果对象的物理表示等同于它的逻辑内容,则默认的序列化可能合适
l 即使默认的序列化形式合适,常仍要提供一 个readObject方法以保证约束关系和安全性
l 不管选择哪种序列化形式,都要为每个可序列化 的类声明一个显式的序列版本UID(serial version UID)
9.3 保护性地编写readObject方法
9.4 必要时提供一个readResolve方法