第2章 创建和销毁对象##
第1条:考虑用静态工厂代替构造器
静态工厂的优点:
- 有名字
- 不必每次调用都创建新的对象
- 可以返回原类型的任何子类型(可以于服务者提供框架,如:JDBC、SLF4J)
- 在创建参数化类型实例时代码更加简洁
静态工厂的缺点:
- 类如果不包含公有或受保护的构造器就不能被子类化(如Collections Framework中的一些实现类)
- 与其它静态方法没有区别,在API文档没有明确标识
第2条:遇到多个构造器参数时要考虑构建器
如果类的构造器或者静态工厂中具有多个参数,设计这种类时,Builder模式就是种不错的选择,特别时当大多数参数都是可选的时候。与使用传统的重叠构造器模式相比,使用Builder模式的代码将更易于阅读,构建器也比JavaBean更加安全。
第3条:用私有构造器或者枚举类强化Singleton属性
单元素的枚举类型已经成为实现Singleton的最佳方法
第4条:通过私有构造器强化不可实例化的能力
企图通过将类做成抽象类来强制该类不可实例化,这是行不通的。该类可以被子类化,并且该子类也可以被实例化。通过私有构造器可以强化不可实例化的能力:
//Noinstantiable
private class UtilityClass {
//suppress default constructor noninstantiability
private UtilityClass(){
throw AssertionError();
}
... // Remainder omitted
}
这种用法也有副作用,它使得一个类不能被子类化。
第5条:避免创建不必要的对象
- 重用对象(对象池)
- 延迟初始化(不建议,因为实现逻辑复杂)
- 为对象提供Adapter/View
- 注意基本类型和装箱基本类型的使用
- 保护性拷贝除外
第6条:消除过期的对象引用
内存泄露的来源:
- 类自己管理内存
- 缓存(可以使用 WeakHashMap 代表缓存)
- 监听器和其它回调(使用 WeakHashMap )
第7条:避免使用终结方法
终结方法的缺陷
- Java语言规范不仅不保证终结方法会被及时执行,而且根本不保证它们会被执行。不应该依赖终结方法来更新重要的持久状态。
- 终结过程中出现异常,异常会被忽略,终结过程中止。
- 终结方法有非常严重的性能损失
如果确实需要终结,使用显式的终结方法和cry-finally结构结合,如:InputStream,OutputStream上的close方法。
子类的终结方法必须手工调用超类的终结方法,最好使用终结方法守卫者
//Finalier Guardian idiom
Public clss Foo {
//Sole purpose of this object is to finalize outer Foo object
private final Object finalizerGuardian = new Object(){
@Override
protected void finalize() throw Throwable{
...//Finalize outer Foo object
}
}
...// Remaider omitted
}
第3章:对所以对象通用的方法
第8条:覆盖equals时请遵守通用约定
equals方法通用约定(等价关系):
- 自反性
- 对称性
- 传递性
- 一致性
- 非空性(Non-Nullity)
我们无法在扩展可示例化的类的同时,即增加新的值组件,同时又保留equals约定
复合优先于继承
实现高质量equals方法的诀窍:
- 使用==操作符检查“参数是否为这个对象的引用”
- 使用instanceof操作符检查“参数是否为正确的类型”
- 把参数转换为正确的类型
- 对于类中每个关键域,检查参数中的域是否与该对象中对应的域相匹配的。
- 当你写完equals方法时,编写单元测试来验证对称性,传递性,一致性
一些告诫:
- 覆盖equals时重要覆盖hashCode方法
- 不要企图让equals过于智能
- 不要将声明中的Object类型替换为其它类型
第9条:覆盖equals时总要覆盖hashCode
相等的对象必须有相等的散列码(hashCode)
为不同的对象产生不同的散列码
- 把某个非零的值,如17,保存在名为result的int型变量中。
- 对于对象中每个关键域f(指equals中涉及的域),完成以下步骤:
- a.为该域计算int类型的散列值c:
- ⅰ.如果该域是boolean类型,则计算(f?1:0)。
- ⅱ.如果该域是byte、char、short、或者int类型则计算(int)f。
- ⅲ.如果该域是long类型,则计算(int)(f^(f»>32))。
- ⅳ.如果该域是folat类型,则计算Float.floatToIntBits(f)。
- ⅴ.如果该域是double类型,则计算Double.doubelToLongBits(f),然后按步骤2.a.ⅲ, 为得到的long类型值计算散列值。
- ⅵ.如果该域是一个对象引用,并且该类的对象的equals通过递归调用equals来比较这个域, 则同样为这个域递归调用hashCode。如果需要更复杂的比较,则为这个域计算一个“范式”, 然后针对这个范式调用hashCode。如果这个域的值为null,则返回0。
- ⅶ.如果该域为一个数组,则要把每个元素当做单独的域来处理。也就是说,递归地应用以上规则, 对每个重要的元素计算一个散列码,然后根据2.b中的做法把这些散列值组合起来。如果数组域中的每个元素都很重要,可以利用发行版本1.5中增加的其中一个Arrays.hashCode方法。
- b.按照下面的公式把步骤2.a中计算的到的散列值c合并到result中:
result=32*result+c
;
- 返回result。
- 写完了hashCode方法之后,问问自己“相等的实例是否都有相等的散列码”。要编写单元测试来验证你的判断。
第10条:始终要覆盖toString
在实现toString时,必须做一个重要的决定:是否在文档中指定返回值得格式。无论你是否指定格式,都应该在文档中明确地表明你的意图。
第11条:谨慎地覆盖clone
Cloneable接口的目的是作为一个对象的一个mixin接口。遗憾的是,它并没有成功地达到这个目的。其主要原因是它缺少一个clone方法,Object中的clone方法是受保护的。不能因为一个对象实现的Cloneable方法就可以调用clone方法,因为不能保证该对象一定具有可访问的clone方法。
一下告诫:
- 如果构造器一样,clone方法不应该在构造的过程中,调用新对象中任何非final的方法(见17条)。
- 所有实现了Cloneable都有应该用一个公有的clone方法覆盖clone方法,此方法首先调用super.clone,然后修正需要修正的域。
- 另一个实现对象拷贝的好方法是提供一个拷贝构造器或者拷贝工厂。
第12条:考虑实现Comparable接口
需要注意的地方:
- compareTo方法的约定于equals方法相似。
- 为实现了Comparable接口的类添加组件是不要扩展这个类,而是要编写一个不相关的类,包含第一个类的实例,提供一个视图返回这个实例。
- 有序集合使用compareTo方法而不是equals施加等同性测试。
- 比较浮点域的时候要使用Float.compare或者Double.compare。整型相减时要注意可能的溢出。
第4章:类和接口
第13条:使类和成员的可访问性最小
一下告诫:
- 尽可能的使每个类或者成员不能被外部访问。注意实现了Serializable的接口的类可能会把私有域泄露出去。
- 实例域决不能是公有的。
- 长度非零的数组总是可变的,所以,类具有公有的静态final数组域,或者返回该域的访问方法,这几乎总是错误的。
第14条:在公有类中使用访问方法而非公有域
第15条:使可变性最小化
为了是类成为不可变类需要遵循以下5条规则:
- 不要提供任何会改变对象状态的方法。
- 保证该类不会被扩展。
- 使所有的域都是final的。
- 使所有的域都成为私有的。
- 确保对于任何可变组件的互斥访问。如果该类有指向可变对象的域,则必须保证该类的客户端无法获得这些可变对象的引用。并且,永远不要用客户端提供的对象引用来初始化这样的域。在构造器、访问方法和readObject方法中要使用保护性拷贝的技术。
为了提高性能,以上规则可以这样:没有一个方法能够对对象产生外部可见的改变。
不可变对象的优点
- 简单
- 本质上是线程安全的
- 易于共享
不可变对象的缺点
每个不同的值都需要单独的对象
第16条:复合优先于继承
继承打破了封装性,子类依赖与超类的具体实现细节。
复合和转发
复合类 -> 包装类(wrapper) -> 转发类(forwarding)
包装类不适合用于回调框架
继承的功能非常强大,但是也有诸多问题,继承破坏了封装性原则。只有当子类和超类的确存在子类型关系时,使用继承关系才是恰当的。
第17条:要么为继承而设计,并提供文档说明,要么就禁止继承
编写为继承而设计的类要注意的问题:
- 该类必须有文档说明它可覆盖方法的自用性。
- 唯一的测试方法就是编写子类
- 构造器决不能调用可被覆盖的方法
- 如果要实现Clone或者Serializable接口,要注意做一些限制
第18条:接口优先于抽象类
接口的优点:
- 现有类可以很容易被更新,以实现接口
- 接口是定义mixin(混合类型)的理性选择
- 接口允许我们构造非层次结构的类型框架
虽然接口不允许定义方法,但是通过对你导出的每一个重要接口提供一个骨架实现类(skeletal implementation),把接口和抽象类的优点结合起来。
第19条:接口只用于定义类型
常量接口是对接口的不良使用
如果要导出常量,可以有几种合理的方案:
- 如果这些常量与现有的某个接口或者类密切相关就应该把这些常量添加到这个接口或类中。
- 如果这些常量最好被看作枚举类型的成员,就应该用枚举类型来导出这些常量,否则应该使用不可实例化的工具类来导出这些常量。
第20条:类层次优先于标签类
标签类过于冗长,容易出错,并且效率低下
当你想要编写一个包含显示便签域的类时,应该考虑一下,这个标签类是否可以被取消,这个类是否可以用层次来替换。当你遇到一个包含便签域的类的时候就要考虑将它重构到一个层次结构中去。
第21条:用函数对象表示策略
第22条:优先考虑静态成员类
四种嵌套类:
- 静态成员类
- 非静态成员类(常用于定义Adapter,如Map的KeySet)
- 匿名类
- 局部类(用的最少)
如果一个嵌套类需要在单个方法之外仍然是可见的,或者它太长了,不适合放在方法内部,就应该使用成员类。如果成员类的每一个实例都需要指向一个其外围类的引用,就要把成员类做成非静态的;否则,就做成静态的。假设这个嵌套类属于一个方法内部,如果你只需要在一个地方创建实例,并且有了一个预设了类型可以说明这个类的特征,就要把它做成匿名类;否则,就做成局部类。
第5章:泛型
第23条:请不要在新代码中使用原生类型
如果使用原生类型,就失去了泛型在安全性和表述性方面的考虑
?是无限制通配符,不能将任何元素(除null外)放到Collections<?>
中。
在类文字(class literal)中必须使用原生类型(Jackson 的TypeReference)
第24条:消除非受检警告
非受检警告很重要,不要忽略它们。每一条警告都表示可能在运行时引起ClassCastException异常。要尽最大努力消除这些警告。如果无法消除非受检警告,同时可以证明引起警告的代码是类型安全的,就可以在尽量小的范围内,用@SuppressWarnings(“unchecked”)注解来禁止该警告。要用注释把禁止该警告的原因记录下来。
第25条:列表优先于数组
列表和数组的不同:
- 数组是协变的,泛型是不可变的。如果Sub是Super的子类型,Sub[]就是Super[]的子类型。List
<sub>
不是List<Super>
的子类型。 - 数组是具体化的。数组会在运行时在知道并检查它们的元素类型约束。相比之下,泛型是通过擦除来实现的。因此泛型只在编译时强化它们的类型信息,并在运行时丢弃(擦除)它们的元素类型信息。擦除就是使泛型可以与没有使用泛型的代码随意进行互用。
创建泛型数组是非法的,因为它们不是类型安全的。泛型和数组不能很好的混合起来使用。
第26条:优先使用泛型
第27条:优先使用泛型方法
类型推导
第28条:利用有限制通配符来提高API的灵活性
在API中使用通配符类型虽然比较需要技巧,但是使API变得更加灵活的多。如果编写的是将被广泛使用的类库,则一定要适当利用通配符类型。记住基本的规则:producer-extends,consumer-super(PECS)。还要记住所有的Comparable和comparator都是消费者。
public static <T extends Comparable<? super T>> T max (List<? extends T> list);
public static void swap(List<?> list,int i,int j){
swapHelper(list,i,j);
}
private static void <T> swapHelper(List<T>,int i,int j){
list.set(i.list.set(j,list.get(i)));
}
第29条:优先考虑类型安全的异构容器
集合API说明了泛型的一般用法限制你每个容器只能有固定数目的类型参数。你可以通过将类型参数放在键上而不是容器上来避开这一限制。对于这种类型安全的异构容器,可以使用Class对象作为键。以这种方式使用的Class对象被称为类型令牌。
public class Favorites{
public <T> void putFavorite(Class<T> type,T instance);
public <T> T getFavorite(Class <T> type);
}
第6章:枚举和注解
第30条:用enum代替int常量
与int常量相比,枚举类型的优势是不言而喻的。枚举类型要易读的多,也更加安全,功能更加强大。许多枚举都不需要显示的构造器或者成员,但许多其他枚举则受益于“每个常量是属性的关联”以及“提供行为受这个属性影响的方法”。只有极少数的枚举受益于将多种行为与单个方法关联。在这种相对少见的情况下,特定于常量的方法要优先于启用自有值的枚举。如果多个枚举常量同时共享相同的行为,则考虑策略枚举。 public enum Operation{ PLUS { double apple(double x,double y) { return x+y; }}
abstract double apply(double x,double y) ;
}
第31条:用实例域代替序数
用于不要根据枚举的序数导出与它相关联的值,而是要用将它保存在一个实例域中。
第32条:用EnumSet代替位域
用EnumSet代替位域是代码更加简洁、更加清楚、也更加安全。
第33条:用EnumMap代替序数索引
最好不要用序数(ordnal)来索引数组,而要用EnumMap。如果你所表示的这种关系时多维的,就使用EnumMap<...,EnumMap<...>>
。应用程序的程序员在一般情况下都不使用Enum.ordinal,即使要用也很少,因此这是一种特殊情况。
第34条:用接口模拟可伸缩的枚举
虽然无法编写可扩展的枚举类型,却可以通过编写接口以及实现该接口的基础枚举类型,对它进行模拟。这用允许客户端编写自己的枚举类型来实现接口。如果API是根据接口编写的,那么在使用基础类型的任何地方,也可以使用这些枚举。
第35条:注解优先于命名模式
命名模式的缺点:
- 文字拼写错误会导致失败
- 无法确保他们只用于相应的程序元素上
- 他们没有提供参数值与程序元素关联起来的好方法
第36条:坚持使用Override注解
Override注解可以确保你正确地覆盖了超类的方法,可以替你防止大量的错误。
第7章:方法
第38条:检查参数的有效性
对于公有方法,应该在文档中清楚指明参数的限制,并且在方法体的开头处检查参数,以强制实施这些限制,要有Java的@throw标签在文档中说明违反参数限制时会抛出的异常。非公有方法通常使用断言来检查它们的参数。
第39条:必要是进行保护性拷贝
如果类具有从客户端得到或者返回到客户端的可变组件,类就必须保护性地拷贝这些组件。如果拷贝的成本受到限制,并且信任它的客户端不会不恰当地修改组件,就在文档中说明客户端的职责是不得修改受到影响的组件,以此来代替保护性拷贝。
第40条:谨慎设计方法签名
- 谨慎地选择方法名称
- 不要过于追求便利的方法
- 避免过长的参数列表(使用分解方法、辅助类或Builder改善过长参数列表)
- 对于参数类型要优先使用接口而不是类
第41条:慎用重载
“能够重载方法”并不意味着就“应该重载方法”。一般情况下,对于多个具有相同参数数目的方法来说,应该尽量避免重载方法。在某些情况下,特别是在涉及构造器的时候要遵循这条建议也许是不可能的。在这种情况下,至少应该避免这样的情形:同一组参数只需要经过类型转换就可以被传递给不同的重载方法。如果不能避免这种情形,例如,因为正在改造一个现有的类以实现新的接口,就应该保证:当传递同样的参数时,所有重载的方法的行为必须一致。如果不能做到这一点,程序员就很难有效地使用被重载的方法或者构造器,他们就不能理解它为什么不能正常地工作。
第42条:慎用可变参数
第43条:返回零长度的数组或集合而不是null
第44条:为所有导出的API元素编写文档注释
第8章:通用程序设计
第45条:将局部变量作用域最小化
for循环优先于while循环
第46条:for-each循环优先于传统的for循环
for-each循环在简洁性和预防bug方面有着传统for循环无法比拟的优势,而且没有性能损失。
第47条:了解和使用类库
第48条:如果需要精确的答案,请避免使用float和double
对任何需要精确答案的计算任务,不要使用float或double,而要使用BigDecimal。
第49条:基本类型优先于装箱基本类型
使用装箱基本类型是要特别小心。自动装箱减少了使用装箱基本类型的繁琐性,但是并没有减少它的风险。
第50条:如果其它类型更适合,尽量避免使用字符串。
如果可以使用更加合适的数据类型,或者可以编写更加适当的数据类型,就应该避免使用字符串来表示对象。若使用不当,字符串会比其它类型更加笨拙,更加不灵活,速度更慢,也更容易出错。
第51条:当心字符串的连接性能
不要使用字符串来连接多个字符串,除非性能无关紧要。相反,应该使用StringBuilder的append方法。
第52条:通过接口引用对象
第53条:接口优先于反射机制
反射机制的缺点:
- 丧失了编译时类型检查的好处。
- 执行反射访问所需要的代码非常笨拙和冗长。
- 性能损失。
如果有可能就应该仅仅使用反射机制来实例化对象,而访问对象时则使用摸个编译时已知的接口或者超类。
第54条:谨慎地使用本地方法
使用本地方法来提高性能的做法不值得提倡
不要费力去编写快速的程序————应该努力编写好的程序,速度自然会随之而来。在设计系统的时候,特别是在设计API、线路层协议和永久数据格式的时候,一定要考虑性能因素。当构建完系统之后,测试它的性能。如果它足够快,你的任务就完成了。如果不够快,则可以在性能剖析器的帮助下,找到问题的根源,然后设法优化系统中相关的部分。第一个步骤就是检查所选择的算法;再多的底层优化也无法弥补算法的选择不当。必要时重复这个过程,在每次改变之后都要测量性能,直到满意为止。
第56条:遵守普遍接受的命名习惯
第9章:异常
第57条:只针对异常的情况才使用异常
第58条:对可恢复的情况使用受检异常,对编程错误使用运行时异常
第59条:避免不必要的使用受检异常
第60条:优先使用标准异常
IllegalArgumentException
IllegalStateException
NullPointorException
IndexOutOfBoundsException
ConcurrentModifficationException
UnspportedOperationException
第61条:抛出与抽象相应的异常
如果不能阻止或者处理来自更底层的异常,一般的做法是使用异常转译,除非底层方法碰巧尅保证它抛出的所以异常对高层也合适才可以将底层异常传播到高层。异常链对高层和底层都提供了最佳的功能:它允许抛出适当的高层异常,同时又能捕获底层的原因进行失败分析。
第62条:每个方法抛出的异常都要有文档
要为你编写的每个方法所能抛出的异常建立文档。对于未受检和受检的异常,以及对于抽象的和具体的方法都一样。要为每个受检异常提供单独的throws子句,不要为受检的异常提供throws子句。
第63条:在细节信息中包含能捕获失败的信息
为了捕获失败,异常的细节信息应该包含所有“对异常有贡献”的参数和域的值。
第64条:努力使失败保持原子性
任何异常都应该让对象保持在该方法调用之前的状态。如果违反了这条规则,API文档就应该指明对象处于什么样的状态。
第65条:不要忽略异常
第10章:并发
第66条:同步访问共享的可变数据
Java语言规范保证读或者写一个变量是原子的(atomic),除非这个变量的类型为long或者double。换句话说,独缺一个非long或者double类型的变量,可以保证值是某个线程保存在该变量中的,几时多个线程在没有同步的情况下并发第修改这个变量也是如此。
虽然语言规范保证了线程在读取原子数据的时候,不会看到任意的数据,但是它并不保证一个线程写入的值对应于另一个线程将是可见的。**为了在线程之间进行可靠的通信,也为了互斥访问,同步是必要的。**这归因与Java中的语言规范中的内存模型,它规定了一个线程所做的变化何时以及如何变成对其它线程可见。
如果读写操作都没有同步,同步就不会起作用。
增量操作符(++)不是原子性的。
当多个线程共享可变数据时,每个读或写数据的线程都必须执行同步。如果没有同步,就无法保证一个线程的修改可以被另一个线程获知。未能同步共享可变数据会造成程序的活性失败和安全性失败。这样的失败是难以调试的。他们可能是间歇性的,且与时间相关,程序的行为在不同的VM上可能根本不同。如果只需要在线程之间的交互通讯,而不是互斥访问,volatile修饰符就是一种可以接受的同步形式,但是要正确使用它要一些技巧。
第67条:避免过度同步
为了避免死锁和数据破坏,千万不要从同步区域调用外来方法。更为一般的讲,要尽量限制他不区域内部的工作量。当你在设计一个可变类时,要考虑他们是否应该自己完成同步操作。在现在这个多核的时代,这笔永远不要过度同步来得重要。只有你有足够的理由一定要在内部同步类的时候,才应该这样做,同时还应该将这个决定清楚地写在文档中。
第68条:executor和task优先于线程
第69条:并发工具优先于wait和notify
直接使用Wait和notify就像是“并发汇编语言”进行编程一样,而java.util.concurrent则提供了更高级的语言。没有理由在新的代码中使用wait和notify。即使有也是极少的。如果你在维护使用notify和wait的代码,务必确保始终利用标准的模式从while循环内部调用wait。一般情况下,你应该优先使用notifyAll,而不是notify。如果使用notify,请一定要小心,以确保程序的活性。
第70条:线程安全性文档化
一个类为了可被多个线程使用,必须在文档中清楚地说明它所支持的线程安全级别。
- 不可变
- 无条件线程安全(Random,ConcurrentHashMap)
- 有条件线程安全(Collections.synchronized包装返回的集合,它们的迭代器要求外部同步。
- 非线程安全
- 线程对立的(线程对立的根源通常在于,没有同步第修改静态数据)
第71条:慎用延迟初始化
大多数的域应该正常的初始化,而不是延迟初始化。如果为了达到性能目标,或者为了破坏有害的初始化循环,而必须延迟初始化一个域,就可以使用相应的延迟初始化方法。对于实例域,就使用双重检查模式;对于静态域就使用lazy initialization class idiom。对于可重复初始化的实例域,也可以考虑使用单重检查模式。
private static class FieldHolder{
static final FieldType field = computeFieldValue();
}
static FieldType getField(){return FieldHolder.field;}
private volatile FieldType field;
FieldType getField(){
FieldType result=field;
if(result==null){
synchronized(this){
result=field;
if(result==null){
field=result=computeFieldValue();
}
}
}
}
第72条:不要依赖于线程调度器
不要让程序的正确性依赖与线程调度器。否则,结果得到的应用程序及不健壮,也不具可移植性。
第73条:避免使用线程组
第11章:序列化
第74条:谨慎地实现Serializable接口
实现Serializable而付出的最大代价就是,一旦一个类被发布,就大大降低了“改变这个类的实现”的灵活性。
实现Serializable的第二个代价是,它增加了出现bug和安全漏洞的可能性。
实现Serializable的第三个代价是,随着类发行新的版本,相关的测试负担也增加了。
千万不要以为实现Serializable接口会很容易。除非一个类在用了一段时间之后就会被抛弃,否则,实现Serializable接口就是一个很严肃的承诺,必须认真对待。如果一个类是为了继承而设计的,则更加需要加倍小心。对于这样的类而言,在“允许子类实现Serializable接口”或“禁止子类实现Serializable接口”之间的一个折中的方案是提供一个可访问的无参构造器。这种方案允许子类实现Serializable接口。
第75条:考虑使用自定义的序列化形式
如果一个对象的物理表示法等同于它的逻辑内容,可能就适合使用默认的序列化形式。
当一个对象的物理表示法与它的逻辑数据内容有实质性的区别时,使用默认序列化形式会有以下4个缺点:
- 它是这个类的导出API永远束缚在该类的内部表示法上。
- 它会消耗过多的空间。
- 它会消耗过多的时间。
- 它会引起栈溢出。
不管你选择使用那种序列化形式,都要为自己编写的每个可序列化的类声明一个显式的序列版本UID。
第76条:保护性地编写readObject方法
ReadObject实际上相当于另一个公有的构造器。
第77条:对于实例控制,枚举类型优先于readResolve
TODO
第78条:考虑用序列化代理替代序列化实例
TODO