第1章 引言
1. 代码应该被重用,而不是被拷贝
2. 模块之间的依赖性尽可能降到最小
3. Java语言支持四种类型:接口、类、数据和基本类型
4. 类的成员由它的field、method、member class和member interface组成
5. 方法签名(signature)由它的名称和所有参数类型组成,签名不包括它的返回类型
6. 类、接口、构造器、成员以及序列化形式被统称为API元素(API Element)。导出的API由所有可在定义该API的包之外访问的API元素组成
7. 一个包的导出API(exported API)是由该包中的每个公有(public)类或者接口中所有公有的或者受保护的(protected)成员和构造器组成
第2章 创建和销毁对象
第1条:考虑用静态工厂方法代替构造器
一个类可以通过静态工厂方法而非构造器来创建其实例,例如Boolean.valueOf(boolean b)方法。需要注意的是,静态工厂方法与设计模式中的工厂方法模式不同。
1. 静态工厂方法的优势
(1)它们有名称
a. 当一个类需要多个带有相同签名的构造器时,就用静态工厂方法代替构造器,并且慎重地选择名称以便突出它们之间的区别
(2)不必在每次调用它们的时候都创建一个新对象
a. 如果程序经常请求创建相同的对象,并且创建对象的代价很高,则这项技术可以极大地提升性能
b. 借助静态工厂方法,我们可以创造实例受控的类(instance-controlled class),这样有助于类总能严格控制在某个时刻哪些实例应该存在
(3)它们可以返回原返回类型的任何子类型的对象(这样使得我们在选择返回对象的具体类型时有了更大的灵活性)
a. 该灵活性的一个应用场景是,一个API可以返回一个对象,同时又可以隐藏该对象所对应的具体类(可以是private类型的类),以这种方式隐藏实现类会使API变得非常简洁
b. 这项技术适用于基于接口的框架(interface-based framework)
c. Java Collections Framework的集合接口有32个便利实现,分别提供了不可修改的集合、同步集合等等。几乎所有这些实现都通过静态工厂方法在一个不可实例化的类(java.util.Collections)中导出。所有返回对象对应的类都是非公有的。
d. 静态工厂方法返回的对象所属的类,在编写包含该静态工厂方法的类时可以不必存在。这种灵活的静态工厂方法构成了服务提供者框架(Service Provider Framework)的基础,例如JDBC API
PS:Service Provider Framework
该框架包含四个组件:Service Interface、Provider Registration API、Service Access API和Service Provider Interface(optional),其中Service Access API就是“灵活的静态工厂”,具体可参考书籍描述
(4)在创建参数化类型实例的时候,它们使代码变得更加简洁
a. 有了静态工厂方法,编译器可以替你找到类型参数,可称之为类型推导(type inference)。例如
Map<String, List<String>> m = new HashMap<String, List<String>>();
可改进为:
Map<String, List<String>> m = HashMap.newInstance();(假设HashMap提供了"public static <K,V> HashMap<K,V> newInstance(){return new HashMap<K,V>();}"方法)
2. 静态工厂方法的缺点
(1)类如果不含公有的或者受保护的构造器,就不能被子类化
(2)它们与其他的静态方法实际上没有任何区别
a. 这导致的结果是,对于提供了静态工厂方法而不是构造器的类来说,要查明如何实例化一个类,是非常困难的
b. 通过在类或者接口注释中关注静态工厂,并遵守标准的命名习惯,可以在一定程度上弥补这一劣势
第2条:遇到多个构造器参数时要考虑用构建器
静态工厂和构造器有个共同的局限性:它们都不能很好地扩展到大量的可选参数。遇到许多构造器可选参数时,有3中解决方案
1. 重叠构造器模式
提供第一个只有必要参数的构造器,第二个构造器有一个可选参数,第三个有两个可选参数,以此类推。缺点如下:
(1)当有许多参数时,客户端代码会很难编写,且难以阅读
2. JavaBeans模式
调用一个无参构造器来创建对象,然后调用setter方法来设置每个必要的参数,以及每个相关的可选参数。缺点如下:
(1)构造过程被分到了几个调用中,在构造过程中JavaBean可能处于不一致的状态
(2)JavaBeans模式阻止了把类做成不可变(即对象一旦生成,则不能再设置其属性值,简单来说就是不提供setter方法)的可能,这就需要程序员付出额外的努力来确保它的线程安全性。
3. Builder模式
不直接生成想要的对象,而是让客户端利用所有必要的参数调用构造器(或者静态工厂),得到一个builder对象。然后客户端在builder对象上调用类似于setter的方法(可设计builder的setter方法返回builder本身,以实现链式调用),来设置每个相关的可选参数。最后,客户端调用无参的build方法来生成不可变的对象。这个builder是它构建的类的静态成员类。优点如下:
(1)使得客户端代码很容易编写,且易于阅读。builder模式模拟了具名的可选参数,就像Ada和Python中的一样
(2)Java中传统的抽象工厂实现是Class对象,用newInstance方法充当build方法的一部分。但是,Class.newInstance方法破坏了编译时的异常检查,而Builder模式则可弥补这些不足
简而言之,如果类的构造器或者静态工厂中具有多个参数,设计这种类时,Builder模式就是中不错的选择,特别是当大多数参数都是可选的时候。
第3条:用私有构造器或者枚举类型强化Singleton属性
使类成为singleton会使它的客户端测试变得十分困难,因为无法给singleton替换模拟实现,除非它实现一个充当其类型的接口。为了使Singleton类变成是可序列化的,仅仅在声明中加上“Implements Serializable”是不够的。为了维护并保证Singleton,必须声明所有实例域都是瞬时(transient)的,并提供一个新的实例。否则,每次反序列化一个序列化的实例时,都会创建一个新的实例。使用单元素的枚举类型来实现Singleton可以解决该问题
第4条:通过私有构造器强化不可实例化的能力
有时候,我们可能需要编写只包含静态方法和静态域的类。这样的类不希望被实例化,实例化对它没任何意义。如果企图通过将这样的类做成抽象类来强制该类不可被实例化,这是行不通的。因为该类可以被子类化,并且其子类也可以被实例化。那么,想编写这样的类,我们可以使用私有构造器来实现其不可实例化的特性。
第5条:避免创建不必要的对象
对于同时提供了静态工厂方法和构造器的不可变类,通常可以使用静态工厂方法而不是构造器来创建对象,以避免创建不必要的对象。除了可以重用不可变的对象之外,也可以重用那些已知不会被改变的可变对象。
1. 当你应该重用现有对象的时候,请不要创建新的对象
2. 要优先使用基本类型而不是装箱基本类型,要担心无意识的自动装箱
3. 由于小对象的构造器只做很少的显式工作,所以实际上小对象的创建和回收动作是非常廉价的。为了提升程序的清晰性、简洁性和功能性,有时候创建一些附加的小对象是件好事
4. 通过维护自己的对象池(object pool)来避免创建对象并不是一种好的做法,除非池中的对象是非常重量级的。真正正确使用对象池的典型对象示例就是数据库连接池。
第6条:消除过期的对象引用
在支持垃圾回收的语言中,内存泄漏是很隐蔽的,称这类内存泄漏为“无意识的对象保持(unintentional object retention)”更为恰当。如果一个对象引用被无意识地保留起来了,那么,垃圾回收机制不仅不会处理这个对象,而且也不会处理被这个对象所引用的所有其他对象。
1. 清空对象引用(例如,显式地将一个对象引用置为null)应该是一种例外,而不是一种规范行为
2. 一般而言,只要类是自己管理内存,那么程序员就应该警惕内存泄漏的问题。一旦元素被释放掉,则该元素中包含的任何对象引用都应该被清空。
3. 内存泄漏的另一个常见来源是缓存。一旦你把对象引用放到缓存中,它就很容易被遗忘掉。对于这个问题,有几种可能的解决方案
(1)只要在缓存之外存在对某个项的键的引用,该项就有意义,那么可以使用WeakHashMap代表缓存
当缓存中的项过期之后,它们就会自动被删除。记住,只有当所要的缓存项的生命周期是由该键的外部引用而不是由值决定时,WeakHashMap才有用处。
(2)针对“缓存项的生命周期是否有意义”并不是很容易确定,且随着时间的推移,其中的项会变得越来越没有价值的场景,应该使用一个后台线程来完成缓存清除工作。
4. 内存泄漏的第三个常见来源是监听器和其他回调。如果你实现了一个API,客户端在这个API中注册回调,却没有显式地取消注册,那么除非你采取某些动作,否则它们将会积聚。确保回调立即被当做垃圾回收的最佳方法是只保存它们的弱引用(weak reference)
第7条:避免使用finalize方法
1. finalize方法通常是不可预测的,也是很危险的,所以一般情况下不要使用
2. finalize方法的缺点在于不能保证会被及时地执行
3. java语言规范不仅不保证finalize方法会被及时地执行,而且根本就不保证它们会被执行。因此,不应该依赖finalize方法来更新重要的持久状态。
4. 使用finalize方法有一个非常严重的性能损失
5. 如果类的对象中封装的资源(例如文件或者线程)确实需要终止,应该怎么做才能不用编写finalize方法呢?
只需提供一个显式的终结方法,并要求该类的客户端在每个实例不再有用时调用这个方法。例如InputStream、java.sql.Connection的close方法以及java.util.Timer方法等等。另外,需要注意的是,显式的终结方法必须在一个私有域中记录下“该对象已经不再有效”,如果这些方法是在对象已经终止之后被调用,其他的方法就必须检查这个域,并抛出IllegalStateException异常
6. 显式的终止方法通常与try-finally结构结合起来使用,以确保及时终止
7. finalize方法有两种合法用途
(1)当对象的所有者忘记调用前面段落中建议的显式终止方法时,终结方法可以充当“安全网”。如果finalize方法中发现资源还未被终止,则应该先终止资源,并在日志中记录一条警告,表示客户端代码中存在bug
(2)用来终止非关键的本地资源
因为本地对等体(native peer)不是一个普通对象,所以垃圾回收器不会知道它,当它的java对等体被回收的时候,它不会被回收。
第3章 对于所有对象都通用的方法
第8条:覆盖equals时请遵守通用约定
1. 什么时候该覆盖Object.equals方法?
(1)类具有自己特有的“逻辑相等”概念
(2)超类还没有覆盖equals以实现期望的行为
这通常适用于“值类(value class)”的情形
2. 有时,为了满足程序员的需求,不仅必须覆盖equals方法,而且这样做也使得这个类的实例可以被用作Map的Key,或者Set的元素,使Map或者Set表现出预期的行为
3. 有一种“值类”,不需要覆盖equals方法,即用“实例受控”确保“每个值至多只存在一个对象”的类(枚举类型就属于这种类),这样的类,逻辑相同与对象等同是一回事。
4. equals方法的通用约定(equals方法实现了等价关系)
(1)自反性
要求对象必须等于其自身
(2)对称性
任何两个对象对于“它们是否相等”的问题都必须保持一致
(3)传递性
(4)一致性
如果两个对象相等,它们就必须始终保持相等,除非它们中有一个对象被修改了。无论类是否是不可变的,都不要使equals方法依赖于不可靠的资源
(5)非空性
所有对象都必须不等于null。在进行类型转换之前,equals方法必须使用instanceof操作符,检查其参数是否为正确的类型。如果instanceof的第一个操作数是null,那么,不管第二个操作数是哪种类型,instanceof操作符都指定应该返回false[JLS, 15.20.2]
5. 实现高质量equals方法的诀窍
(1)使用==操作符检查“参数是否为这个对象的引用”,如果是,则返回true
(2)使用instanceof操作符检查“参数是否为正确的类型”,如果不是,则返回false
(3)把参数转换成正确的类型。因为转换之前进行过instanceof测试,所以确保会成功
(4)对于该类中的每个“关键(significant)”域,检查参数中的域是否与该对象中对应的域相匹配。如果这些测试全部成功,则返回true
(5)当你编写完成了equals方法之后,应该问自己三个问题:它是否是对称的、传递的、一致的?
(6)覆盖equals时总要覆盖hashCode方法
(7)不要企图让equals方法过于智能
(8)不要将equals声明中的Object对象替换为其他的类型(因为这样实际上并没有覆盖Object.equals方法,使用@Override注解可以防止犯这种错误)
第9条:覆盖equals时总要覆盖hashCode
1. 相等的对象必须具有相等的hash code
(1)HashMap中有一项优化,可以将与每个项相关联的散列码缓存起来,如果散列码不匹配,也就不必检验对象的等同性
实际上,hashMap的hash桶的定位就是根据hash code来的
(2)一个好的散列函数通常倾向于“为不相等的对象产生不相等的散列码”
2. 一个好的散列码生成策略
(1)把某个非零的常数值,比如说17,保存在一个名为result的int类型的变量中
(2)对于对象中每个关键域f(指equals方法中涉及的每个域),完成以下步骤
a. 为该域计算int类型的散列码c
i. boolean类型,则计算(f?1:0)
ii. byte、char、short或者int类型,则计算(int)f
iii. long类型,则计算(int)(f^(f>>>32))
iv. float类型,则计算Float.floatToIntBits(f)
v. double类型,则计算Double.doubleToLongBits(f),然后按照步骤iii为得到的long类型值计算散列值
vi. 对象引用,并且该类的equals方法通过递归地调用equals的方式来比较这个域,则同样为这个域递归地调用hashCode
vii. 数组,则要把每一个元素当做单独的域来处理
b. 按照下面的公式,把a.中计算得到的散列码c合并到result中
result = 31*result+c; 其中result的初始值随机选定的,而这里的31,则是因为它是一个奇素数。如果使用偶数,并且乘法溢出的话,信息就会丢失,因为与2相乘等价于移位运算;使用素数的好处则不是很明显,但是习惯上都使用素数来计算散列结果;最后,31还有个很好的特性,即用移位和减法来代替乘法,可以得到更好的性能:31*i == (i<<5)-i,现代的vm可以自动完成这种优化
(3)返回result值
(4)写完了hashCode方法之后,问问自己“相等的实例是否都具有相等的hashCode”
3. 在散列码的计算过程中,可以把冗余域(redundant field)排除在外,所谓冗余域即为那些可以参与其他域值计算出来的域
4. 必须排除equals比较计算中没有用到的任何域
5. 如果一个类是不可变的,并且计算散列码的开销也比较大的话,就应该考虑把散列码缓存在对象内部,而不是每次请求的时候都重新计算散列码
6. 不要试图从散列码计算中排除掉一个对象的关键部分来提高性能
7. java平台类库中的许多类,比如String, Integer和Date,都可以把它们的hashCode方法返回的确切值规定为该实例的一个函数。一般来说,这并不可取,因为这样做严格限制了将来的版本中改进散列函数的能力
第10条:始终要覆盖toString
1. toString的通用约定指出,被返回的字符串应该是一个“简洁的,但信息丰富,并且易于阅读的表达形式”
2. 提供好的toString实现可以使类用起来更加舒适
当对象被传递给println、printf、字符串联操作符(+)以及assert或者被调试器打印出来时,toString方法会被自动调用
3. 在实际应用中,toString方法应该返回对象中包含的所有值得关注的信息
4. 在实现toString方法时,必须做一个重要的决定:是否要在文档中注明返回值的格式
(1)指定格式的优点:可以被用作一种标准的、明确的、适合人阅读的标准表示法
如果你指定了格式,最好再提供一个相匹配的静态工厂或者构造器,以便程序员可以很容易地在对象及其字符串表示法之间来回转换
(2)指定格式的缺点:如果这个类已经被广泛使用,那么一旦指定了格式,那么就必须始终如一地坚持这种格式
5. 无论你是否决定指定格式,都应该在文档中明确地表明你的意图
6. 无论是否指定格式,都应该为toString返回值中所包含的所有信息,提供一种编程式的访问途径
第11条:谨慎地覆盖clone
1. Cloneable接口的目的是做为对象的一个mixin interface,表明该对象允许克隆
如果一个类实现了Cloneable接口,那么Object的clone方法就返回该对象的逐域拷贝,否则就会抛出CloneNotSupportedException。Cloneable接口不包含任何方法,它改变了超类中受保护的方法的行为,这是一种极端非典型的做法,并不值得效仿
2. 如果实现Cloneable接口是要对某个类起作用,类和它的所有超类都必须遵守一个相当复杂的、不可实施的,并且基本上没有文档说明的协议。由此得到一种语言之外的机制(extralinguistic):无须构造器也能创建对象实例
3. Clone方法的通用约定是非常弱的
(1)x.clone() != x
(2)x.clone().getClass() == x.getClass()
(3)x.clone().equals(x)
但是,以上约定是没有绝对要求的。
4. 如果你覆盖了一个非final类的clone方法,则应该返回一个通过调用super.clone方法而得到的对象
5. 永远不要让客户去做那些类库可以替客户完成的事情
6. 实际上,clone方法就是另一个构造器,你必须确保它不会伤害到原始对象,并确保正确地创建被克隆对象中的约束条件(invariant)
7. clone架构与引用可变对象的final域的正常用法是不兼容的
8. 克隆对象的另一种方法是,调用super.clone(),然后将创建的对象的所有域置为空白状态,再调用高层方法来重新设置对象的状态
9. 如同构造器一样,clone方法不应该在构造的过程中,调用新对象中的任何非final方法
如果clone调用了一个被覆盖的方法,那么在该方法所在的子类有机会修正它在克隆对象中的状态之前,该方法就会先执行(?)
10. 如果你决定用线程安全的类来实现Cloneable接口,那么就要记得它的clone方法必须得到很好的同步
11. 简而言之,所有实现了Cloneable接口的类都应该用一个公有的方法覆盖clone。此公有方法实现步骤如下
(1)调用super.clone()
(2)修正任何需要修正的域
(3)拷贝任何包含内部“深层结构”的可变对象,并用新对象的引用代替原来指向这些对象的引用(深克隆)
12. 如果你扩展了一个实现了Cloneable接口的类,那么除了实现一个行为良好的clone方法之外,没有别的选择
13. 另一个实现对象拷贝的好办法是提供一个拷贝构造器(copy constructor)或拷贝工厂(copy factory)
(1)拷贝构造器
例如 public Yum(Yum yum)
(2)拷贝工厂
例如 public static Yum new Instance(Yum yum)
拷贝构造器与拷贝工厂相对于clone方法而言,有诸多的优势
14. 由于Cloneable具有诸多问题,因此为了继承而设计的类不应该去实现该接口,也不应该去覆盖clone方法
15. 一个专门为了继承而设计的类,如果你未能提供行为良好的protected clone方法,那么其子类就不可能实现Cloneable接口
第12条:考虑实现Comparable接口
1. 类实现了Comparable接口,就表明它的实例具有内在的排序关系
2. 实际上,java平台类库中的很多值类都实现了Comparable接口
3. 如果你正在编写一个值类,并且它具有非常明显的内在排序关系,比如按字母顺序、按数值顺序或者按年代顺序,那你应该坚决考虑实现该接口
4. 与equals不同的是,当跨越不同类的时候,comparaTo可以不做比较,直接抛出ClassCastException
5. 就像违反了hashCode约定的类会破坏其他依赖于hash算法的类一样,违反compareTo约定的类也会破坏其他依赖于比较关系的类
依赖于比较关系的类包括有序集合类TreeSet和TreeMap,以及工具类Collections和Arrays,它们内部包含有搜索与排序算法
第4章 类和接口
第13条:使类和成员的可访问性最小化
1. 设计良好的模块会隐藏所有的实现细节,把它的API与它的实现清晰地隔离开来。这个概念被称为信息隐藏或封装。信息隐藏可以有效地对模块进行解耦
2. java中提供了许多机制来协助信息隐藏
(1)访问控制机制
该机制决定了类、接口和成员的可访问性
3. 尽可能地使每个类或成员不被外界访问
如果类或者接口能够被做成包级私有的,那么它就应该被做成包级私有的。通过把类或者接口做成包级私有的,它实际上成为这个包的实现的一部分,而不是该包的导出API的一部分
4. 公有的类或者接口是包的导出API的一部分,类或者接口一旦被导出,你就有责任永远支持它,以保持它们的兼容性
5. 一个类的成员有四种
(1)域
(2)方法
(3)嵌套类
(4)嵌套接口
6. 受保护的成员是类的导出API的一部分,必须永远得到支持
7. 子类中的访问级别不允许低于超类中的访问级别,这样可以确保任何可以使用超类实例的地方也都可以使用子类实例
8. 接口中的所有域和方法都只能是public的,且域都是static的(即便没有显式定义static)
9. 为了便于测试,你可以试着使类、接口或者成员变得更容易访问。但是不能为了测试,而将类、接口或者成员变成包的导出API的一部分
10. 包含公有可变可变域的类并不是线程安全的
11. 如果final域包含可变对象的引用,那么它便具有非final域的所有缺点
12. 使用final来修饰一个数组,几乎是没意义的,而且是安全漏洞的一个常见根源,因为数组的内容是可以被任意改变的
第14条:在公有类中使用访问方法而非公有域
1. 如果类可以在它所在的包的外部进行访问,就提供访问方法,以保留将来改变该类的内部表示法的灵活性
2. 如果公有类暴露了它的数据域,那么将来改变其内部表示法是不可能的
3. 如果类是包级私有的,或者是私有的嵌套类,那么直接暴露它的数据域并没有本质的错误
4. 总之,公有类永远都不应该暴露可变的域,但是,有时候会需要用包级私有的或者私有的嵌套类来暴露域
第15条:使可变性最小化
1. 不可变类指的是其实例不能被修改的类,为了使类成为不可变,必须要遵循以下5条规则
(1)不提供任何会修改对象状态的方法(也成为mutator)
(2)保证类不会被扩展
(3)使所有的域都是final的
(4)使所有的域都成为私有的
(5)确保对于任何可变组件的互斥访问
如果类具有指向可变对象的域,则必须确保该类的客户端无法获得指向这些对象的引用。并且永远不要使用客户端提供的对象引用来初始化这样的域,也不要从任何访问方法(accessor)中返回该对象引用。在构造器、访问方法和readObject方法中要使用保护性拷贝技术(defensive copy)
2. 不可变类存在的意义:不可变类比可变类更加易于设计、实现和使用,它们不容易出错,且更加安全
3. 不可变对象可以只有一种状态,即被创建时的状态,可变对象则可以有任意复杂的状态空间
4. 不可变对象本质上是线程安全的,它们不要求同步,因此它可以被自由地共享
5. 不可变的类可以提供一些静态工厂,它们把频繁被请求的实例缓存起来
6. 在设计新的类时,选择用静态工厂代替公有的构造器可以让你以后有添加缓存的灵活性,而不必影响客户端
7. 不可变类真正唯一的缺点是,对于每个不同的值都需要一个单独的对象。不过,可以使用可变配套类(companing class)来弥补该缺点
(1)在java平台类库中,可变配套类的方法的主要例子是String类,它的可变配套类是StringBuilder
8. 让不可变的类变成final的另一种方法是,让类的所有构造器都变成私有或者包级私有的,并添加公有的静态工厂(static factory)来代替公有的构造器
(1)注意,工厂方法并不一定要单独封装到一个工厂类中,可以直接以静态工厂的方式存在于“产品”类中(o)
9. 值得注意的是,由于历史原因,BigInteger和BigDecimal这两个不可变类并没有被声明为final。因此,如果你在编写一个类,它的安全性依赖于不可信客户端传递进来的BigInteger或者BigDecimal参数的话,就必须进行检查,以确保这个参数是“真正”的BigInteger或者BigDecimal,而不是不可信任的子类的实例。如果是后者,则必须在假设它可能是可变的前提下对它进行保护性拷贝
10. 实际上,有时候为了性能,我们会对不可变类的诸多规则有所放松。例如,许多不可变类拥有一个或多个非final域,它们在第一次被请求执行这些计算的时候,把一些开销昂贵的计算结果缓存在这些域中。
11. 如果你选择让自己的不可变类实现Serializable接口,并且它包含一个或者多个指向可变对象的域,就必须提供一个显式的readObject或者readResolve方法,或者使用ObjectOutputStream.writeUnshared的ObjectInputStream.readUnshared方法,即使默认的序列化形式是可以接受的,也是如此,否则攻击者可能从不可变的类创建可变的实例。
12. 坚决不要为每个get方法编写一个相应的方法,除非有很好的理由要让类成为可变的类,否则就应该是不可变的。
13. 不可变类有许多优点,唯一的缺点就是在特定的情况下存在潜在的性能问题
14. 构造器应该创建完全初始化的对象,并建立起所有的约束关系。
15. 不要在构造器或者静态工厂之外再提供公有的初始化方法。同样的也不应该提供“重新初始化”方法,因为与所增加的复杂性相比,“重新初始化”方法通常并没有带来太多的性能优势
第16条:复合优先于继承
1. 在包的内部使用继承是非常安全的,然而,对普通的具体类进行跨越包边界的继承,则是非常危险的。
2. 与方法调用不同的是,继承打破了封装性。换句话说,子类依赖于超类中特定功能的实现细节,如果超类随着发行版本的变化而变化,那么子类可能会遭到破坏。
3. 注意,类A包含两个方法,分别是方法a和方法b(其中a方法调用了b方法),如果类B继承了类A,且覆盖了类A的方法a和方法b,在B.a()中调用了super.a(),那么如果main方法中调用了B.a(),则最终的调用链会是:main()——>B.a()——>A.a()——>B.b()(而非A.b())
4. 导致子类脆弱的一个相关的原因是,它们的超类在后续的发行版本中可以获得新的方法
5. 复合设计可以避免上面提到的因继承而引起的问题,它不依赖于组件类的实现细节,即使组件类添加了新的方法,也不会影响到引用类
6. 有时候,复合和转发的结合(即复合模式/decorator模式)也被错误地称为“委托(delegation)”。从技术的角度而言,这不是委托,除非包装对象把自身传递给被包装的对象
7. 包装类几乎没有什么缺点,但是需要注意的是,包装类不适合用在回调框架中
8. 只有当子类真正是超类的子类型时(即存在“is-a”关系),才适合用继承
9. 继承机制会把超类API中的所有缺陷传播到子类中,而复合则允许设计新的API来隐藏这些缺陷
10. 如果子类和超类处在不同的包中,并且超类并不是为了继承而设计的,那么继承将会导致脆弱性(fragility)。为了避免这种脆弱性,可以用复合和转发机制来代替继承,尤其是当存在适当的接口可以实现包装类的时候
11. 包装类不仅比子类更加健壮,而且功能也更加强大
第17条:要么为继承而设计,并提供文档说明,要么就禁止继承
1. 对于不是为了继承而设计,并且没有文档说明的“外来”类进行子类化是很危险的
2. 为了继承而设计的类,必须提供文档,说明它的可覆盖方法的自用性(self-use)。对于每个公有的或者受保护的方法,它的文档必须指明该方法调用了哪些可覆盖的方法
3. 好的API文档应该描述一个给定的方法做了什么工作,而不是描述它是如何做到的
4. 为了使程序员能够编写出更加有效的子类,而无需承受不必要的痛苦,类必须通过某种形式提供适当的钩子(hook),以便能够进入到它的内部工作流程中,这种形式可以是精心选择的受保护的方法,也可以是受保护的域,后者比较少见
5. 对于为了继承而设计的类,唯一的测试方法就是编写子类。经验表明,3个子类通常就足以测试一个可扩展的类
6. 为了允许继承,类还必须遵守其他一些约束
(1)构造器决不能调用可被覆盖的方法,无论是直接调用还是间接调用
7. 在为了继承而设计类的时候,Cloneable和Serializable接口出现了特殊的困难,因为它们把一些实质性负担转嫁到了扩展这个类的程序员的身上
8. 如果你决定在一个为了继承而设计的类中实现Cloneable或者Serializable接口,就应该意识到,因为clone和readObject方法在行为上非常类似于构造器,所以类似的限制规则也是适用的:无论是clone还是readObject,都不可以调用可覆盖的方法,不管是以直接还是间接的方式。
9. 对于那些并非为了安全地进行子类化而设计和编写文档的类,要禁止子类化,有两种办法
(1)把这个类声明成final
(2)把所有构造器都变成私有的,或者包级私有的,并增加一些公有的静态工厂来替代构造器
10. 你可以机械地消除类中可覆盖方法的自用特性,而不改变它的行为。将每个可覆盖方法的代码体移到一个私有的“辅助方法(helper method)”中,并且让每个可覆盖的方法调用它的私有辅助方法。然后,用“直接调用可覆盖方法的私有辅助方法”来代替“可覆盖方法的每个自调用”
第18条:接口优于抽象类
1. 现有的类可以很容易被更新,以实现新的接口
2. 接口是定义mixin类型的理想选择
mixin是指这样的类型,类除了实现它的“基本类型”之外,还可以实现这个mixin类型,以表明它提供了某些可供选择的行为
3. 接口允许我们构造非层次结构的类型框架
4. 通过包装类模式,接口使得安全地增强类的功能成为可能
5. 通过对你导出的每个重要接口都提供一个抽象的骨架实现(skeletal implementation)类,可以把接口和抽象类的优点结合起来。接口的作用仍然是定义类型,但是骨架实现类接管了所有与接口实现相关的工作。如果设计得当,骨架实现可以使程序员很容易提供他们自己的接口实现
6. 骨架实现类与静态工厂的结合可以使得我们无需通过继承来复用骨架类
7. 骨架实现的美妙之处在于,它们为抽象类提供了实现上的帮助,但又不强加“抽象类被用作类型定义时”所特有的严格限制
8. 骨架类仍然能够有助于接口的实现
实现了这个接口的类可以把对于接口方法的调用,转发到一个内部私有类的实例上,这个内部私有类扩展了骨架实现类。这种方法被称作模拟多重继承(simulated multiple inheritance)
9. 编写骨架实现类相对比较简单
(1)认真研究接口,并确定哪些方法是最为基本的(primitive),其他的方法则可以根据它们来实现
(2)将(1)中确认的基本方法设计为抽象方法
(3)为接口中剩余的所有方法提供具体的实现
10. 使用抽象类来定义允许多个实现的类型,与使用接口相比有一个明显的优势:抽象类的演变要比接口的演变容易得多。简单来说,即抽象类中新增一个方法要比接口中新增一个方法来得容易
11. 设计公有的接口要非常谨慎,因为接口一旦被公开发行,并且已被广泛实现,再想改变这个接口几乎是不可能的
12. 如果你导出了一个重要的接口,就应该坚决考虑同时提供骨架实现类
第19条:接口只用于定义类型
1. 常量接口模式是对接口的不良使用
所谓常量接口,即那些没有包含任何方法,只包含静态的final域的接口。如果非final类实现了常量接口,它的所有子类的命名空间也会被接口中的常量所污染
2. 导出常量的几种合理方案
(1)如果这些常量与某个现有的类或者接口紧密相关,就应该把这些常量添加到这个类或者接口中
(2)如果这些常量最好被看作枚举类型的成员,就应该用枚举类型来导出这些常量
(3)如果排除了(1)(2)两种情况,那么就应该使用不可实例化的工具类来导出这些常量
工具类通常要求客户端要用类名来修饰这些常量名,如果大量利用工具类导出的常量,可以利用静态导入机制来避免用类名来修饰常量名
第20条:类层次优于标签类(tagged class)
1. 所谓标签类,就是那些带有两种甚至更多种风格的实例的类,并包含表示实例风格的标签域
2. 标签类有着许多缺点,它们中充斥着样板代码,包括枚举声明、标签域以及条件语句,由于多个实现乱七八糟地挤在单个类中,破坏了可读性
3. 无法给标签类添加风格,除非可以修改它的源文件
4. 标签类过于冗长、容易出错,并且效率低下
5. 标签类正是类层次的一种简单的仿效
6. 类层次的另一种好处在于,它们可以用来反映类型之间本质上的层次关系,有助于增强灵活性,并进行更好的编译时类型检查
7. 当你遇到一个包含标签域的现有类时,就要考虑将它重构到一个层次结构中去
第21条:用函数对象表示策略
1. 有些语言支持函数指针(function pointer)、代理(delegate)、lambda表达式,或者支持类似的机制,允许程序把“调用特殊函数的能力”存储起来并传递这种能力。这种机制通常用于允许函数的调用者通过传入第二个函数,来指定自己的行为
2. java没有提供函数指针,但是可以用对象引用实现同样的功能
3. 如果一个类仅仅导出这样的一个方法,它的实例实际上就等同于一个指向该方法的指针。这样的实例被称为函数对象(function object)
4. 一个具体策略类是无状态的
5. 一个具体策略类的实例可以看做是一个函数对象
6. 带泛型的类或接口只是一种类模板而已,具体使用时是需要指定泛型所代表的具体类型的
7. 我们在设计具体策略类时,还需要定义一个策略接口
8. 具体的策略类往往使用匿名类声明
9. 因为策略接口被用作所有具有具体策略实例的类型,所以我们并不需要为了导出具体策略,而把具体策略类做成公有的。具体的策略类可以是宿主类(host class)的私有嵌套类
10. 函数指针的主要用途就是实现策略模式
为了在java中实现这种模式,要声明一个接口来表示该策略,并且为每个具体策略声明一个实现了该接口的类。当一个具体策略类只被使用一次时,通常使用匿名类来声明和实例化这个具体策略类。当一个具体策略类是设计用来重复使用的时候,它的类通常就要被实现为私有的静态成员类,并通过公有的静态final域来导出,其类型为该策略接口
第22条:优先考虑静态成员类
1. 嵌套类(nested class)是指被定义在另一个类的内部的类。嵌套类存在的目的应该只是为它的外围类(enclosing class)提供服务
2. 嵌套类有四种
(1)静态成员类
(2)非静态成员类
(3)匿名类
(4)局部类
除了第一种以外,其他三种都被成为内部类(inner class)
3. 静态成员类是最简单的一种嵌套类,最好把它看做是普通的类,只是碰巧被声明在另一个类的内部而已,它可以访问外围类的所有成员,包括那些声明为私有的成员
4. 静态成员类的一种常见用法是作为公有的辅助类,仅当与它的外部类一起使用时才有意义
5. 静态成员类与非静态成员类的区别
(1)非静态成员类的每个实例都隐含着与外围类的一个外围实例(enclosing instance)相关联
(2)非静态成员类的实例方法内部,可以调用外围实例上的方法,或者利用修饰过的this构造获得外围实例的引用
(3)如果嵌套类的实例可以在它外围类的实例之外单独存在,这个嵌套类就必须是静态成员类。因为在没有外围实例的情况下,要想创建非静态成员类的实例是不可能的
6. 当非静态成员类的实例被创建的时候,它和外围实例之间的关联关系也随之被建立起来
7. 非静态成员类的一种常见用法是定义一个Adapter,它允许外部类的实例被看作是另一个不相关的类的实例,例如Map接口的实现往往使用非静态成员类来实现它们的集合视图(collection view)
8. 在宿主类的外围实例化非静态成员类的语法
enclosingInstance.new MemberClass(args)
9. 如果声明成员类不要求访问外围实例,就要始终把static修饰符放在它的声明中,使它成为静态成员类
10. 如果在没有外围实例的情况下,也需要分配实例,就不能使用非静态成员类,因为非静态成员类的实例必须要有一个外围实例
11. 私有静态成员类的一种常见用法是用来代表外围类所代表的对象的组件
12. 匿名类不是外围类的一个成员,它并不与其他的成员一起被声明,而是在使用的同时被声明和实例化。
13. 当且仅当匿名类出现在非静态的环境中时,它才有外围实例
14. 匿名类可以出现在代码中任何允许存在表达式的地方
15. 匿名类的三种常见用法
(1)动态地创建函数对象(function object)
(2)创建过程对象(process object),例如Runnable、Thread、TimerTask实例
(3)在静态工厂方法的内部
16. 局部类是四种嵌套类中用得最少的类,在任何“可以声明局部变量”的地方,都可以声明局部类
17. 与匿名类一样,只有当局部类是在非静态环境中定义的时候,才有外围实例,它们也不能包含静态成员
第5章 泛型
第23条:请不要在新代码中使用原生态类型
1. 声明中具有一个或者多个类型参数(type parameter)的类或者接口,就是泛型(generic)类或者接口
2. 泛型类或者接口统称为泛型
3. 每种泛型定义一组参数化的类型
4. 每个泛型都定义一个原生态类型(raw type),即不带任何实际类型参数的泛型名称
例如,与List<E>相对应的原生态类型是List
5. 如果使用原生态类型,就失掉了泛型在安全性和表述性方面的所有优势
6. 泛型有子类型化(subtyping)的规则,List<String>是原生态类型List的一个子类型,而不是参数化类型List<Object>的子类型
7. 无限制的通配符类型(unbounded wildcard type)
如果使用泛型,但不确定或者不关心实际的类型参数,就可以使用一个问号代替(例如,Set<?>读作“某个类型的集合”)
8. 泛型信息可以在运行时被擦除
9. 在class literal中必须使用原生态类型
(1)List<String>.class和List<?>.class都是非法的
(2)在参数化类型上使用instanceof操作符是非法的
第24条:消除非受检警告
1. 要尽可能地消除每一个非受检警告
如果消除了所有警告,就可以确保代码是类型安全的
2. 如果无法消除警告,同时可以证明引起警告的代码是类型安全的
在这种情况下,则可以用一个@SuppressWarnings("unchecked")注解来禁止这条警告,SuppressWarnings注解可以用在任何粒度的级别中,从单独的局部变量声明到整个类都可以
3. 每当使用SuppressWarnings("unchecked")注解时,都要添加一条注释,说明为什么这么做是安全的
第25条:列表优先于数组
1. 数组与泛型相比,有两个重要的不同点
(1)数组是协变的(covariant)
如果sub为super的子类型,那么数组类型sub[]就是super[]的子类型;相反,List<sub>则不是List<super>的子类型
(2)数组是可具体化的(reified)
数组会在运行时才知道并检查它们的元素类型约束;相反,泛型则是通过擦除(erasure)来实现的,因此泛型只在编译时强化它们的类型信息,并在运行时丢弃它们的元素类型信息
2. 创建泛型、参数化类型或者类型参数的数组都是非法的,因为泛型数组不是类型安全的
3. 不可具体化的类型(non-reifiable type)是指其运行时表示法包含的信息比它的编译时表示法包含的信息更少的类型
4. 唯一可具体化的参数化类型是无限制的通配符类型
5. 在结合使用可变参数(varargs)方法和泛型时会出现令人费解的警告
这是由于每当调用可变参数方法时,就会创建一个数组来存放varargs参数
6. 当你得到泛型数组创建错误时,最好的解决办法通常是优先使用集合类型List<E>,而不是参数类型E[]
7. 记住,元素类型信息会在运行时从泛型中被擦除
第26条:优先考虑泛型
1. 一旦你证明了未受检的转换是安全的,就要在尽可能小的范围中禁止警告
2. 解决泛型数组创建错误有两种方案
(1)创建一个Object数组,并将它转换成泛型数组
E[] elements = (E[])new Object[3];
(2)直接声明Object数组,然后将从数组中获取到的元素转换成E
Object[] elements = new Object[3];
E result = (E) elements[0];
3. java并不是生来就支持列表,因此有些泛型如ArrayList,则必须在数组上实现。另外,为了提升性能,其他泛型如HashMap,也是在数组上实现的
4. 有限制的类型参数
<E extends OutputStream>,要求实际的类型参数必须是OutputStream的一个子类型
5. 使用泛型比使用需要在客户端代码中进行转换的类型来得更加安全,也更加容易
第27条:优先考虑泛型方法
1. 静态工具方法尤其适合于泛型化
2. 声明类型参数的类型参数列表,处在方法的修饰符及其返回类型之间
3. 泛型方法的一个显著特性是,无需明确指定类型参数的值。编译器通过检查方法参数的类型来计算类型参数的值,这个过程称作类型推导
4. 可以利用泛型方法调用所提供的类型推导,使创建参数化类型实例的过程变得更加轻松
5. 在调用泛型构造器的时候,要明确传递类型参数的值可能有点小麻烦,因为类型参数需要出现在变量声明的左右两边,显得有些冗余,对于这种情况,可以使用泛型静态工厂方法来解决
6. 虽然相对少见,但是通过某个包含该类型参数本身的表达式来限制类型参数是允许的(如<T extends Comparable<T>>),这就是递归类型限制(recursive type bound)
7. 泛型方法就像泛型一样,使用起来比要求客户端转换输入参数并返回值的方法来得更加安全,也更加容易
8. 就像类型一样,你应该确保新方法可以不用转换就能使用,这通常意味着要将它们泛型化
第28条:利用有限制通配符来提升API的灵活性
1. 利用有限制的通配符类型来解决参数化类型的子类型问题
例如Iterable<Integer>是不能应用于pushAll(Iterable<E> xx)方法中的,但是可以将pushAll方法设计成pushAll(Iterable<? extends E> xx)来解决此问题
2. PECS助记符突出了使用通配符类型的基本原则
(1)producer-extends (<? extends T>)
(2)consumer-super(<? super T>)
3. 不要用通配符类型作为返回类型
4. 如果使用得当,通配符类型对于类的用户来说几乎是无形的
5. 如果编译器不能推断你希望它拥有的类型,可以通过一个显式的类型参数(explicit type parameter)来告诉它要使用哪种类型
6. 类型参数和通配符之间具有双重性,许多方法都可以利用其中一个或者另一个进行声明
7. 如果类型参数只在方法声明中出现一次,就可以用通配符取代它
如果是无限制的类型参数,就用无限制的通配符取代它;如果是有限制的类型参数,就用有限制的通配符取代它
8. 在API中使用通配符类型虽然比较需要技巧,但是使API变得灵活得多
第29条:优先考虑类型安全的异构容器
1. 泛型最常用于集合,如Set和Map,以及单元素的容器,如ThreadLocal、Atomic和Reference
2. 将key进行参数化而不是将container进行参数化,可以实现以类型安全的方式访问数据库的所有列
3. 当一个类的字面文字被用在方法中,来传达编译时和运行时的类型信息时,就被称作type token
4. 类型安全的异构容器
例如:
public class Favorites{
public <T> void putFavorite(Class<T> type, T instance);
public <T> void getFavorite(Class<T> type);
}
5. 利用Class的cast方法,可以将对象引用动态地转换成Class对象所表示的类型
6. Favorites类有两种局限性值得注意
(1)恶意的客户端可以很轻松地破坏Favorites实例的类型安全,只要以它的原生态形式使用Class对象
(2)它不能用在不可具体化(non-reifiable)的类型中(因为我们无法为List<String>获得一个Class对象,List<String>.Class是个语法错误)
7. 注解API广泛地利用了有限制的类型令牌(类型令牌如Class<T>)
8. 被注解的元素本质上是个类型安全的异构容器,容器的键属于注解类型
9. Class类的asSubClass方法,可以将调用它的Class对象转换成用其参数表示的类的一个子类
10. 集合API说明了泛型的一般用法,限制你每个容器只能有固定数目的类型参数。你可以通过将类型参数放在键上而不是容器上来避开这一限制
11. 对于类型安全的异构容器,可以用Class对象作为键。以这种方式使用的Class对象称作类型令牌(type token)。你也可以使用定制的键类型
第6章 枚举和注解
第30条:用enum代替int常量
1. int枚举模式(int enum pattern)存在诸多不足,它在类型安全性和使用方便性方面没有任何帮助
2. java的枚举本质上是int值
java枚举类型背后的基本想法非常简单:它们就是通过公有的静态final域为每个枚举常量导出实例的类
3. 因为没有可以访问的构造器,所以枚举类型是真正的final
4. 枚举提供了编译时的类型安全
5. 为了将数据与枚举常量关联起来,得声明实例域,并编写一个带有数据并将数据保存在域中的构造器
6. 枚举天生就是不可变的,因此所有的域都应该为final的
7. 除非迫不得已要将枚举方法导出至它的客户端,否则都应该将它声明为私有的
8. 如果一个类具有普遍适用性,它就应该成为一个顶层类;如果它只是被用在一个特定的顶层类中,它就应该成为该顶层类的一个成员类
9. 特定于常量的方法实现(constant-specific class body),是将不同的行为与每个枚举常量关联起来的一种很好的方式
public enum Operation{
PLUS{double apply(double x, double y){return x+y;}},
MINUS{double apply(double x, double y){return x-y;}};
abstract double apply(double x, double y);
}
10. 枚举类型中的抽象方法必须被它所有常量中的具体方法所覆盖
11. 枚举类型有一个自动产生的valueOf(String)方法,它将常量的名字转变成变量本身
12. 如果在枚举类型中覆盖toString,要考虑编写一个fromString方法,将定制的字符串表示法变回相应的枚举
13. 特定于常量的方法有一个美中不足的地方,就是它们使得在枚举常量中共享代码变得更加困难了
14. 枚举中的switch语句适合于给外部的枚举类型增加特定于常量的行为
例如,public static Operation inverse(Operation op){
switch(op){
case PLUS:...;
case MINUS:...;
}
}
15. 与int常量相比,枚举类型的优势是不言而喻的
16. 如果多个枚举常量同时共享共同的行为,则考虑策略枚举。然后,多个枚举常量就可以将某些逻辑委托给指定的策略枚举对象
第31条:用实例域代替序数
1. 所有的枚举都有一个ordinal方法,它返回每个枚举常量在类型中的数字位置
除非你在编写的是类似EnumSet和EnumMap这种基于枚举的通用数据结构,否则最好完全避免使用ordinal方法
2. 永远不要根据枚举的序数导出与它关联的值,而是要将它保存在一个实例域中
第32条:用EnumSet代替位域
1. 这种用or位运算符将几个常量合并到一个集合中的方式,称作位域(bit field)
2. java.util包提供了EnumSet类来有效地表示从单个枚举类型中提取的多个值的多个集合
3. EnumSet类集位域的简洁和性能优势及第30条中所述的枚举类型的所有优点于一身
第33条:用EnumMap代替序数索引
1. java.util.EnumMap是一个非常快速的、专门用于枚举键的Map实现
2. EnumMap在运行速度方面之所以能与通过序数索引的数组相媲美,是因为EnumMap在内部使用了这种数组
3. 注意EnumMap构造器采用键类型的Class对象:这是一个有限制的类型令牌,它提供了运行时的泛型信息
4. 最好不要使用序数(枚举类型中的ordinal方法来索引数组,而要使用EnumMap)
第34条:用接口模拟可伸缩的枚举
1. <T extends Enum<T> & Operation> Class<T>确保了Class对象既表示枚举又表示Operation的子类型
2. 除非需要灵活地合并多个实现类型的操作,否则应该尽可能使用有限制的类型令牌
3. 总而言之,虽然无法编写可扩展的枚举类型,却可以通过编写接口以及实现接口的基础枚举类型,对它进行模拟
这样允许客户端编写自己的枚举来实现接口,如果API是根据接口编写的,那么在可以使用基础枚举类型的任何地方,也都可以使用这些枚举
第35条:注解优先于命名模式
1. java 1.5发行版本之前,一般使用命名模式(naming pattern)表明有些程序元素需要通过某种工具或者框架进行特殊处理
例如,JUnit测试框架原本要求它的用户一定要用test作为测试方法名称的开头
2. 命名模式的几个缺点
(1)文字拼写错误会导致失败,且没有任何提示
(2)无法确保它们只用于相应的程序元素上
(3)它们没有提供将参数值与程序元素关联起来的好方法
3. 注解很好地解决了命名模式的几个问题
4. Test注解类型的声明就是它自身通过Retention和Target注解进行了注解,注解类型声明中的这种注解被称作元注解(meta-annotation)
@Retention(RetentionPolicy.RUNTIME)元注解表明,Test注解应该在运行时保留;@Target(ElementType.METHOD)元注解表明,Test注解只在方法声明中才是合法的:它不能运用到类声明、域声明或者其他程序元素上
5. 注解永远不会改变被注解代码的含义,但是使它可以通过工具进行特殊的处理
6. 测试运行工具在命令行上使用完全匹配的类名,并通过调用Method.invoke反射式地运行类中所有标注了Test的方法。isAnnotationPresent方法告知该工具要运行哪些方法
7. 如果是在编写一个需要程序员给源文件添加信息的工具,就要定义一组适当的注解类型。既然有了注解,就完全没有理由再使用命名模式了
8. 除了“工具铁匠”(toolsmiths——特定的程序员)之外,大多数程序员都不必定义注解类型,但是所有的程序员都应该使用java平台提供的预定义的注解类型。还要考虑使用IDE或者静态分析工具所提供的任何注解,这种注解可以提升由这些工具所提供的诊断信息的质量
第36条:坚持使用Override注解
1. 应该在你想要覆盖超类声明的每个方法声明中使用Override注解
2. 现代的IDE提供了坚持使用Override注解的另一种理由,这种IDE具有自动检查功能,称作代码检验(code inspection)
3. 总而言之,如果在你想要的每个方法声明中使用Override注解来覆盖超类声明,编译器就可以替你防止大量的错误
第37条:用标记接口定义类型
1. 标记接口(marker interface)是没有包含方法声明的接口,而只是声明一个类实现了具有某种属性的接口
2. 标记接口胜过标记注解的两点
(1)标记接口定义的类型是由被标记类的实例实现的;标记注解则没有定义这样的类型
(2)它们可以被更加精确地进行锁定
3. 标记注解胜过标记接口的最大优点在于,它可以通过默认的方式添加一个或者多个注解类型元素,给已被使用的注解类型添加更多的信息
4. 何时使用标记接口,何时使用标记注解
(1)如果标记是应用到任何程序元素而不是类或者接口,就必须使用标记注解
(2)如果标记只应用给类或者接口,则优先考虑标记接口,这样可以用接口作为相关方法的参数类型,从而获得编译时类型检查的好处
5. 总而言之,标记接口和标记注解都各有用处
(1)如果想要定义一个任何新方法都不会与之关联的类型,标记接口就是最好的选择
(2)如果想要标记程序元素而非类和接口,考虑到未来可能要给标记添加更多的信息,或者标记要适合于已经广泛使用了注解类型的框架,那么标记注解就是正确的选择
第7章 方法
第38条:检查参数的有效性
1. 每当编写方法或者构造器的时候,应该考虑它的参数有哪些限制。应该把这些限制写到文档中,并且在这个方法体的开头处,通过显式的检查来实施这些限制
第39条:必要时进行保护性拷贝
1. 假设类的客户端会尽其所能来破坏这个类的约束条件,因此你必须保护性地设计程序
2. 不管是哪种情况,编写一些面对客户的不良行为时仍能保持健壮性的类,这是非常值得投入时间去做的事情
3. 对于构造器的每个可变参数进行保护性拷贝(defensive copy)是必要的
4. 保护性拷贝是在检查参数的有效性之前进行的,并且有效性检查是针对拷贝之后的对象,而不是针对原始对象
5. 需要注意的是,针对一个非final类进行保护性拷贝时,不应该使用其clone方法,因为clone方法可能返回专门出于恶意的目的而设计的不可信子类的实例
6. 对于参数类型可以被不可信任方子类化的参数,请不要使用clone方法进行保护性拷贝
7. 值得注意的是,如果一个类的访问方法提供了对其可变的内部成员的访问能力,那么该类就有受恶意攻击的危险。为了防止这种恶意攻击,可以通过修改该类的访问方法,使它返回可变内部域的保护性拷贝即可
8. 参数的保护性拷贝并不仅仅针对不可变类。每当编写方法或者构造器时,如果它要允许客户提供的对象进入到内部数据结构中,则有必要考虑一下,客户提供的对象是否有可能是可变的。在内部组件返回给客户端之前,对它们进行保护性拷贝也是同样的道理
9. 实际上,只要有可能,都应该使用不可变的对象作为对象内部的组件,这样就不必再为保护性拷贝操心
10. 简而言之,如果类具有从客户端得到或者返回到客户端的可变组件,类就必须保护性地拷贝这些组件。如果拷贝的成本受到限制,并且类信任它的客户端不会不恰当地修改组件,就可以在文档中指明客户端的职责是不得修改受到影响的组件,以此来代替保护性拷贝
第40条:谨慎设计方法签名
1. 谨慎地选择方法的名称
方法的名称应该始终遵循标准的命名习惯
2. 不要过于追求提供便利的方法
每个方法都应该尽其所能。方法太多会使接口实现者和接口用户的工作变得复杂起来
3. 避免过长的参数列表
目标是四个参数,或者更少。相同类型的长参数序列格外有害
4. 有三种方法可以缩短过长的参数列表
(1)把方法分解成多个方法,每个方法只需要这些参数的一个子集
(2)缩短长参数列表的第二种方法是创建辅助类(helper class),用来保存参数的分组。这些辅助类一般为静态成员类
如果一个频繁出现的参数序列可以被看做是代表了某个独特的实体,则建议使用这种方法
(3)从对象构建到方法调用都采用Builder模式
如果方法带有多个参数,尤其是当它们中有些是可选的时候,最好定义一个对象来表示所有参数
5. 通过提升一个类中所有方法的正交性(orthogonality),可以减少方法的数目
6. 对于参数类型,要优先使用接口而不是类
7. 对于boolean参数,要优先使用两个元素的枚举类型
它使代码更易于阅读和编写,也使以后更易于添加更多的选项,提升扩展性
第41条:慎用重载
1. 要调用哪个重载方法是在编译时做出决定的
2. 系统对于重载方法的选择是静态的,而对于被覆盖的方法的选择则是动态的
3. 应该避免胡乱地使用重载机制
安全而保守的策略是,永远不要导出两个具有相同参数数目的重载方法。如果方法使用可变参数,那么保守的策略是根本不要重载它
4. 可以考虑使用命名模式(即根据方法的差别来进行方法名称的变形)来替代重载方法
例如ObjectOutputStream的writeBoolean(xx)、writeInt(xx)
5. 一个类的多个构造器总是重载的
在许多情况下,可以选择导出静态工厂,而不是构造器
6. 在java1.5发行版本之前,所有的基本类型都根本不同于所有的引用类型,但是当自动装箱出现之后,就不再如此了,它会导致真正的麻烦
例如调用List<Integer>对象的remove(xx)方法,就可能因为自动拆箱机制而导致bug
7. java语言中添加了泛型和自动装箱之后,破坏了List接口
自动装箱和泛型成了java语言的一部分之后,谨慎重载显得更加重要了
8. 在java1.5发行版本中,新增了一个称作CharSequence的接口,用来为StringBuffer、StringBuilder、String、CharBuffer以及其他类似的类型提供公共接口,为实现这个接口,对它们全都进行了改造
9. 让更具体化的重载方法把调用转发给更一般化的重载方法可以确保参数列表存在父子类关系的重载方法返回相同的结果(参p169)
10. 一般情况下,对于多个具有相同参数数目的方法来说,应该尽量避免重载方法
11. 在没法避免使用重载方法的情况下,至少应该避免这样的情形:同一组参数只需经过类型转换就可以被传递给不同的重载方法
如果不能避免这种情形,那么就应该保证:当传递同样的参数时,所有重载方法的行为必须一致
第42条:慎用可变参数
1. 可变参数机制通过先创建一个数组,数组的大小为在调用位置所传递的参数数量,然后将参数值传到数组中,最后将数组传递给方法
2. 有时候,有必要编写需要1个或者多个某种类型参数的方法,而不是需要0个或者多个,后者可能会增加额外的参数个数检查,还可能导致运行时异常
3. 可变参数是为printf而设计的,它是在java1.5发行版本中添加到平台中的。printf和反射机制都从可变参数中极大地受益
4. 可以将以数组当做final参数的现有方法,改造成以可变参数代替,而不影响现有的客户端,但是可以并不意味着应该这么做
5. Arrays.asList方法现在“增强”为使用可变参数,将int类型的数组digits的对象引用集中到数组的单个元素数组中,并忠实地将它包装到List<int[]>实例中
6. 在java1.5发行版本中,Arrays类得到了补充完整的Arrays.toString方法,专门为了将任何类型的数组转变成字符串而设计的
7. 在重视性能的情况下,使用可变参数机制要特别小心
可变参数方法的每次调用都会导致进行一次数组分配和初始化
8. 简而言之,在定义参数数目不定的方法时,可变参数方法是一种很方便的方式,但是它们不应该被过度滥用。如果使用不当,会产生混乱的结果
第43条:返回零长度的数组或者集合,而不是null
1. 零长度数组是不可变的,而不可变对象有可能被自由地共享(见第15条),所以无需担心返回零长度数组比返回null多出的性能开销
2. Collection.toArray(T[])的规范保证:如果输入数组大到足够容纳这个集合,它就将返回这个输入数组
3. 返回类型为数组或集合的方法没理由返回null
第44条:为所有导出的API元素编写文档注释
1. 为了正确地编写API文档,必须在每个被导出的类、接口、构造器、方法和域声明之前增加一个文档注释。如果类是可序列化的,也应该对它的序列化形式编写文档
2. 方法的文档注释应该简洁地描述出它和客户端之间的约定
3. 文档注释应该列举出这个方法的所有前提条件(precondition)和后置条件(postcondition)
4. 每个未受检的异常都对应一个前提违例(precondition violation)。同样地,也可以在一些受影响的参数的@param标记中指定前提条件
5. 除了前提条件和后置条件之外,每个方法还应该在文档中描述它的副作用(side effect)
所谓副作用是指系统状态中可以观察到的变化(该变化不是为了获得后置条件而明确要求的变化)
6. 文档注释也应该描述类或者方法的线程安全性(thread safety)
7. 对于方法抛出的每个异常,无论是受检的还是未受检的,都应该有一个@throws标签
8. 按惯例,跟在@throws标签之后的文字应该包含单词“if”,紧接着是一个名词短语
9. 按惯例,@param、@return、@throws标签后面的短语或者子句都不用句点来结束
10. Javadoc工具会把文档注释翻译成HTML,文档注释中包含的任意HTML元素都会出现在结果HTML文档中
11. 为了产生包含HTML元字符的文档(例如“<”、“>”以及“&”)的最佳办法是用{@literal}标签将它们包围起来,这样就限制了HTML标记和嵌套的Javadoc标签的处理
12. 文档注释在源代码和产生的文档中都应该是易于阅读的
13. 为了避免混淆,同一个类或者接口中的两个成员或者构造器,不应该具有同样的概要描述
14. 方法和构造器的概要描述往往是个动词短语
15. 类、接口和域的概要描述往往是个名词短语
16. Java1.5发行版本中增加的三个特性在文档注释中需要特别小心:泛型、枚举和注解。
(1)当为泛型或者方法编写文档时,确保要在文档中说明所有的类型参数
(2)当为枚举类型编写文档时,要确保在文档中说明常量,以及类型还有任何公有的方法
(3)当为注解类型编写文档时,要确保在文档中说明所有成员,以及类型本身
对于该类型的概要描述,要使用一个动词短语,说明当程序元素具有这种类型的注解时它表示什么意思
17. 类的导出API有两个特征经常被人忽视,即线程安全性和可序列化性
如果类是可序列化的,就应该在文档中说明它的序列化形式
18. 为了降低文档注释中出错的可能性,一种简单的办法是通过一个HTML有效性检查器来运行由Javadoc产生的HTML文件
19. 简而言之,要为API编写文档,文档注释是最好、最有效的途径
20. 对于所有可导出的API元素来说,使用文档注释应该被看做是强制性的
21. 在文档注释内部出现任何HTML标签都是允许的,但是HTML元字符必须要经过转义
第8章 通用程序设计
第45条:将局部变量的作用域最小化
1. 要使局部变量的作用域最小化,最有力的方法就是在第一次使用它的地方声明。如果变量在使用之前进行声明,这只会造成混乱
2. 过早地声明局部变量不仅会使它的作用域过早地扩展,而且结束得也过于晚了
3. 几乎每个局部变量的声明都应该包含一个初始化表达式。如果你还没有足够的信息来对一个变量进行有意义的初始化,就应该推迟这个声明
4. 如果在循环终止之后不再需要循环变量的内容,for循环就优先于while循环
5. 使用for循环与使用while循环相比还有另外一个优势:更简短,从而增强了可读性
6. 最后一种“将局部变量的作用域最小化”的方法是:使方法小而集中
第46条:for-each循环优先于传统的for循环
1. java1.5发行版中引入的for-each循环,通过完全隐藏迭代器或者索引变量,避免了混乱和出错的可能。这种模式同样适用于集合和数组
2. 注意,利用for-each循环不会有性能损失,甚至用于数组也一样
3. for-each循环不仅让你遍历集合和数组,还让你遍历任何实现Iterable接口的对象
4. 总之,for-each循环在简洁性和预防bug方面有着传统的for循环无法比拟的优势,并且没有性能损失。
5. 有三种常见的情况无法使用for-each循环
(1)过滤:如果需要遍历集合,并删除选定的元素,就需要使用显式的迭代器,以便可以调用它的remove方法
(2)转换:如果需要遍历列表或者数组,并取代它部分或者全部的元素值,就需要列表迭代器或者数组索引,以便设定元素的值
(3)平行迭代:如果需要并行地遍历多个集合,就需要显式地控制迭代器或者索引变量,以便所有迭代器或者索引变量都可以得到同步前移
第47条:了解和使用类库
1. 通过使用标准类库,可以充分利用这些编写标准类库的专家的知识,以及在你之前的其他人的使用经验
2. 每个程序员都应该熟悉java.lang、java.util,某种程度上还有java.io中的内容
3. 总之,不要重新发明轮子
第48条:如果需要精确的答案,请避免使用float和double
1. float和double类型尤其不适合用于货币计算,因为要让一个float或者double精确地表示0.1是不可能的
2. 取而代之的是,可以使用BigDecimal、int或者long进行货币计算
3. 使用BigDecimal有两个缺点:与使用基本运算类型相比,这样做很不方便,而且很慢
4. 总而言之,对于任何需要精确答案的计算任务,请不要使用float或者double
(1)如果你想让系统来记录十进制小数点,并且不介意因为不使用基本类型而带来的不便,就请使用BigDecimal
(2)如果性能非常关键,并且你又不介意自己记录十进制小数点,而且所涉及的数值又不太大,就可以使用int或者long
第49条:基本类型优先于装箱基本类型
1. java有一个类型系统由两部分组成,包含基本类型和引用类型
2. 对装箱基本类型运用==操作符几乎总是错误的
3. 当在一项操作中混合使用基本类型和装箱基本类型时,装箱基本类型就会自动拆箱
4. 如果null对象引用被自动拆箱,就会得到一个NullPointerException异常
5. 那么什么时候应该使用装箱基本类型呢?以下是几个合理的用处
(1)作为集合中的元素、键和值
(2)参数化类型中,必须使用装箱基本类型作为类型参数
(3)在使用反射的方法调用时,必须使用装箱基本类型
6. 总之,当可以选择的时候,基本类型要优于装箱基本类型
7. 自动装箱减少了使用装箱基本类型的繁琐性,但是并没有减少它的风险
8. 当程序装箱了基本类型值时,会导致高开销和不必要的对象创建
第50条:如果其他类型更适合,则尽量避免使用字符串
1. 字符串不适合代替其他的值类型
2. 字符串不适合代替枚举类型
枚举类型比字符串更加适合用来表示枚举类型的常量
3. 字符串不适合代替聚集类型
如果一个实体有多个组件,用一个字符串来表示这个实体通常是很不恰当的
4. 字符串也不适合代替能力表(capabilities)
5. 若使用不当,字符串会比其他的类型更加笨拙、更不灵活、速度更慢,也更容易出错
6. 经常被错误地用字符串代替的类型包括基本类型、枚举类型和聚集类型
第51条:当心字符串连接的功能
1. 为连接n个字符串而重复地使用字符串连接操作符,需要n的平方级的时间,这是由于字符串不可变而导致的不幸结果
2. 为了获得可以接受的性能,请使用StringBuilder替代String
3. 不要使用字符串连接操作符来合并多个字符串,除非性能无关紧要
第52条:通过接口引用对象
1. 应该优先使用接口而不是类来引用对象
如果有合适的接口类型存在,那么对于参数、返回值、变量和域来说,就都应该使用接口类型进行声明
2. 如果代码中依赖于一个具体实现类的任何特殊属性,那么就要在声明变量的地方给这些需求建立相应的文档说明
3. 不存在适当接口类型的几种情形
(1)如果没有合适的接口存在,完全可以用类而不是接口来引用对象
更一般地讲,如果具体类没有相关联的接口,不管它是否表示一个值,你都没有别的选择,只有通过它的类来引用它的对象
(2)对象属于一个框架,而框架的基本类型是类,不是接口
如果对象属于这种基于类的框架(class-based framework),就应该用相关的基类来引用这个对象,而不是用它的实现类
java.util.TimerTask抽象类就属于这种情形
(3)类实现了接口,但是它提供了接口中不存在的额外方法
如果程序依赖于这些额外方法,这种类就应该只被用来引用它的实例,它很少应该被用作参数类型
4. 实际上,给定的对象是否具有适当的接口应该是很显然的。如果是,用接口引用对象就会使程序更加灵活;如果不是,则使用类层次结构中提供了必要功能的最基础的类
第53条:接口优先于反射机制
1. 核心反射机制(core reflection facility)java.lang.reflect,提供了“通过程序来访问关于已装载的类的信息”的能力(即内省机制)
2. Constructor、Method和Field实例使你能够通过反射机制操作它们的底层对等体。通过调用Constructor、Method和Field实例上的方法,可以构造底层类的实例、调用底层类的方法,并访问底层类的域。
例如,Method.invoke使你可以调用任何类的任何对象上的任何方法(遵从常规的安全限制)
3. 反射机制所需要付出的代价
(1)丧失了编译时类型检查的好处
(2)执行反射访问所需要的代码非常笨拙和冗长
(3)性能损失
4. 反射功能只是在设计时(design time)被用到。通常,普通应用程序在运行时不应该以反射方式访问对象
5. 类对于在运行时可能不存在的其他类、方法或者域的依赖性,用反射法进行管理,这种方法是合理的,但是很少使用
6. 如果你编写的程序必须要与编译时未知的类一起工作,如有可能,就应该仅仅使用反射机制来实例化对象,而访问对象时则使用编译时已知的某个接口或者超类
第54条:谨慎地使用本地方法
1. 从历史上看,本地方法主要有三种用途
(1)提供了“访问特定于平台的机制”的能力
例如访问注册表和文件锁
(2)提供了访问遗留代码库的能力
(3)本地方法可以通过本地语言,编写应用程序中注重性能的部分,以提高系统的性能
2. 使用本地方法来提高性能的做法不值得提倡
3. 使用本地方法之前务必三思
第55条:谨慎地进行优化
1. 不要因为性能而牺牲合理的结构。要努力编写好的程序而不是快的程序
2. 好的程序体现了信息隐藏(information hiding)的原则
只要有可能,它们就会把设计决策集中在单个模块中
3. 努力避免那些限制性能的设计决策
4. 要考虑API设计决策的性能后果
(1)使公有的类型成为可变的(mutable),这可能会导致大量不必要的保护性拷贝
(2)在适合使用组合模式的公有类中使用继承,会把这个类与它的超类永远地束缚在一起,从而人为地限制了子类的性能
(3)在API中使用实现类型而不是接口,会把你束缚在一个具体的实现上,即使将来出现更快的实现你也无法使用
5. 一般而言,好的API设计也会带来好的性能
为获得好的性能而对API进行包装,这是一种非常不好的想法
6. 在每次试图做优化之前和之后,要对性能进行测量
7. 性能剖析工具有助于你决定应该把优化的重心放在哪里
8. 在Java平台上对优化的结果进行测量,比在其他的传统平台上更有必要,因为Java程序设计语言没有很强的性能模型。各种基本操作的相对开销也没有明确定义,程序员所编写的代码与CPU执行的代码之间存在“语义沟”(semantic gap)
9. 总而言之,不要费力去编写快速的程序——应该努力编写好的程序,速度自然就会随之而来
10. 性能优化的第一步骤应该是检查所选择的算法
再多的底层优化也无法弥补算法的选择不当
第56条:遵守普遍接受的命名惯例
1. 包名称的命名惯例
(1)任何将在你的组织之外使用的包,其名称都应该以你的组织的Internet域名开头,并且顶级域名放在前面
(2)用户创建的包的名称绝不能以java和javax开头
(3)包名称的其余部分应该包括一个或者多个描述该包的组成部分。这些组成部分应该比较简短,通常不超过8个字符
(4)鼓励使用有意义的缩写形式
例如,使用util而不是utilities。只取首字母的缩写形式也是可以接受的,例如awt
2. 类和接口的命名惯例
(1)应该尽量避免缩写,除非是一些首字母缩写和一些通用的缩写
3. 方法和域的名称与类和接口的名称一样,都遵守相同的字面惯例
4. 注意,常量域是唯一推荐使用下划线的情形
5. 局部变量名称的字面命名惯例和成员名称类似,只不过它也允许缩写,单个字符和短字符序列的意义取决于局部变量所在的上下文环境
6. 类型参数名称通常由单个字母组成。这个字母通常是以下五种类型之一
(1)T表示任意的类型
(2)E表示集合的元素类型
(3)K表示映射的键类型
(4)V表示映射的值类
(5)X表示异常
7. 语法命名惯例比字面惯例更加灵活,也更有争议
(1)类(包括枚举类型)通常用一个名词或者名词短语命名
(2)执行某个动作的方法通常用动词或者动词短语来命名
(3)对于返回boolean值的方法,其名称往往以单词“is”开头
(4)如果方法返回被调用对象的一个非boolean的函数或者属性,它通常用名词、名词短语,或者以动词“get”开头的动词短语来命名
(5)转换对象类型的方法、返回不同类型的独立对象的方法,通常被称为toType
例如toString和toArray
(6)返回视图(view,视图的类型不同于接收对象的类型)的方法通常被称为asType
例如asList
(7)返回一个与被调用对象同值的基本类型的方法,通常被称为typeValue
例如intValue
(8)静态工厂的常用名称为valueOf、of、getInstance、newInstance、getType和NewType
8. 域名称的语法惯例没有很好地建立起来,也没有类、接口和方法名称的惯例那么重要,因为设计良好的API很少会包含暴露出来的域
9. boolean类型的域命名与boolean类型的访问方法很类似,但是省去了初始的“is”。其他类型的域通常用名词或者名词短语来命名
第9章 异常
第57条:只针对异常的情况才使用异常
1. 异常机制的设计初衷是用于不正常的情形,所以很少会有JVM实现试图对它们进行优化,使得与显式的测试一样快速
2. 把代码放在try-catch块中有可能会阻止现在JVM实现本来可能要执行的某些特定优化
3. 实际上,在现代JVM实现上,基于异常的模式比标准模式要慢得多
4. 顾名思义,异常应该只用于异常的情况下,它们永远不应该用于正常的控制流
5. 设计良好的API不应该强迫它的客户端为了正常的控制流而使用异常
如果类具有“状态相关(state-dependent)”的方法(即只有在特定的不可预知的条件下才可以被调用的方法),这个类往往也应该有个单独的“状态测试(state-testing)”方法,即指示是否可以调用这个状态相关的方法。例如Iterator接口的next()与hasNext()
6. 另一种提供单独的状态测试方法的做法是,如果“状态相关”的方法被调用时,该对象处于不适当的状态之中,则它就会返回一个可识别的值,比如null
7. 对于“状态测试方法”和“可识别的返回值”这两种做法,有些指导原则可以帮助你在两者之中做出选择
(1)如果对象将在缺少外部同步的情况下被并发访问,或者可被外界改变状态,那么使用可识别的返回值可能是很有必要的
(2)如果单独的“状态测试”方法必须重复“状态相关”方法的工作,从性能角度考虑,就应该使用可被识别的返回值
(3)如果所有其他方面都是等同的,那么“状态测试”方法则略优于可被识别的返回值,因为它具有更好的可读性,而且对于使用不当的情形,可能更易于检测和改正:如果忘了去调用状态测试方法,状态相关的方法就会抛出异常,使这个bug变得很明显;如果忘了去检查可识别的返回值,这个bug将很难会被发现
8. 总而言之,异常是为了在异常情况下使用而设计的。不要将它们用于普通的控制流,也不要编写迫使它们这么做的API
第58条:对可恢复的情况使用受检异常,对编程错误使用运行时异常
1. java语言提供了三种可抛出结构(throwable)
(1)受检异常(checked exception)
(2)运行时异常(run-time exception)
(3)错误(error)
2. 如果期望调用者能够适当地从异常情形中恢复,对于这种情况就应该使用受检的异常
通过抛出受检的异常,强迫调用者在一个catch子句中处理该异常,或者将它传播出去
3. API的设计者让API用户面对受检的异常,以此强制用户从这个异常条件中恢复
4. 如果程序抛出未受检的异常或者错误,往往就属于不可恢复的情形,继续执行下去有害无益
5. 用运行时异常来表明编程错误。大多数的运行时异常都表示前提违例(precondition violation)
6. 你实现的所有未受检的抛出结构都应该是RuntimeException的子类(直接或间接的)
7. 总而言之,对于可恢复的情况,使用受检异常;对于程序错误,则使用运行时异常
8. “解析异常的字符串表示法”的代码可能是不可移植的,也是非常脆弱的
9. 因为受检的异常往往指明了可恢复的条件,所以,对于这样的异常,提供一些辅助方法尤其重要,通过这些方法,调用者可以获得一些有助于恢复的信息
第59条:避免不必要地使用受检的异常
1. 过分使用受检的异常会使API使用起来非常不方便
2. 如果正确地使用API并不能阻止这种异常条件的产生,并且一旦产生异常,使用API的程序员立即采取有用的动作,这种负担就被认为是正当的。除非这两个条件都成立,否则更适合于使用未受检的异常
3. 异常受检的本质并没有为程序员提供任何好处,它反而需要付出努力,还使程序更为复杂
4. 把受检的异常变成未受检的异常的一种方法是,把这个抛出异常的方法分成两个方法,其中第一个方法返回一个boolean,表明是否应该抛出异常。不过这种重构并不总是恰当的,其本质上就是“状态测试方法”
第60条:优先使用标准的异常
1. 专家级程序员与缺乏经验的程序员的一个最主要区别在于,专家追求并且通常也能够实现高度的代码重用
2. 重用现有的异常有多方面的好处
(1)它使你的API更加易于学习和使用
(2)对于用到这些API的程序而言,它们的可读性会更好
(3)异常类越少,意味着内存印迹(footprint)就越小,装载这些类的时间开销也越少
3. 几个比较经常被重用的异常
(1)IllegalArgumentException
(2)IllegalStateException
(3)NullPointerException
(4)IndexOutOfBoundsException
(5)ConcurrentModificationException
如果一个对象被设计为专用于单线程或者与外部同步机制配合使用,一旦发现它正在(或已经)被并发地修改,就应该抛出这个异常
(6)UnsupportedOperationException
如果对象不支持所请求的操作,就会抛出这个异常
4. 一定要确保抛出异常的条件与该异常的文档中描述的条件一致。这种重用必须建立在语义的基础上,而不是建立在名称的基础之上
第61条:抛出与抽象相对应的异常
1. 如果方法抛出的异常与它所执行的任务没有明显的联系,这种情形将会使人不知所措。当方法传递由低层抽象抛出的异常时,往往会发生这种情况
2. 更高层的实现应该捕获低层的异常,同时抛出可以按照高层抽象进行解释的异常。这种做法被称为异常转译(exception translation)
3. 一种特殊的异常转译形式称为异常链(exception chaining),如果低层的异常对于调试导致高层异常的问题非常有帮助,使用异常链就很合适
throw new HigherLevelException(cause),其中cause为低层异常对象
4. 大多数标准的异常都有支持链的构造器
异常链不仅让你可以通过程序访问原因,它还可以将原因的堆栈轨迹集成到更高层的异常中
5. 尽管异常转译与不加选择地从低层传递异常的做法相比有所改进,但是它也不能被滥用
6. 处理来自低层异常的最好做法是,在调用低层方法之前确保它们会成功执行,从而避免它们抛出异常
7. 总而言之,如果不能阻止或者处理来自更低层的异常,一般的做法是使用异常转译
8. 异常链对高层和低层异常都提供了最佳的功能:它允许抛出适当的高层异常,同时又能捕获底层的原因进行失败分析
第62条:每个方法抛出的异常都要有文档
1. 描述一个方法所抛出的异常,是正确使用这个方法时所需文档的重要组成部分
2. 始终要单独地声明受检的异常,并且利用javadoc的@throws 标记,准确地记录下抛出每个异常的条件
3. 虽然java语言本身并不要求程序员为一个方法声明它可能会抛出的未受检异常,但是,如同受检异常一样,仔细地为它们建立文档是非常明智的
对于方法可能抛出的未受检异常,如果将这些异常信息很好地组织成列表文档,就可以有效地描述出这个方法被成功执行的前提条件
4. 使用javadoc的@throws 标记记录下一个方法可能抛出的每个未受检异常,但是不要使用throws关键字将未受检的异常包含在方法的声明中
5. 如果一个类中的许多方法出于同样的原因而抛出同一个异常,在该类的文档注释中对这个异常建立文档,这是可以接受的
6. 总而言之,要为你编写的每个方法所能抛出的每个异常建立文档
第63条:在细节消息中包含能捕获失败的信息
1. 异常类型的toString方法应该尽可能多地返回有关失败原因的信息,这一点特别重要
换句话说,异常的细节信息应该捕获住失败,便于以后分析
2. 为了捕获失败,异常的细节信息应该包含所有“对该异常有贡献”的参数和域的值
3. 虽然在异常的细节信息中包含所有相关的“硬数据(hard data)”是非常重要的,但是包含大量的描述信息往往没有什么意义
4. 与用户层次的错误信息不同,异常的字符串表示法主要是让程序员或者域服务人员用来分析失败原因的。因此,信息的内容比可理解性要重要得多
5. 为异常的“失败捕获”信息提供一些访问方法是合适的
第64条:努力使失败保持原子性
1. 一般而言,失败的方法调用应该使对象保持在被调用之前的状态
具有这种属性的方法被称为具有失败原子性(failure atomic)
2. 获得失败原子性最常见的办法是,在执行操作之前检查参数的有效性。这可以使得在对象的状态被修改之前,先抛出适当的异常。
3. 一种类似的获得失败原子性的办法是,调整计算处理过程的顺序,使得任何可能会失败的计算部分都在对象状态被修改之前发生
4. 第三种获得失败原子性的办法并不常用,做法是编写一段恢复代码(recovery code),由它来拦截操作过程中发生的失败,以及使对象回滚到操作开始之前的状态上。这种办法主要用于永久性的数据结构(基于磁盘的)
5. 最后一种获得失败原子性的办法是,在对象的一份临时拷贝上执行操作,当操作完成之后再用临时拷贝中的结果代替对象的内容
例如,Collections.sort在执行排序之前,首先把它的输入列表转到一个数组中
6. 错误(相对于异常)通常是不可恢复的,当方法抛出错误时,它们不需要努力保持失败原子性
7. 一般而言,作为方法规范的一部分,产生的任何异常都应该让对象保持在该方法调用之前的状态
第65条:不要忽略异常
1. 空的catch块会使异常达不到应有的目的,即强迫你处理异常的情况。至少,catch块也应该包含一条说明,解释为什么可以忽略这个异常
2. 只要将异常传播给外界,至少会导致程序迅速地失败,从而保留了有助于调试该失败条件的信息
第10章 并发
第66条:同步访问共享的可变数据
1. java语言规范保证读或者写一个变量是原子的(atomic),除非这个变量的类型为long或者double
2. 你可能听说过,为了提高性能,在读或者写原子数据的时候,应该避免使用同步。这个建议是非常危险而错误的
3. 为了在线程之间进行可靠的通信,也为了互斥访问,同步是必要的
这归因于java语言规范中的内存模型(memory model),它规定了一个线程所做的变化何时以及如何变成对其他线程可见
4. 如果对共享的可变数据的访问不能同步,其后果将非常可怕,即使这个变量是原子可读写的。
5. 要阻止一个线程妨碍另一个线程,建议做法是让第一个线程轮询(poll)一个boolean域,这个域一开始为false,但是可以通过第二个线程设置为true,以表示第一个线程将终止自己。如果读写boolean域不使用同步,那么程序很可能会出问题,即便boolean域的读写操作都是原子的。(参p230)
6. 实际上,如果读和写操作没有都被同步,同步就不会起作用
7. StopThread中被同步方法的动作即使没有同步也是原子的。换句话说,这些方法的同步只是为了它的通信效果,而不是为了互斥访问。(参p231)
8. 虽然volatile修饰符不执行互斥访问,但它可以保证任何一个线程在读取该域的时候都将看到最近刚刚被写入的值
9. 值得注意的是,增量操作符(++)不是原子的。它相当于执行了两项操作:首先读取值,并执行递增操作,然后回写
这个问题,可以用AtomicLong.getAndIncrement()来改进
10. 要么共享不可变的数据,要么压根不共享。换句话说,将可变数据限制在单个线程中
如果采用这一策略,那么对它建立文档就很重要,以便它可以随着线程的发展而得到维护
11. 将这种对象引用(effectively immutable object)从一个线程传递到其他的线程被称作安全发布(safe publication)
12. 安全发布对象引用有许多种方法
(1)可以将它保存在静态域中,作为类初始化的一部分
(2)可以将它保存在volatile域、final域或者通过正常锁定访问的域中
(3)可以将它放到并发的集合中
13. 简而言之,当多个线程共享可变数据的时候,每个读或者写数据的线程都必须执行同步
14. 未能同步共享可变数据会造成程序的活性失败(liveness failure)和安全性失败(safety failure)。这样的失败最难以调试
第67条:避免过度同步
1. 为了避免活性失败和安全性失败,在一个被同步的方法或者代码块中,永远不要放弃对客户端的控制
换句话说,在一个被同步的区域内部,不要调用设计成要被覆盖的方法,或者是由客户端以函数对象的形式提供的方法
2. 在同步区域内,不要调用外来方法
根据外来方法的作用,从同步区域中调用它会导致异常、死锁或者数据损坏
3. 我们正企图在遍历列表的过程中,将一个元素从列表中删除,这是非法的
4. 从同步区域中调用外来方法,在真实的系统中已经造成了许多死锁(参p236)
5. 可重入的锁简化了多线程的面向对象程序的构造,但是它们可能会将活性失败变成安全性失败
6. 将外来方法的调用移出同步代码块有两种方式
(1)给同步块中操作的数据对象或者数据结构拍快照,随后在同步块之外再针对快照的对象进行外部调用(参p237)
(2)java1.5之后的版本,可以使用并发集合,称之为CopyOnWriteArrayList,这是专门为此定制的
7. CopyOnWriteArrayList是ArrayList的一种变体,通过重新拷贝整个底层数据,在这里实现所有的写操作。由于内部数组永远不改动,因此迭代不需要锁定,速度也非常快
8. 在同步区域之外被调用的外来方法被称作“开放调用(open call)”。除了可以避免死锁之外,开放调用还可以极大地增加并发性。外来方法的运行时间可能会任意长
9. 通常,你应该在同步区域内做尽可能少的工作
(1)获得锁
(2)检查共享数据
(3)根据需要转换数据
(4)释放锁
10. 在这个多核的时代,过度同步的实际成本并不是指获取锁所花费的CPU时间,而是指失去了并行的机会,以及因为需要确保每个核都有一个一致的内存视图而导致的延迟。过度同步的另一项潜在开销在于,它会限制VM优化代码执行的能力
11. 如果一个可变的类要并发使用,应该使这个类变成是线程安全的
通过内部同步,你还可以获得明显比从外部锁定整个对象更高的并发性。否则(即非并发使用的情况),就不要在内部同步,让客户在必要的时候从外部同步。
12. 当你不确定的时候,就不要同步你的类,而是应该建立文档,注明它不是线程安全的
13. 如果你在内部同步了类,就可以使用不同的方法来实现高并发性,例如
(1)分拆锁(lock splitting)
(2)分离锁(lock striping)
(3)非阻塞(nonblocking)并发控制
14. 如果方法修改了静态域,那么你也必须同步对这个域的访问,即使它往往只用于单个线程。
客户要在这种方法上执行外部同步是不可能的,因为不可能保证其他不相关的客户也会执行外部同步
15. 简而言之,为了避免死锁和数据破坏,千万不要从同步区域内部调用外来方法
第68条:executor和task优先于线程
1. java1.5版本之后,java平台中增加了java.util.concurrent,这个包中包含了一个Executor Framework,这是一个很灵活的基于接口的任务执行工具
2. 如果你只想让一个线程来处理来自这个队列的请求,则使用如下
ExecutorService executor = Executors.newSingleThreadExecutor();
3. 如果你想让不止一个线程来处理来着这个队列的请求,则只要调用一个不同的静态工厂,这个工厂创建了一种不同的executor service,称作线程池(thread pool)
4. 如果你想来点特别的,你可以直接使用ThreadPoolExecutor类,这个类允许你控制线程池操作的几乎每个方面
5. 为特殊的应用程序选择executor service是很有技巧的
(1)在轻负载的服务器中,使用Executors.newCachedThreadPool通常是个不错的选择
(2)在高负载的服务器中,最好使用Executors.newFixedThreadPool,它为你提供了一个包含固定线程数据的线程池,或者为了最大限度地控制它,就直接使用ThreadPoolExecutor类
6. 你不仅应该尽量不要编写自己的工作队列,而且还应该尽量不直接使用线程
7. 现在关键的抽象不再是Thread了,它以前即充当工作单元,又是执行机制。现在工作单元和执行机制是分开的
8. 现在关键的抽象是工作单元,称作任务(task)。任务有两种
(1)Runnable
(2)Callable:与Runnable类似,但它会返回值
9. 执行任务的通用机制是executor service。从本质上讲,Executor Framework所做的工作是执行
10. Executor Framework也有一个可以代替java.util.Timer的东西,即ScheduledThreadPoolExecutor
Timer只用一个线程来执行任务,这在面对长期运行的任务时,会影响到定时的准确性。如果Timer唯一的线程抛出未被捕获的异常,Timer就会停止运行。ScheduledThreadPoolExecutor支持多个线程,并且可以优雅地从抛出的未受检异常的任务中恢复
第69条:并发工具优先于wait和notify
1. 既然正确地使用wait和notify比较困难,就应该用更高级的并发工具来代替
2. java.util.conrurrent中更高级的工具分成三类
(1)Executor Framework
(2)并发集合(Concurrent Collection)
并发集合为标准的集合接口提供了高性能的并发实现。为了提高并发性,这些实现在内部自己管理同步
(3)同步器(Synchronizer)
3. 这意味着客户无法原子地对并发集合进行方法调用
4. 除非不得已,否则应该优先使用ConcurrentHashMap,而不是使用Collections.synchronizedMap或者Hashtable
5. 应该优先使用并发集合,而不是使用外部同步的集合
6. 不出所料,大多数ExecutorService实现(包括ThreadPoolExecutor)都使用BlockingQueue
7. 同步器(Synchronizer)是一些使线程能够等待另一个线程的对象,允许它们协调动作
最常用的同步器是CountDownLatch和Semaphore
8. 对于间歇式的定时,始终应该优先使用System.nanoTime,而不是使用System.currentTimeMills。System.nanoTime更加准确也更加精确,它不受系统的实时始终调整所影响
9. 始终应该使用wait循环模式来调用wait方法,永远不要在循环之外调用wait方法。循环会在等待之前和之后测试条件
10. 如果条件已经成立,并且在线程等待之前,notify(或者notifyAll)方法已经被调用,则无法保证该线程将会从等待中苏醒过来
11. 当条件不成立时,有下面一些理由可使一个线程苏醒过来
(1)另一个线程可能已经得到了锁,并且从一个线程调用notify那一刻起,到等待线程苏醒过来这段时间中,得到锁的线程已经改变了受保护的状态
(2)条件并不成立,但是另一个线程可能意外地或恶意地调用了notify
(3)通知线程在唤醒等待线程时使用了notifyAll
(4)在没有通知的情况下,等待线程也可能(但很少)会苏醒过来。这被称为“伪唤醒”(spurious wakeup)
12. notify唤醒的是单个正在等待的线程,而notifyAll唤醒的则是所有正在等待的线程
13. 线程从被唤醒到得到锁这个过程,实际上还有一整段时间的(o)
14. notifyAll代替notify可以避免来自不相关线程的意外或恶意的等待。否则,这样的等待会“吞掉”一个关键的通知,使真正的接收线程无限地等待下去
15. 简而言之,直接使用wait和notify就像用“并发汇编语言”进行编程一样,而java.util.concurrent则提供了更高级的语言
16. 没有理由在新代码中使用wait和notify,即使有,也是极少的
17. 如果你在维护使用wait和notify的代码,务必确保始终是利用标准的模式从while循环内部调用wait
18. 一般情况下,你应该优先使用notifyAll,而不是使用notify,如果使用notify,请一定要小心,以确保程序的活性
第70条:线程安全性的文档化
1. 如果你没有在一个类的文档中描述其行为的并发性情况,那么使用这个类的程序员将不得不做出某些假设
2. 实际上,线程安全性有多个级别。一个类为了可被多个线程安全地使用,必须在文档中清楚地说明它所支持的线程安全级别
(1)不可变的(immutable)
即类实例是不可变的,例如String、Long和BigInteger
(2)无条件的线程安全(unconditionally thread-safe)
这个类的实例是可变的,但是这个类有着足够的内部同步
(3)有条件的线程安全(conditionally thread-safe)
(4)非线程安全(not thread-safe)
(5)线程对立的(thread-hostile)
这个类不能安全地被多个线程并发使用。线程对立的根源通常在于,没有同步地修改静态数据
3. 在文档中描述一个有条件的线程安全类要特别小心,你必须指明哪个调用序列需要外部同步,还要指明为了执行这些序列,必须获得哪一把锁
4. 除非从返回类型来看已经很明显,否则静态工厂必须在文档中说明被返回对象的线程安全性
5. 为了避免这种拒绝服务攻击(denial-of service,客户端超时地保持公有可访问锁),应该使用一个私有锁对象来代替同步的方法(隐含着一个公有可访问锁)
6. 注意,如无特殊情况,我们应该把锁对象封装在它所同步的对象中
7. 将lock域的可变性减到最小
8. 私有锁对象模式特别适用于那些专门为继承而设计的类
9. 如果你编写的是无条件的线程安全类,就应该考虑使用私有锁对象来代替同步的方法。这样可以防止客户端程序和子类的不同步干扰
第71条:慎用延迟初始化
1. 对于延迟初始化,最好的建议是:除非绝对必要,否则就不要这么做
2. 延迟初始化实际上降低了性能
3. 如果利用延迟优化来破坏初始化的循环,就要使用同步访问方法
synchronized FieldType getField(){
if(field == null){
field = computeFieldValue();
}
return field;
}
4. 如果出于性能的考虑而需要对静态域使用延迟初始化,就使用lazy initialization holder class模式
这种模式的魅力在于,getField方法不需要使用synchronized,并且只执行一个域访问,因此延迟初始化实际上并没有增加任何访问成本
5. 如果出于性能的考虑而需要对实例域使用延迟初始化,就使用双重检查模式(double-check idiom)
这种模式避免了在域被初始化之后访问这个域时的锁定开销
6. 在java1.5版本发行之前,double-check idiom的功能很不稳定,因为volatile修饰符的语义不够强,难以支持它。
如今,双重检查模式是延迟初始化一个实例域的方法
7. 对于可以接受重复初始化的实例域,也可以考虑使用单重检查模式(single-check idiom)
8. 简而言之,大多数的域应该正常地进行初始化,而不是延迟初始化
第72条:不要依赖于线程调度器
1. 任何依赖于线程调度器来达到正确性或者性能要求的程序,很有可能都是不可移植的
2. 如果线程没有在做有意义的工作,就不应该运行
3. 使runnable任务保持适当的小,彼此独立。但是,也不能太小,否则分配的开销也会影响到性能
4. 线程不应该一直处于忙等状态(busy-wait)
5. 线程优先级是java平台上最不可移植的特征
6. 应该使用Thread.sleep(1)代替Thread.yield来进行并发测试。千万不要使用Thread.sleep(0)
7. 简而言之,不要让应用程序的正确性依赖于线程调度器
第73条:避免使用线程组
1. 除了线程、锁和监视器之外,线程系统还提供了一个基本的抽象,即线程组(thread group)
2. 总而言之,线程组并没有提供太多有用的功能,而且它们提供的许多功能还都是有缺陷的。我们最好把线程组看作是一个不成功的试验,因此可以忽略掉它们
3. 如果你正在设计的一个类需要处理线程的逻辑组,或许就应该使用线程池executor(见第68条)
第11章 序列化
1. 将一个对象编码成一个字节流,称作将该对象序列化(serializing);相反的处理过程被称作反序列化(deserializing)
2. 序列化技术为远程通信提供了标准的线路级(wire-level)对象表示法,也为JavaBean组件结构提供了标准的持久化数据格式。
第74条:谨慎地实现Serializable接口
1. 实现Serializable接口而付出的最大代价是,一旦一个类被发布,就大大降低了“改变这个类的实现”的灵活性
如果一个类实现了Serializable接口,它的字节流编码(或许说序列化形式)就变成了它的导出API的一部分
2. 如果你接受了默认的序列化形式,这个类中私有的和包级私有的实例域将都变成导出的API的一部分,这不符合“最低限度地访问域”的实践准则,从而它就失去了作为信息隐藏工具的有效性
3. 如果你接受了默认的序列化形式,并且以后又要改变这个类的内部表示法,结果可能导致序列化形式的不兼容
4. 设计良好的序列化形式也许会给类的演变带来限制,但是设计不好的序列化形式则可能会使类根本无法演变
5. 序列化会使类的演变收到限制,这种限制的一个例子与流的唯一标识符有关,通常它也被称为序列化版本UID(serial version UID)
每个可序列化的类都有一个唯一标识号与它相关联
6. 如果你没有声明一个显式的序列版本UIC,兼容性将会遭到破坏,在运行时导致InvalidClassException异常
7. 为了保证类序列化的兼容性,我们应该显式地指定Serial Version UID,并且确定以后不要再改变(o)
8. 实现Serializable的第二个代价是,它增加了出现bug和安全漏洞的可能性
反序列化机制是一个“隐藏的构造器”,具备与其他构造器相同的特点。依靠默认的反序列化机制,很容易使对象的约束关系遭到破坏,以及遭受到非法访问
9. 实现Serializable的第三个代价是,随着类发行新的版本,相关的测试负担也增加了
当一个可序列化的类被修订的时候,很重要的一点是,要检查是否可以“在新版本中序列化一个实例,然后在旧版本中反序列化”,反之亦然。除了二进制兼容性以外,你还必须测试语义兼容性
10. 如果在最初编写一个类的时候,就精心设计了自定义的序列化形式,测试的要求就可以有所降低,但是也不能完全没有测试
11. 根据经验,比如Date和BigInteger这样的值类应该实现Serializable,大多数的集合类也应该如此。代表活动实体的类,比如线程池,一般不应该实现Serializable
12. 为了继承而设计的类应该尽可能少地去实现Serializable接口,用户的接口也应该尽可能少地继承Serializable接口
13. 对于为继承而设计的不可序列化的类,你应该考虑提供一个无参构造器
如果一个专门为了继承而设计的类,不是可序列化的,就不可能编写出可序列化的子类。特别是,如果超类没有提供可访问的无参构造器,子类也不可能做到可序列化
14. 最好在所有的约束关系都已经建立的情况下再创建对象
盲目地为一个类增加无参构造器和单独的初始化方法,而它的约束关系仍由其他的构造器来建立,这样做会使该类的状态空间更加复杂,并且增加出错的可能性
15. 内部类不应该实现Serializable
内部类使用编译器产生的合成域(synthetic field)来保存指向外围实例(enclosing instance)的引用,以及保存来自外围作用域的局部变量的值。“这些域如何对应到类定义中”并没有明确的规定,就好像没有指定匿名类和局部类的名称一样。因此内部类的默认序列化形式是定义不清楚的。然而,静态成员类(static member class)却可以实现Serializable接口
16. 实现Serializable接口就是个很严肃的承诺,我们必须认真对待
17. 在“允许子类实现Serializable接口”或“禁止子类实现Serializable接口”两者之间的一个折中设计方案是,提供一个可访问的无参构造器。这种设计方案允许(但不要求)子类实现Serializable接口
第75条:考虑使用自定义的序列化形式
1. 如果没有先认真考虑默认的序列化形式是否合适,则不要贸然接受
2. 对于一个对象来说,理想的序列化形式应该只包含该对象所表示的逻辑数据,而逻辑数据与物理表示法应该是各自独立的
3. 如果一个对象的物理表示法等同于它的逻辑内容,可能就适合于使用默认的序列化形式
4. 即使你确定了默认的序列化形式是合适的,通常还必须提供一个readObject方法以保证约束关系和安全性
5. @serial标签告诉Javadoc工具,把这些文档信息放在有关序列化形式的特殊文档页中
6. 当一个对象的物理表示法与它的逻辑数据内容有实质性的区别时,使用默认序列化形式会有以下4个缺点
(1)它使这个类的导出API永远地束缚在该类的内部表示法上
(2)它会消耗过多的空间
(3)它会消耗过多的时间
序列化逻辑并不了解对象图的拓扑关系,所以它必须要经过一个昂贵的图遍历(traversal)过程
(4)它会引起栈溢出
7. 对于StringList类,合理的序列化形式可以非常简单,只需先包含链表中字符串的数目,然后紧跟着这些字符串即可。这样就构成了StringList所表示的逻辑数据,与它的物理表示法细节脱离
8. transient修饰符表明这个实例域将从一个类的默认序列化形式中省略掉
9. 如果所有的实例域都是瞬时的,从技术角度而言,不调用defaultWriteObject和defaultReadObject也是允许的,但是不推荐这样做
10. 对于StringList,默认的序列化形式不够灵活,并且执行效果不佳,但是序列化和反序列化StringList实例会产生对原始对象的忠实拷贝
11. 对于散列表而言,接受默认的序列化形式将会构成一个严重的bug
对散列表对象进行序列化和反序列化操作所产生的对象,其约束关系会遭到严重的破坏
12. 无论你是否使用默认的序列化形式,当defaultWriteObject方法被调用的时候,每一个未被标记为transient的实例域都会被序列化
13. 在决定将一个域做成非transient的之前,请一定确信它的值将是该对象逻辑状态的一部分。如果你正在使用一种自定义的序列化形式,大多数实例域,或者所有的实例域则都应该被标记为transient
14. 如果你正在使用默认的序列化形式,并且把一个或者多个域标记为transient,则要记住,当一个实例被反序列化的时候,这些域将被初始化为它们的默认值
15. 不管你选择了哪种序列化形式,都要为自己编写的每个可序列化的类声明一个显式的序列版本UID
如果没有提供显式的序列版本UID,就需要在运行时通过一个高开销的计算过程产生一个序列版本UID
16. 如果你想修改一个没有序列版本UID的现有的类,并希望新的版本能够接受现有的序列化实例,就必须使用那个自动为旧版本生成的值。如通过在旧版本的类上运行serialver工具,可以得到这个数值——被序列化的实例为之存在的那个数值
17. 如果你想为一个类生成一个新的版本,这个类与现有的类不兼容,那么你只需修改序列版本UID声明中的值即可。
18. 只有当默认的序列化形式能够合理地描述对象的逻辑状态时,才能使用默认的序列化形式
19. 你应该分配足够多的时间来设计类的序列化形式,就好像分配足够多的时间来设计它的导出方法一样
20. 正如你无法在将来的版本中去掉导出方法一样,你也不能去掉序列化形式中的域,它们必须被永久地保留下去,以确保序列化兼容性
第76条:保护性地编写readObject方法
1. 不严格地说,readObject是一个“用字节流作为唯一参数”的构造器
当面对一个人工伪造的字节流时,readObject产生的对象会违反它所属的类的约束条件,这时问题就产生了
2. 为了避免攻击者创建无效的对象实例,可以重写readObject方法,该方法首先调用defaultReadObject,然后检查被反序列化后的对象的有效性
3. 攻击者可以通过伪造字节流来创建不可变类的一个可变对象实例
4. 一旦攻击者获得了一个不可变类的可变对象实例,他就可以将这个实例传递给一个“安全性依赖于不可变类的不可变性”的类,从而造成更大的危害。这种推断并不牵强:实际上,有许多类的安全性就依赖于String的不可变性
5. 当一个对象被反序列化的时候,对于客户端不应该拥有的对象引用,如果哪个域包含了这样的对象引用,就必须要做保护性拷贝,这是非常重要的
6. 对于每个可序列化的不可变类,如果它包含了私有的可变组件,那么在它的readObject方法中,必须要对这些组件进行保护性拷贝
7. 注意,保护性拷贝是在有效性检查之前进行的,而且,我们没有使用Date的clone方法来执行保护性拷贝
8. 不要使用writeUnshared和readUnshared方法。它们通常比保护性拷贝更快,但是它们不提供必要的安全性保护
9. 对于非final的可序列化的类,在readObject方法和构造器之间还有其他类似的地方。readObject方法不可以调用可被覆盖的方法,无论是直接调用还是间接调用都不可以。如果违反了这条规则,并且覆盖了该方法,被覆盖的方法将在子类的状态被反序列化之前先运行
10. 总而言之,每当你编写readObject方法的时候,都要这样想:你正在编写一个公有的构造器,无论给它传递什么样的字节流,它都必须产生一个有效的实例。
11. 编写健壮的readObject方法的指导方针
(1)对于对象引用域必须保持为私有的类,要保护性地拷贝这些域中的每个对象
(2)对于任何约束条件,如果检查失败,则抛出一个InvalidObjectException异常。这些检查动作应该跟在所有的保护性拷贝之后
(3)如果整个对象图在被反序列化之后必须进行验证,就应该使用ObjectInputValidation接口
(4)无论是直接方式还是间接方式,都不要调用类中任何可被覆盖的方法
第77条:对于实例控制,枚举类型优先于readResolve
1. 如果一个Singleton类的声明中加上了“implements Serializable”的字样,那么它就不再是一个Singleton了。无论该类使用了默认的序列化形式,还是自定义的序列化形式
2. 任何一个readObject方法,不管是显式的还是默认的,它都会返回一个新建的实例
3. readResolve特性允许你用readObject创建的实例代替另一个实例
4. 事实上,如果依赖readResolve进行实例控制,带有对象引用类型的所有实例域则都必须声明为transient的
5. 这种攻击有点复杂,但是背后的思想却很简单:如果Singleton包含一个非transient的对象引用域,这个域的内容就可以在Singleton的readResolve方法运行之前被反序列化。当对象引用域的内容被反序列化时,它就允许一个精心制作的流“盗用”指向最初被反序列化的Singleton的引用
6. 从历史上看,readResolve方法被用于所有可序列化的实例受控(instance-controlled)的类
自从java1.5发行以来,它就不再是可序列化的类中维持实例控制的最佳方法了
7. readResolve的可访问性(accessibility)很重要
8. 总而言之,你应该尽可能得使用枚举类型来实施实例控制的约束条件。如果做不到,同时又需要一个即可序列化又是实例受控的类,就必须提供一个readResolve方法,并确保该类的所有实例域都为基本类型,或者是transient的
第78条:考虑用序列化代理代替序列化实例
1. 决定实现Serializable接口,会增加出错和出现安全问题的可能性,因为它导致实例要利用语言之外的机制来创建,而不是用普通的构造器
2. 正如保护性拷贝方法一样,序列化代理模式可以阻止伪字节流的攻击以及内部域的盗用攻击
3. 序列化代理模式允许反序列化实例有着与原始序列化实例不同的类
4. 实际上,EnumSet就使用了序列化代理模式
5. 序列化代理模式有两个局限性
(1)它不能与可以被客户端扩展的类兼容
(2)它也不能与对象图中包含循环的某些类兼容
如果你企图从一个对象的序列化代理的readResolve方法内部调用这个对象中的方法,就会得到一个ClassCastException异常,因为你还没有这个对象,只有它的序列化代理
6. 序列化代理模式所增强的功能和安全性是有代价的,它可能比保护性拷贝的开销要来得大
7. 总而言之,每当你发现自己必须在一个不能被客户端扩展的类上编写readObject或者writeObject方法的时候,就应该考虑使用序列化代理模式。
1. 代码应该被重用,而不是被拷贝
2. 模块之间的依赖性尽可能降到最小
3. Java语言支持四种类型:接口、类、数据和基本类型
4. 类的成员由它的field、method、member class和member interface组成
5. 方法签名(signature)由它的名称和所有参数类型组成,签名不包括它的返回类型
6. 类、接口、构造器、成员以及序列化形式被统称为API元素(API Element)。导出的API由所有可在定义该API的包之外访问的API元素组成
7. 一个包的导出API(exported API)是由该包中的每个公有(public)类或者接口中所有公有的或者受保护的(protected)成员和构造器组成
第2章 创建和销毁对象
第1条:考虑用静态工厂方法代替构造器
一个类可以通过静态工厂方法而非构造器来创建其实例,例如Boolean.valueOf(boolean b)方法。需要注意的是,静态工厂方法与设计模式中的工厂方法模式不同。
1. 静态工厂方法的优势
(1)它们有名称
a. 当一个类需要多个带有相同签名的构造器时,就用静态工厂方法代替构造器,并且慎重地选择名称以便突出它们之间的区别
(2)不必在每次调用它们的时候都创建一个新对象
a. 如果程序经常请求创建相同的对象,并且创建对象的代价很高,则这项技术可以极大地提升性能
b. 借助静态工厂方法,我们可以创造实例受控的类(instance-controlled class),这样有助于类总能严格控制在某个时刻哪些实例应该存在
(3)它们可以返回原返回类型的任何子类型的对象(这样使得我们在选择返回对象的具体类型时有了更大的灵活性)
a. 该灵活性的一个应用场景是,一个API可以返回一个对象,同时又可以隐藏该对象所对应的具体类(可以是private类型的类),以这种方式隐藏实现类会使API变得非常简洁
b. 这项技术适用于基于接口的框架(interface-based framework)
c. Java Collections Framework的集合接口有32个便利实现,分别提供了不可修改的集合、同步集合等等。几乎所有这些实现都通过静态工厂方法在一个不可实例化的类(java.util.Collections)中导出。所有返回对象对应的类都是非公有的。
d. 静态工厂方法返回的对象所属的类,在编写包含该静态工厂方法的类时可以不必存在。这种灵活的静态工厂方法构成了服务提供者框架(Service Provider Framework)的基础,例如JDBC API
PS:Service Provider Framework
该框架包含四个组件:Service Interface、Provider Registration API、Service Access API和Service Provider Interface(optional),其中Service Access API就是“灵活的静态工厂”,具体可参考书籍描述
(4)在创建参数化类型实例的时候,它们使代码变得更加简洁
a. 有了静态工厂方法,编译器可以替你找到类型参数,可称之为类型推导(type inference)。例如
Map<String, List<String>> m = new HashMap<String, List<String>>();
可改进为:
Map<String, List<String>> m = HashMap.newInstance();(假设HashMap提供了"public static <K,V> HashMap<K,V> newInstance(){return new HashMap<K,V>();}"方法)
2. 静态工厂方法的缺点
(1)类如果不含公有的或者受保护的构造器,就不能被子类化
(2)它们与其他的静态方法实际上没有任何区别
a. 这导致的结果是,对于提供了静态工厂方法而不是构造器的类来说,要查明如何实例化一个类,是非常困难的
b. 通过在类或者接口注释中关注静态工厂,并遵守标准的命名习惯,可以在一定程度上弥补这一劣势
第2条:遇到多个构造器参数时要考虑用构建器
静态工厂和构造器有个共同的局限性:它们都不能很好地扩展到大量的可选参数。遇到许多构造器可选参数时,有3中解决方案
1. 重叠构造器模式
提供第一个只有必要参数的构造器,第二个构造器有一个可选参数,第三个有两个可选参数,以此类推。缺点如下:
(1)当有许多参数时,客户端代码会很难编写,且难以阅读
2. JavaBeans模式
调用一个无参构造器来创建对象,然后调用setter方法来设置每个必要的参数,以及每个相关的可选参数。缺点如下:
(1)构造过程被分到了几个调用中,在构造过程中JavaBean可能处于不一致的状态
(2)JavaBeans模式阻止了把类做成不可变(即对象一旦生成,则不能再设置其属性值,简单来说就是不提供setter方法)的可能,这就需要程序员付出额外的努力来确保它的线程安全性。
3. Builder模式
不直接生成想要的对象,而是让客户端利用所有必要的参数调用构造器(或者静态工厂),得到一个builder对象。然后客户端在builder对象上调用类似于setter的方法(可设计builder的setter方法返回builder本身,以实现链式调用),来设置每个相关的可选参数。最后,客户端调用无参的build方法来生成不可变的对象。这个builder是它构建的类的静态成员类。优点如下:
(1)使得客户端代码很容易编写,且易于阅读。builder模式模拟了具名的可选参数,就像Ada和Python中的一样
(2)Java中传统的抽象工厂实现是Class对象,用newInstance方法充当build方法的一部分。但是,Class.newInstance方法破坏了编译时的异常检查,而Builder模式则可弥补这些不足
简而言之,如果类的构造器或者静态工厂中具有多个参数,设计这种类时,Builder模式就是中不错的选择,特别是当大多数参数都是可选的时候。
第3条:用私有构造器或者枚举类型强化Singleton属性
使类成为singleton会使它的客户端测试变得十分困难,因为无法给singleton替换模拟实现,除非它实现一个充当其类型的接口。为了使Singleton类变成是可序列化的,仅仅在声明中加上“Implements Serializable”是不够的。为了维护并保证Singleton,必须声明所有实例域都是瞬时(transient)的,并提供一个新的实例。否则,每次反序列化一个序列化的实例时,都会创建一个新的实例。使用单元素的枚举类型来实现Singleton可以解决该问题
第4条:通过私有构造器强化不可实例化的能力
有时候,我们可能需要编写只包含静态方法和静态域的类。这样的类不希望被实例化,实例化对它没任何意义。如果企图通过将这样的类做成抽象类来强制该类不可被实例化,这是行不通的。因为该类可以被子类化,并且其子类也可以被实例化。那么,想编写这样的类,我们可以使用私有构造器来实现其不可实例化的特性。
第5条:避免创建不必要的对象
对于同时提供了静态工厂方法和构造器的不可变类,通常可以使用静态工厂方法而不是构造器来创建对象,以避免创建不必要的对象。除了可以重用不可变的对象之外,也可以重用那些已知不会被改变的可变对象。
1. 当你应该重用现有对象的时候,请不要创建新的对象
2. 要优先使用基本类型而不是装箱基本类型,要担心无意识的自动装箱
3. 由于小对象的构造器只做很少的显式工作,所以实际上小对象的创建和回收动作是非常廉价的。为了提升程序的清晰性、简洁性和功能性,有时候创建一些附加的小对象是件好事
4. 通过维护自己的对象池(object pool)来避免创建对象并不是一种好的做法,除非池中的对象是非常重量级的。真正正确使用对象池的典型对象示例就是数据库连接池。
第6条:消除过期的对象引用
在支持垃圾回收的语言中,内存泄漏是很隐蔽的,称这类内存泄漏为“无意识的对象保持(unintentional object retention)”更为恰当。如果一个对象引用被无意识地保留起来了,那么,垃圾回收机制不仅不会处理这个对象,而且也不会处理被这个对象所引用的所有其他对象。
1. 清空对象引用(例如,显式地将一个对象引用置为null)应该是一种例外,而不是一种规范行为
2. 一般而言,只要类是自己管理内存,那么程序员就应该警惕内存泄漏的问题。一旦元素被释放掉,则该元素中包含的任何对象引用都应该被清空。
3. 内存泄漏的另一个常见来源是缓存。一旦你把对象引用放到缓存中,它就很容易被遗忘掉。对于这个问题,有几种可能的解决方案
(1)只要在缓存之外存在对某个项的键的引用,该项就有意义,那么可以使用WeakHashMap代表缓存
当缓存中的项过期之后,它们就会自动被删除。记住,只有当所要的缓存项的生命周期是由该键的外部引用而不是由值决定时,WeakHashMap才有用处。
(2)针对“缓存项的生命周期是否有意义”并不是很容易确定,且随着时间的推移,其中的项会变得越来越没有价值的场景,应该使用一个后台线程来完成缓存清除工作。
4. 内存泄漏的第三个常见来源是监听器和其他回调。如果你实现了一个API,客户端在这个API中注册回调,却没有显式地取消注册,那么除非你采取某些动作,否则它们将会积聚。确保回调立即被当做垃圾回收的最佳方法是只保存它们的弱引用(weak reference)
第7条:避免使用finalize方法
1. finalize方法通常是不可预测的,也是很危险的,所以一般情况下不要使用
2. finalize方法的缺点在于不能保证会被及时地执行
3. java语言规范不仅不保证finalize方法会被及时地执行,而且根本就不保证它们会被执行。因此,不应该依赖finalize方法来更新重要的持久状态。
4. 使用finalize方法有一个非常严重的性能损失
5. 如果类的对象中封装的资源(例如文件或者线程)确实需要终止,应该怎么做才能不用编写finalize方法呢?
只需提供一个显式的终结方法,并要求该类的客户端在每个实例不再有用时调用这个方法。例如InputStream、java.sql.Connection的close方法以及java.util.Timer方法等等。另外,需要注意的是,显式的终结方法必须在一个私有域中记录下“该对象已经不再有效”,如果这些方法是在对象已经终止之后被调用,其他的方法就必须检查这个域,并抛出IllegalStateException异常
6. 显式的终止方法通常与try-finally结构结合起来使用,以确保及时终止
7. finalize方法有两种合法用途
(1)当对象的所有者忘记调用前面段落中建议的显式终止方法时,终结方法可以充当“安全网”。如果finalize方法中发现资源还未被终止,则应该先终止资源,并在日志中记录一条警告,表示客户端代码中存在bug
(2)用来终止非关键的本地资源
因为本地对等体(native peer)不是一个普通对象,所以垃圾回收器不会知道它,当它的java对等体被回收的时候,它不会被回收。
第3章 对于所有对象都通用的方法
第8条:覆盖equals时请遵守通用约定
1. 什么时候该覆盖Object.equals方法?
(1)类具有自己特有的“逻辑相等”概念
(2)超类还没有覆盖equals以实现期望的行为
这通常适用于“值类(value class)”的情形
2. 有时,为了满足程序员的需求,不仅必须覆盖equals方法,而且这样做也使得这个类的实例可以被用作Map的Key,或者Set的元素,使Map或者Set表现出预期的行为
3. 有一种“值类”,不需要覆盖equals方法,即用“实例受控”确保“每个值至多只存在一个对象”的类(枚举类型就属于这种类),这样的类,逻辑相同与对象等同是一回事。
4. equals方法的通用约定(equals方法实现了等价关系)
(1)自反性
要求对象必须等于其自身
(2)对称性
任何两个对象对于“它们是否相等”的问题都必须保持一致
(3)传递性
(4)一致性
如果两个对象相等,它们就必须始终保持相等,除非它们中有一个对象被修改了。无论类是否是不可变的,都不要使equals方法依赖于不可靠的资源
(5)非空性
所有对象都必须不等于null。在进行类型转换之前,equals方法必须使用instanceof操作符,检查其参数是否为正确的类型。如果instanceof的第一个操作数是null,那么,不管第二个操作数是哪种类型,instanceof操作符都指定应该返回false[JLS, 15.20.2]
5. 实现高质量equals方法的诀窍
(1)使用==操作符检查“参数是否为这个对象的引用”,如果是,则返回true
(2)使用instanceof操作符检查“参数是否为正确的类型”,如果不是,则返回false
(3)把参数转换成正确的类型。因为转换之前进行过instanceof测试,所以确保会成功
(4)对于该类中的每个“关键(significant)”域,检查参数中的域是否与该对象中对应的域相匹配。如果这些测试全部成功,则返回true
(5)当你编写完成了equals方法之后,应该问自己三个问题:它是否是对称的、传递的、一致的?
(6)覆盖equals时总要覆盖hashCode方法
(7)不要企图让equals方法过于智能
(8)不要将equals声明中的Object对象替换为其他的类型(因为这样实际上并没有覆盖Object.equals方法,使用@Override注解可以防止犯这种错误)
第9条:覆盖equals时总要覆盖hashCode
1. 相等的对象必须具有相等的hash code
(1)HashMap中有一项优化,可以将与每个项相关联的散列码缓存起来,如果散列码不匹配,也就不必检验对象的等同性
实际上,hashMap的hash桶的定位就是根据hash code来的
(2)一个好的散列函数通常倾向于“为不相等的对象产生不相等的散列码”
2. 一个好的散列码生成策略
(1)把某个非零的常数值,比如说17,保存在一个名为result的int类型的变量中
(2)对于对象中每个关键域f(指equals方法中涉及的每个域),完成以下步骤
a. 为该域计算int类型的散列码c
i. boolean类型,则计算(f?1:0)
ii. byte、char、short或者int类型,则计算(int)f
iii. long类型,则计算(int)(f^(f>>>32))
iv. float类型,则计算Float.floatToIntBits(f)
v. double类型,则计算Double.doubleToLongBits(f),然后按照步骤iii为得到的long类型值计算散列值
vi. 对象引用,并且该类的equals方法通过递归地调用equals的方式来比较这个域,则同样为这个域递归地调用hashCode
vii. 数组,则要把每一个元素当做单独的域来处理
b. 按照下面的公式,把a.中计算得到的散列码c合并到result中
result = 31*result+c; 其中result的初始值随机选定的,而这里的31,则是因为它是一个奇素数。如果使用偶数,并且乘法溢出的话,信息就会丢失,因为与2相乘等价于移位运算;使用素数的好处则不是很明显,但是习惯上都使用素数来计算散列结果;最后,31还有个很好的特性,即用移位和减法来代替乘法,可以得到更好的性能:31*i == (i<<5)-i,现代的vm可以自动完成这种优化
(3)返回result值
(4)写完了hashCode方法之后,问问自己“相等的实例是否都具有相等的hashCode”
3. 在散列码的计算过程中,可以把冗余域(redundant field)排除在外,所谓冗余域即为那些可以参与其他域值计算出来的域
4. 必须排除equals比较计算中没有用到的任何域
5. 如果一个类是不可变的,并且计算散列码的开销也比较大的话,就应该考虑把散列码缓存在对象内部,而不是每次请求的时候都重新计算散列码
6. 不要试图从散列码计算中排除掉一个对象的关键部分来提高性能
7. java平台类库中的许多类,比如String, Integer和Date,都可以把它们的hashCode方法返回的确切值规定为该实例的一个函数。一般来说,这并不可取,因为这样做严格限制了将来的版本中改进散列函数的能力
第10条:始终要覆盖toString
1. toString的通用约定指出,被返回的字符串应该是一个“简洁的,但信息丰富,并且易于阅读的表达形式”
2. 提供好的toString实现可以使类用起来更加舒适
当对象被传递给println、printf、字符串联操作符(+)以及assert或者被调试器打印出来时,toString方法会被自动调用
3. 在实际应用中,toString方法应该返回对象中包含的所有值得关注的信息
4. 在实现toString方法时,必须做一个重要的决定:是否要在文档中注明返回值的格式
(1)指定格式的优点:可以被用作一种标准的、明确的、适合人阅读的标准表示法
如果你指定了格式,最好再提供一个相匹配的静态工厂或者构造器,以便程序员可以很容易地在对象及其字符串表示法之间来回转换
(2)指定格式的缺点:如果这个类已经被广泛使用,那么一旦指定了格式,那么就必须始终如一地坚持这种格式
5. 无论你是否决定指定格式,都应该在文档中明确地表明你的意图
6. 无论是否指定格式,都应该为toString返回值中所包含的所有信息,提供一种编程式的访问途径
第11条:谨慎地覆盖clone
1. Cloneable接口的目的是做为对象的一个mixin interface,表明该对象允许克隆
如果一个类实现了Cloneable接口,那么Object的clone方法就返回该对象的逐域拷贝,否则就会抛出CloneNotSupportedException。Cloneable接口不包含任何方法,它改变了超类中受保护的方法的行为,这是一种极端非典型的做法,并不值得效仿
2. 如果实现Cloneable接口是要对某个类起作用,类和它的所有超类都必须遵守一个相当复杂的、不可实施的,并且基本上没有文档说明的协议。由此得到一种语言之外的机制(extralinguistic):无须构造器也能创建对象实例
3. Clone方法的通用约定是非常弱的
(1)x.clone() != x
(2)x.clone().getClass() == x.getClass()
(3)x.clone().equals(x)
但是,以上约定是没有绝对要求的。
4. 如果你覆盖了一个非final类的clone方法,则应该返回一个通过调用super.clone方法而得到的对象
5. 永远不要让客户去做那些类库可以替客户完成的事情
6. 实际上,clone方法就是另一个构造器,你必须确保它不会伤害到原始对象,并确保正确地创建被克隆对象中的约束条件(invariant)
7. clone架构与引用可变对象的final域的正常用法是不兼容的
8. 克隆对象的另一种方法是,调用super.clone(),然后将创建的对象的所有域置为空白状态,再调用高层方法来重新设置对象的状态
9. 如同构造器一样,clone方法不应该在构造的过程中,调用新对象中的任何非final方法
如果clone调用了一个被覆盖的方法,那么在该方法所在的子类有机会修正它在克隆对象中的状态之前,该方法就会先执行(?)
10. 如果你决定用线程安全的类来实现Cloneable接口,那么就要记得它的clone方法必须得到很好的同步
11. 简而言之,所有实现了Cloneable接口的类都应该用一个公有的方法覆盖clone。此公有方法实现步骤如下
(1)调用super.clone()
(2)修正任何需要修正的域
(3)拷贝任何包含内部“深层结构”的可变对象,并用新对象的引用代替原来指向这些对象的引用(深克隆)
12. 如果你扩展了一个实现了Cloneable接口的类,那么除了实现一个行为良好的clone方法之外,没有别的选择
13. 另一个实现对象拷贝的好办法是提供一个拷贝构造器(copy constructor)或拷贝工厂(copy factory)
(1)拷贝构造器
例如 public Yum(Yum yum)
(2)拷贝工厂
例如 public static Yum new Instance(Yum yum)
拷贝构造器与拷贝工厂相对于clone方法而言,有诸多的优势
14. 由于Cloneable具有诸多问题,因此为了继承而设计的类不应该去实现该接口,也不应该去覆盖clone方法
15. 一个专门为了继承而设计的类,如果你未能提供行为良好的protected clone方法,那么其子类就不可能实现Cloneable接口
第12条:考虑实现Comparable接口
1. 类实现了Comparable接口,就表明它的实例具有内在的排序关系
2. 实际上,java平台类库中的很多值类都实现了Comparable接口
3. 如果你正在编写一个值类,并且它具有非常明显的内在排序关系,比如按字母顺序、按数值顺序或者按年代顺序,那你应该坚决考虑实现该接口
4. 与equals不同的是,当跨越不同类的时候,comparaTo可以不做比较,直接抛出ClassCastException
5. 就像违反了hashCode约定的类会破坏其他依赖于hash算法的类一样,违反compareTo约定的类也会破坏其他依赖于比较关系的类
依赖于比较关系的类包括有序集合类TreeSet和TreeMap,以及工具类Collections和Arrays,它们内部包含有搜索与排序算法
第4章 类和接口
第13条:使类和成员的可访问性最小化
1. 设计良好的模块会隐藏所有的实现细节,把它的API与它的实现清晰地隔离开来。这个概念被称为信息隐藏或封装。信息隐藏可以有效地对模块进行解耦
2. java中提供了许多机制来协助信息隐藏
(1)访问控制机制
该机制决定了类、接口和成员的可访问性
3. 尽可能地使每个类或成员不被外界访问
如果类或者接口能够被做成包级私有的,那么它就应该被做成包级私有的。通过把类或者接口做成包级私有的,它实际上成为这个包的实现的一部分,而不是该包的导出API的一部分
4. 公有的类或者接口是包的导出API的一部分,类或者接口一旦被导出,你就有责任永远支持它,以保持它们的兼容性
5. 一个类的成员有四种
(1)域
(2)方法
(3)嵌套类
(4)嵌套接口
6. 受保护的成员是类的导出API的一部分,必须永远得到支持
7. 子类中的访问级别不允许低于超类中的访问级别,这样可以确保任何可以使用超类实例的地方也都可以使用子类实例
8. 接口中的所有域和方法都只能是public的,且域都是static的(即便没有显式定义static)
9. 为了便于测试,你可以试着使类、接口或者成员变得更容易访问。但是不能为了测试,而将类、接口或者成员变成包的导出API的一部分
10. 包含公有可变可变域的类并不是线程安全的
11. 如果final域包含可变对象的引用,那么它便具有非final域的所有缺点
12. 使用final来修饰一个数组,几乎是没意义的,而且是安全漏洞的一个常见根源,因为数组的内容是可以被任意改变的
第14条:在公有类中使用访问方法而非公有域
1. 如果类可以在它所在的包的外部进行访问,就提供访问方法,以保留将来改变该类的内部表示法的灵活性
2. 如果公有类暴露了它的数据域,那么将来改变其内部表示法是不可能的
3. 如果类是包级私有的,或者是私有的嵌套类,那么直接暴露它的数据域并没有本质的错误
4. 总之,公有类永远都不应该暴露可变的域,但是,有时候会需要用包级私有的或者私有的嵌套类来暴露域
第15条:使可变性最小化
1. 不可变类指的是其实例不能被修改的类,为了使类成为不可变,必须要遵循以下5条规则
(1)不提供任何会修改对象状态的方法(也成为mutator)
(2)保证类不会被扩展
(3)使所有的域都是final的
(4)使所有的域都成为私有的
(5)确保对于任何可变组件的互斥访问
如果类具有指向可变对象的域,则必须确保该类的客户端无法获得指向这些对象的引用。并且永远不要使用客户端提供的对象引用来初始化这样的域,也不要从任何访问方法(accessor)中返回该对象引用。在构造器、访问方法和readObject方法中要使用保护性拷贝技术(defensive copy)
2. 不可变类存在的意义:不可变类比可变类更加易于设计、实现和使用,它们不容易出错,且更加安全
3. 不可变对象可以只有一种状态,即被创建时的状态,可变对象则可以有任意复杂的状态空间
4. 不可变对象本质上是线程安全的,它们不要求同步,因此它可以被自由地共享
5. 不可变的类可以提供一些静态工厂,它们把频繁被请求的实例缓存起来
6. 在设计新的类时,选择用静态工厂代替公有的构造器可以让你以后有添加缓存的灵活性,而不必影响客户端
7. 不可变类真正唯一的缺点是,对于每个不同的值都需要一个单独的对象。不过,可以使用可变配套类(companing class)来弥补该缺点
(1)在java平台类库中,可变配套类的方法的主要例子是String类,它的可变配套类是StringBuilder
8. 让不可变的类变成final的另一种方法是,让类的所有构造器都变成私有或者包级私有的,并添加公有的静态工厂(static factory)来代替公有的构造器
(1)注意,工厂方法并不一定要单独封装到一个工厂类中,可以直接以静态工厂的方式存在于“产品”类中(o)
9. 值得注意的是,由于历史原因,BigInteger和BigDecimal这两个不可变类并没有被声明为final。因此,如果你在编写一个类,它的安全性依赖于不可信客户端传递进来的BigInteger或者BigDecimal参数的话,就必须进行检查,以确保这个参数是“真正”的BigInteger或者BigDecimal,而不是不可信任的子类的实例。如果是后者,则必须在假设它可能是可变的前提下对它进行保护性拷贝
10. 实际上,有时候为了性能,我们会对不可变类的诸多规则有所放松。例如,许多不可变类拥有一个或多个非final域,它们在第一次被请求执行这些计算的时候,把一些开销昂贵的计算结果缓存在这些域中。
11. 如果你选择让自己的不可变类实现Serializable接口,并且它包含一个或者多个指向可变对象的域,就必须提供一个显式的readObject或者readResolve方法,或者使用ObjectOutputStream.writeUnshared的ObjectInputStream.readUnshared方法,即使默认的序列化形式是可以接受的,也是如此,否则攻击者可能从不可变的类创建可变的实例。
12. 坚决不要为每个get方法编写一个相应的方法,除非有很好的理由要让类成为可变的类,否则就应该是不可变的。
13. 不可变类有许多优点,唯一的缺点就是在特定的情况下存在潜在的性能问题
14. 构造器应该创建完全初始化的对象,并建立起所有的约束关系。
15. 不要在构造器或者静态工厂之外再提供公有的初始化方法。同样的也不应该提供“重新初始化”方法,因为与所增加的复杂性相比,“重新初始化”方法通常并没有带来太多的性能优势
第16条:复合优先于继承
1. 在包的内部使用继承是非常安全的,然而,对普通的具体类进行跨越包边界的继承,则是非常危险的。
2. 与方法调用不同的是,继承打破了封装性。换句话说,子类依赖于超类中特定功能的实现细节,如果超类随着发行版本的变化而变化,那么子类可能会遭到破坏。
3. 注意,类A包含两个方法,分别是方法a和方法b(其中a方法调用了b方法),如果类B继承了类A,且覆盖了类A的方法a和方法b,在B.a()中调用了super.a(),那么如果main方法中调用了B.a(),则最终的调用链会是:main()——>B.a()——>A.a()——>B.b()(而非A.b())
4. 导致子类脆弱的一个相关的原因是,它们的超类在后续的发行版本中可以获得新的方法
5. 复合设计可以避免上面提到的因继承而引起的问题,它不依赖于组件类的实现细节,即使组件类添加了新的方法,也不会影响到引用类
6. 有时候,复合和转发的结合(即复合模式/decorator模式)也被错误地称为“委托(delegation)”。从技术的角度而言,这不是委托,除非包装对象把自身传递给被包装的对象
7. 包装类几乎没有什么缺点,但是需要注意的是,包装类不适合用在回调框架中
8. 只有当子类真正是超类的子类型时(即存在“is-a”关系),才适合用继承
9. 继承机制会把超类API中的所有缺陷传播到子类中,而复合则允许设计新的API来隐藏这些缺陷
10. 如果子类和超类处在不同的包中,并且超类并不是为了继承而设计的,那么继承将会导致脆弱性(fragility)。为了避免这种脆弱性,可以用复合和转发机制来代替继承,尤其是当存在适当的接口可以实现包装类的时候
11. 包装类不仅比子类更加健壮,而且功能也更加强大
第17条:要么为继承而设计,并提供文档说明,要么就禁止继承
1. 对于不是为了继承而设计,并且没有文档说明的“外来”类进行子类化是很危险的
2. 为了继承而设计的类,必须提供文档,说明它的可覆盖方法的自用性(self-use)。对于每个公有的或者受保护的方法,它的文档必须指明该方法调用了哪些可覆盖的方法
3. 好的API文档应该描述一个给定的方法做了什么工作,而不是描述它是如何做到的
4. 为了使程序员能够编写出更加有效的子类,而无需承受不必要的痛苦,类必须通过某种形式提供适当的钩子(hook),以便能够进入到它的内部工作流程中,这种形式可以是精心选择的受保护的方法,也可以是受保护的域,后者比较少见
5. 对于为了继承而设计的类,唯一的测试方法就是编写子类。经验表明,3个子类通常就足以测试一个可扩展的类
6. 为了允许继承,类还必须遵守其他一些约束
(1)构造器决不能调用可被覆盖的方法,无论是直接调用还是间接调用
7. 在为了继承而设计类的时候,Cloneable和Serializable接口出现了特殊的困难,因为它们把一些实质性负担转嫁到了扩展这个类的程序员的身上
8. 如果你决定在一个为了继承而设计的类中实现Cloneable或者Serializable接口,就应该意识到,因为clone和readObject方法在行为上非常类似于构造器,所以类似的限制规则也是适用的:无论是clone还是readObject,都不可以调用可覆盖的方法,不管是以直接还是间接的方式。
9. 对于那些并非为了安全地进行子类化而设计和编写文档的类,要禁止子类化,有两种办法
(1)把这个类声明成final
(2)把所有构造器都变成私有的,或者包级私有的,并增加一些公有的静态工厂来替代构造器
10. 你可以机械地消除类中可覆盖方法的自用特性,而不改变它的行为。将每个可覆盖方法的代码体移到一个私有的“辅助方法(helper method)”中,并且让每个可覆盖的方法调用它的私有辅助方法。然后,用“直接调用可覆盖方法的私有辅助方法”来代替“可覆盖方法的每个自调用”
第18条:接口优于抽象类
1. 现有的类可以很容易被更新,以实现新的接口
2. 接口是定义mixin类型的理想选择
mixin是指这样的类型,类除了实现它的“基本类型”之外,还可以实现这个mixin类型,以表明它提供了某些可供选择的行为
3. 接口允许我们构造非层次结构的类型框架
4. 通过包装类模式,接口使得安全地增强类的功能成为可能
5. 通过对你导出的每个重要接口都提供一个抽象的骨架实现(skeletal implementation)类,可以把接口和抽象类的优点结合起来。接口的作用仍然是定义类型,但是骨架实现类接管了所有与接口实现相关的工作。如果设计得当,骨架实现可以使程序员很容易提供他们自己的接口实现
6. 骨架实现类与静态工厂的结合可以使得我们无需通过继承来复用骨架类
7. 骨架实现的美妙之处在于,它们为抽象类提供了实现上的帮助,但又不强加“抽象类被用作类型定义时”所特有的严格限制
8. 骨架类仍然能够有助于接口的实现
实现了这个接口的类可以把对于接口方法的调用,转发到一个内部私有类的实例上,这个内部私有类扩展了骨架实现类。这种方法被称作模拟多重继承(simulated multiple inheritance)
9. 编写骨架实现类相对比较简单
(1)认真研究接口,并确定哪些方法是最为基本的(primitive),其他的方法则可以根据它们来实现
(2)将(1)中确认的基本方法设计为抽象方法
(3)为接口中剩余的所有方法提供具体的实现
10. 使用抽象类来定义允许多个实现的类型,与使用接口相比有一个明显的优势:抽象类的演变要比接口的演变容易得多。简单来说,即抽象类中新增一个方法要比接口中新增一个方法来得容易
11. 设计公有的接口要非常谨慎,因为接口一旦被公开发行,并且已被广泛实现,再想改变这个接口几乎是不可能的
12. 如果你导出了一个重要的接口,就应该坚决考虑同时提供骨架实现类
第19条:接口只用于定义类型
1. 常量接口模式是对接口的不良使用
所谓常量接口,即那些没有包含任何方法,只包含静态的final域的接口。如果非final类实现了常量接口,它的所有子类的命名空间也会被接口中的常量所污染
2. 导出常量的几种合理方案
(1)如果这些常量与某个现有的类或者接口紧密相关,就应该把这些常量添加到这个类或者接口中
(2)如果这些常量最好被看作枚举类型的成员,就应该用枚举类型来导出这些常量
(3)如果排除了(1)(2)两种情况,那么就应该使用不可实例化的工具类来导出这些常量
工具类通常要求客户端要用类名来修饰这些常量名,如果大量利用工具类导出的常量,可以利用静态导入机制来避免用类名来修饰常量名
第20条:类层次优于标签类(tagged class)
1. 所谓标签类,就是那些带有两种甚至更多种风格的实例的类,并包含表示实例风格的标签域
2. 标签类有着许多缺点,它们中充斥着样板代码,包括枚举声明、标签域以及条件语句,由于多个实现乱七八糟地挤在单个类中,破坏了可读性
3. 无法给标签类添加风格,除非可以修改它的源文件
4. 标签类过于冗长、容易出错,并且效率低下
5. 标签类正是类层次的一种简单的仿效
6. 类层次的另一种好处在于,它们可以用来反映类型之间本质上的层次关系,有助于增强灵活性,并进行更好的编译时类型检查
7. 当你遇到一个包含标签域的现有类时,就要考虑将它重构到一个层次结构中去
第21条:用函数对象表示策略
1. 有些语言支持函数指针(function pointer)、代理(delegate)、lambda表达式,或者支持类似的机制,允许程序把“调用特殊函数的能力”存储起来并传递这种能力。这种机制通常用于允许函数的调用者通过传入第二个函数,来指定自己的行为
2. java没有提供函数指针,但是可以用对象引用实现同样的功能
3. 如果一个类仅仅导出这样的一个方法,它的实例实际上就等同于一个指向该方法的指针。这样的实例被称为函数对象(function object)
4. 一个具体策略类是无状态的
5. 一个具体策略类的实例可以看做是一个函数对象
6. 带泛型的类或接口只是一种类模板而已,具体使用时是需要指定泛型所代表的具体类型的
7. 我们在设计具体策略类时,还需要定义一个策略接口
8. 具体的策略类往往使用匿名类声明
9. 因为策略接口被用作所有具有具体策略实例的类型,所以我们并不需要为了导出具体策略,而把具体策略类做成公有的。具体的策略类可以是宿主类(host class)的私有嵌套类
10. 函数指针的主要用途就是实现策略模式
为了在java中实现这种模式,要声明一个接口来表示该策略,并且为每个具体策略声明一个实现了该接口的类。当一个具体策略类只被使用一次时,通常使用匿名类来声明和实例化这个具体策略类。当一个具体策略类是设计用来重复使用的时候,它的类通常就要被实现为私有的静态成员类,并通过公有的静态final域来导出,其类型为该策略接口
第22条:优先考虑静态成员类
1. 嵌套类(nested class)是指被定义在另一个类的内部的类。嵌套类存在的目的应该只是为它的外围类(enclosing class)提供服务
2. 嵌套类有四种
(1)静态成员类
(2)非静态成员类
(3)匿名类
(4)局部类
除了第一种以外,其他三种都被成为内部类(inner class)
3. 静态成员类是最简单的一种嵌套类,最好把它看做是普通的类,只是碰巧被声明在另一个类的内部而已,它可以访问外围类的所有成员,包括那些声明为私有的成员
4. 静态成员类的一种常见用法是作为公有的辅助类,仅当与它的外部类一起使用时才有意义
5. 静态成员类与非静态成员类的区别
(1)非静态成员类的每个实例都隐含着与外围类的一个外围实例(enclosing instance)相关联
(2)非静态成员类的实例方法内部,可以调用外围实例上的方法,或者利用修饰过的this构造获得外围实例的引用
(3)如果嵌套类的实例可以在它外围类的实例之外单独存在,这个嵌套类就必须是静态成员类。因为在没有外围实例的情况下,要想创建非静态成员类的实例是不可能的
6. 当非静态成员类的实例被创建的时候,它和外围实例之间的关联关系也随之被建立起来
7. 非静态成员类的一种常见用法是定义一个Adapter,它允许外部类的实例被看作是另一个不相关的类的实例,例如Map接口的实现往往使用非静态成员类来实现它们的集合视图(collection view)
8. 在宿主类的外围实例化非静态成员类的语法
enclosingInstance.new MemberClass(args)
9. 如果声明成员类不要求访问外围实例,就要始终把static修饰符放在它的声明中,使它成为静态成员类
10. 如果在没有外围实例的情况下,也需要分配实例,就不能使用非静态成员类,因为非静态成员类的实例必须要有一个外围实例
11. 私有静态成员类的一种常见用法是用来代表外围类所代表的对象的组件
12. 匿名类不是外围类的一个成员,它并不与其他的成员一起被声明,而是在使用的同时被声明和实例化。
13. 当且仅当匿名类出现在非静态的环境中时,它才有外围实例
14. 匿名类可以出现在代码中任何允许存在表达式的地方
15. 匿名类的三种常见用法
(1)动态地创建函数对象(function object)
(2)创建过程对象(process object),例如Runnable、Thread、TimerTask实例
(3)在静态工厂方法的内部
16. 局部类是四种嵌套类中用得最少的类,在任何“可以声明局部变量”的地方,都可以声明局部类
17. 与匿名类一样,只有当局部类是在非静态环境中定义的时候,才有外围实例,它们也不能包含静态成员
第5章 泛型
第23条:请不要在新代码中使用原生态类型
1. 声明中具有一个或者多个类型参数(type parameter)的类或者接口,就是泛型(generic)类或者接口
2. 泛型类或者接口统称为泛型
3. 每种泛型定义一组参数化的类型
4. 每个泛型都定义一个原生态类型(raw type),即不带任何实际类型参数的泛型名称
例如,与List<E>相对应的原生态类型是List
5. 如果使用原生态类型,就失掉了泛型在安全性和表述性方面的所有优势
6. 泛型有子类型化(subtyping)的规则,List<String>是原生态类型List的一个子类型,而不是参数化类型List<Object>的子类型
7. 无限制的通配符类型(unbounded wildcard type)
如果使用泛型,但不确定或者不关心实际的类型参数,就可以使用一个问号代替(例如,Set<?>读作“某个类型的集合”)
8. 泛型信息可以在运行时被擦除
9. 在class literal中必须使用原生态类型
(1)List<String>.class和List<?>.class都是非法的
(2)在参数化类型上使用instanceof操作符是非法的
第24条:消除非受检警告
1. 要尽可能地消除每一个非受检警告
如果消除了所有警告,就可以确保代码是类型安全的
2. 如果无法消除警告,同时可以证明引起警告的代码是类型安全的
在这种情况下,则可以用一个@SuppressWarnings("unchecked")注解来禁止这条警告,SuppressWarnings注解可以用在任何粒度的级别中,从单独的局部变量声明到整个类都可以
3. 每当使用SuppressWarnings("unchecked")注解时,都要添加一条注释,说明为什么这么做是安全的
第25条:列表优先于数组
1. 数组与泛型相比,有两个重要的不同点
(1)数组是协变的(covariant)
如果sub为super的子类型,那么数组类型sub[]就是super[]的子类型;相反,List<sub>则不是List<super>的子类型
(2)数组是可具体化的(reified)
数组会在运行时才知道并检查它们的元素类型约束;相反,泛型则是通过擦除(erasure)来实现的,因此泛型只在编译时强化它们的类型信息,并在运行时丢弃它们的元素类型信息
2. 创建泛型、参数化类型或者类型参数的数组都是非法的,因为泛型数组不是类型安全的
3. 不可具体化的类型(non-reifiable type)是指其运行时表示法包含的信息比它的编译时表示法包含的信息更少的类型
4. 唯一可具体化的参数化类型是无限制的通配符类型
5. 在结合使用可变参数(varargs)方法和泛型时会出现令人费解的警告
这是由于每当调用可变参数方法时,就会创建一个数组来存放varargs参数
6. 当你得到泛型数组创建错误时,最好的解决办法通常是优先使用集合类型List<E>,而不是参数类型E[]
7. 记住,元素类型信息会在运行时从泛型中被擦除
第26条:优先考虑泛型
1. 一旦你证明了未受检的转换是安全的,就要在尽可能小的范围中禁止警告
2. 解决泛型数组创建错误有两种方案
(1)创建一个Object数组,并将它转换成泛型数组
E[] elements = (E[])new Object[3];
(2)直接声明Object数组,然后将从数组中获取到的元素转换成E
Object[] elements = new Object[3];
E result = (E) elements[0];
3. java并不是生来就支持列表,因此有些泛型如ArrayList,则必须在数组上实现。另外,为了提升性能,其他泛型如HashMap,也是在数组上实现的
4. 有限制的类型参数
<E extends OutputStream>,要求实际的类型参数必须是OutputStream的一个子类型
5. 使用泛型比使用需要在客户端代码中进行转换的类型来得更加安全,也更加容易
第27条:优先考虑泛型方法
1. 静态工具方法尤其适合于泛型化
2. 声明类型参数的类型参数列表,处在方法的修饰符及其返回类型之间
3. 泛型方法的一个显著特性是,无需明确指定类型参数的值。编译器通过检查方法参数的类型来计算类型参数的值,这个过程称作类型推导
4. 可以利用泛型方法调用所提供的类型推导,使创建参数化类型实例的过程变得更加轻松
5. 在调用泛型构造器的时候,要明确传递类型参数的值可能有点小麻烦,因为类型参数需要出现在变量声明的左右两边,显得有些冗余,对于这种情况,可以使用泛型静态工厂方法来解决
6. 虽然相对少见,但是通过某个包含该类型参数本身的表达式来限制类型参数是允许的(如<T extends Comparable<T>>),这就是递归类型限制(recursive type bound)
7. 泛型方法就像泛型一样,使用起来比要求客户端转换输入参数并返回值的方法来得更加安全,也更加容易
8. 就像类型一样,你应该确保新方法可以不用转换就能使用,这通常意味着要将它们泛型化
第28条:利用有限制通配符来提升API的灵活性
1. 利用有限制的通配符类型来解决参数化类型的子类型问题
例如Iterable<Integer>是不能应用于pushAll(Iterable<E> xx)方法中的,但是可以将pushAll方法设计成pushAll(Iterable<? extends E> xx)来解决此问题
2. PECS助记符突出了使用通配符类型的基本原则
(1)producer-extends (<? extends T>)
(2)consumer-super(<? super T>)
3. 不要用通配符类型作为返回类型
4. 如果使用得当,通配符类型对于类的用户来说几乎是无形的
5. 如果编译器不能推断你希望它拥有的类型,可以通过一个显式的类型参数(explicit type parameter)来告诉它要使用哪种类型
6. 类型参数和通配符之间具有双重性,许多方法都可以利用其中一个或者另一个进行声明
7. 如果类型参数只在方法声明中出现一次,就可以用通配符取代它
如果是无限制的类型参数,就用无限制的通配符取代它;如果是有限制的类型参数,就用有限制的通配符取代它
8. 在API中使用通配符类型虽然比较需要技巧,但是使API变得灵活得多
第29条:优先考虑类型安全的异构容器
1. 泛型最常用于集合,如Set和Map,以及单元素的容器,如ThreadLocal、Atomic和Reference
2. 将key进行参数化而不是将container进行参数化,可以实现以类型安全的方式访问数据库的所有列
3. 当一个类的字面文字被用在方法中,来传达编译时和运行时的类型信息时,就被称作type token
4. 类型安全的异构容器
例如:
public class Favorites{
public <T> void putFavorite(Class<T> type, T instance);
public <T> void getFavorite(Class<T> type);
}
5. 利用Class的cast方法,可以将对象引用动态地转换成Class对象所表示的类型
6. Favorites类有两种局限性值得注意
(1)恶意的客户端可以很轻松地破坏Favorites实例的类型安全,只要以它的原生态形式使用Class对象
(2)它不能用在不可具体化(non-reifiable)的类型中(因为我们无法为List<String>获得一个Class对象,List<String>.Class是个语法错误)
7. 注解API广泛地利用了有限制的类型令牌(类型令牌如Class<T>)
8. 被注解的元素本质上是个类型安全的异构容器,容器的键属于注解类型
9. Class类的asSubClass方法,可以将调用它的Class对象转换成用其参数表示的类的一个子类
10. 集合API说明了泛型的一般用法,限制你每个容器只能有固定数目的类型参数。你可以通过将类型参数放在键上而不是容器上来避开这一限制
11. 对于类型安全的异构容器,可以用Class对象作为键。以这种方式使用的Class对象称作类型令牌(type token)。你也可以使用定制的键类型
第6章 枚举和注解
第30条:用enum代替int常量
1. int枚举模式(int enum pattern)存在诸多不足,它在类型安全性和使用方便性方面没有任何帮助
2. java的枚举本质上是int值
java枚举类型背后的基本想法非常简单:它们就是通过公有的静态final域为每个枚举常量导出实例的类
3. 因为没有可以访问的构造器,所以枚举类型是真正的final
4. 枚举提供了编译时的类型安全
5. 为了将数据与枚举常量关联起来,得声明实例域,并编写一个带有数据并将数据保存在域中的构造器
6. 枚举天生就是不可变的,因此所有的域都应该为final的
7. 除非迫不得已要将枚举方法导出至它的客户端,否则都应该将它声明为私有的
8. 如果一个类具有普遍适用性,它就应该成为一个顶层类;如果它只是被用在一个特定的顶层类中,它就应该成为该顶层类的一个成员类
9. 特定于常量的方法实现(constant-specific class body),是将不同的行为与每个枚举常量关联起来的一种很好的方式
public enum Operation{
PLUS{double apply(double x, double y){return x+y;}},
MINUS{double apply(double x, double y){return x-y;}};
abstract double apply(double x, double y);
}
10. 枚举类型中的抽象方法必须被它所有常量中的具体方法所覆盖
11. 枚举类型有一个自动产生的valueOf(String)方法,它将常量的名字转变成变量本身
12. 如果在枚举类型中覆盖toString,要考虑编写一个fromString方法,将定制的字符串表示法变回相应的枚举
13. 特定于常量的方法有一个美中不足的地方,就是它们使得在枚举常量中共享代码变得更加困难了
14. 枚举中的switch语句适合于给外部的枚举类型增加特定于常量的行为
例如,public static Operation inverse(Operation op){
switch(op){
case PLUS:...;
case MINUS:...;
}
}
15. 与int常量相比,枚举类型的优势是不言而喻的
16. 如果多个枚举常量同时共享共同的行为,则考虑策略枚举。然后,多个枚举常量就可以将某些逻辑委托给指定的策略枚举对象
第31条:用实例域代替序数
1. 所有的枚举都有一个ordinal方法,它返回每个枚举常量在类型中的数字位置
除非你在编写的是类似EnumSet和EnumMap这种基于枚举的通用数据结构,否则最好完全避免使用ordinal方法
2. 永远不要根据枚举的序数导出与它关联的值,而是要将它保存在一个实例域中
第32条:用EnumSet代替位域
1. 这种用or位运算符将几个常量合并到一个集合中的方式,称作位域(bit field)
2. java.util包提供了EnumSet类来有效地表示从单个枚举类型中提取的多个值的多个集合
3. EnumSet类集位域的简洁和性能优势及第30条中所述的枚举类型的所有优点于一身
第33条:用EnumMap代替序数索引
1. java.util.EnumMap是一个非常快速的、专门用于枚举键的Map实现
2. EnumMap在运行速度方面之所以能与通过序数索引的数组相媲美,是因为EnumMap在内部使用了这种数组
3. 注意EnumMap构造器采用键类型的Class对象:这是一个有限制的类型令牌,它提供了运行时的泛型信息
4. 最好不要使用序数(枚举类型中的ordinal方法来索引数组,而要使用EnumMap)
第34条:用接口模拟可伸缩的枚举
1. <T extends Enum<T> & Operation> Class<T>确保了Class对象既表示枚举又表示Operation的子类型
2. 除非需要灵活地合并多个实现类型的操作,否则应该尽可能使用有限制的类型令牌
3. 总而言之,虽然无法编写可扩展的枚举类型,却可以通过编写接口以及实现接口的基础枚举类型,对它进行模拟
这样允许客户端编写自己的枚举来实现接口,如果API是根据接口编写的,那么在可以使用基础枚举类型的任何地方,也都可以使用这些枚举
第35条:注解优先于命名模式
1. java 1.5发行版本之前,一般使用命名模式(naming pattern)表明有些程序元素需要通过某种工具或者框架进行特殊处理
例如,JUnit测试框架原本要求它的用户一定要用test作为测试方法名称的开头
2. 命名模式的几个缺点
(1)文字拼写错误会导致失败,且没有任何提示
(2)无法确保它们只用于相应的程序元素上
(3)它们没有提供将参数值与程序元素关联起来的好方法
3. 注解很好地解决了命名模式的几个问题
4. Test注解类型的声明就是它自身通过Retention和Target注解进行了注解,注解类型声明中的这种注解被称作元注解(meta-annotation)
@Retention(RetentionPolicy.RUNTIME)元注解表明,Test注解应该在运行时保留;@Target(ElementType.METHOD)元注解表明,Test注解只在方法声明中才是合法的:它不能运用到类声明、域声明或者其他程序元素上
5. 注解永远不会改变被注解代码的含义,但是使它可以通过工具进行特殊的处理
6. 测试运行工具在命令行上使用完全匹配的类名,并通过调用Method.invoke反射式地运行类中所有标注了Test的方法。isAnnotationPresent方法告知该工具要运行哪些方法
7. 如果是在编写一个需要程序员给源文件添加信息的工具,就要定义一组适当的注解类型。既然有了注解,就完全没有理由再使用命名模式了
8. 除了“工具铁匠”(toolsmiths——特定的程序员)之外,大多数程序员都不必定义注解类型,但是所有的程序员都应该使用java平台提供的预定义的注解类型。还要考虑使用IDE或者静态分析工具所提供的任何注解,这种注解可以提升由这些工具所提供的诊断信息的质量
第36条:坚持使用Override注解
1. 应该在你想要覆盖超类声明的每个方法声明中使用Override注解
2. 现代的IDE提供了坚持使用Override注解的另一种理由,这种IDE具有自动检查功能,称作代码检验(code inspection)
3. 总而言之,如果在你想要的每个方法声明中使用Override注解来覆盖超类声明,编译器就可以替你防止大量的错误
第37条:用标记接口定义类型
1. 标记接口(marker interface)是没有包含方法声明的接口,而只是声明一个类实现了具有某种属性的接口
2. 标记接口胜过标记注解的两点
(1)标记接口定义的类型是由被标记类的实例实现的;标记注解则没有定义这样的类型
(2)它们可以被更加精确地进行锁定
3. 标记注解胜过标记接口的最大优点在于,它可以通过默认的方式添加一个或者多个注解类型元素,给已被使用的注解类型添加更多的信息
4. 何时使用标记接口,何时使用标记注解
(1)如果标记是应用到任何程序元素而不是类或者接口,就必须使用标记注解
(2)如果标记只应用给类或者接口,则优先考虑标记接口,这样可以用接口作为相关方法的参数类型,从而获得编译时类型检查的好处
5. 总而言之,标记接口和标记注解都各有用处
(1)如果想要定义一个任何新方法都不会与之关联的类型,标记接口就是最好的选择
(2)如果想要标记程序元素而非类和接口,考虑到未来可能要给标记添加更多的信息,或者标记要适合于已经广泛使用了注解类型的框架,那么标记注解就是正确的选择
第7章 方法
第38条:检查参数的有效性
1. 每当编写方法或者构造器的时候,应该考虑它的参数有哪些限制。应该把这些限制写到文档中,并且在这个方法体的开头处,通过显式的检查来实施这些限制
第39条:必要时进行保护性拷贝
1. 假设类的客户端会尽其所能来破坏这个类的约束条件,因此你必须保护性地设计程序
2. 不管是哪种情况,编写一些面对客户的不良行为时仍能保持健壮性的类,这是非常值得投入时间去做的事情
3. 对于构造器的每个可变参数进行保护性拷贝(defensive copy)是必要的
4. 保护性拷贝是在检查参数的有效性之前进行的,并且有效性检查是针对拷贝之后的对象,而不是针对原始对象
5. 需要注意的是,针对一个非final类进行保护性拷贝时,不应该使用其clone方法,因为clone方法可能返回专门出于恶意的目的而设计的不可信子类的实例
6. 对于参数类型可以被不可信任方子类化的参数,请不要使用clone方法进行保护性拷贝
7. 值得注意的是,如果一个类的访问方法提供了对其可变的内部成员的访问能力,那么该类就有受恶意攻击的危险。为了防止这种恶意攻击,可以通过修改该类的访问方法,使它返回可变内部域的保护性拷贝即可
8. 参数的保护性拷贝并不仅仅针对不可变类。每当编写方法或者构造器时,如果它要允许客户提供的对象进入到内部数据结构中,则有必要考虑一下,客户提供的对象是否有可能是可变的。在内部组件返回给客户端之前,对它们进行保护性拷贝也是同样的道理
9. 实际上,只要有可能,都应该使用不可变的对象作为对象内部的组件,这样就不必再为保护性拷贝操心
10. 简而言之,如果类具有从客户端得到或者返回到客户端的可变组件,类就必须保护性地拷贝这些组件。如果拷贝的成本受到限制,并且类信任它的客户端不会不恰当地修改组件,就可以在文档中指明客户端的职责是不得修改受到影响的组件,以此来代替保护性拷贝
第40条:谨慎设计方法签名
1. 谨慎地选择方法的名称
方法的名称应该始终遵循标准的命名习惯
2. 不要过于追求提供便利的方法
每个方法都应该尽其所能。方法太多会使接口实现者和接口用户的工作变得复杂起来
3. 避免过长的参数列表
目标是四个参数,或者更少。相同类型的长参数序列格外有害
4. 有三种方法可以缩短过长的参数列表
(1)把方法分解成多个方法,每个方法只需要这些参数的一个子集
(2)缩短长参数列表的第二种方法是创建辅助类(helper class),用来保存参数的分组。这些辅助类一般为静态成员类
如果一个频繁出现的参数序列可以被看做是代表了某个独特的实体,则建议使用这种方法
(3)从对象构建到方法调用都采用Builder模式
如果方法带有多个参数,尤其是当它们中有些是可选的时候,最好定义一个对象来表示所有参数
5. 通过提升一个类中所有方法的正交性(orthogonality),可以减少方法的数目
6. 对于参数类型,要优先使用接口而不是类
7. 对于boolean参数,要优先使用两个元素的枚举类型
它使代码更易于阅读和编写,也使以后更易于添加更多的选项,提升扩展性
第41条:慎用重载
1. 要调用哪个重载方法是在编译时做出决定的
2. 系统对于重载方法的选择是静态的,而对于被覆盖的方法的选择则是动态的
3. 应该避免胡乱地使用重载机制
安全而保守的策略是,永远不要导出两个具有相同参数数目的重载方法。如果方法使用可变参数,那么保守的策略是根本不要重载它
4. 可以考虑使用命名模式(即根据方法的差别来进行方法名称的变形)来替代重载方法
例如ObjectOutputStream的writeBoolean(xx)、writeInt(xx)
5. 一个类的多个构造器总是重载的
在许多情况下,可以选择导出静态工厂,而不是构造器
6. 在java1.5发行版本之前,所有的基本类型都根本不同于所有的引用类型,但是当自动装箱出现之后,就不再如此了,它会导致真正的麻烦
例如调用List<Integer>对象的remove(xx)方法,就可能因为自动拆箱机制而导致bug
7. java语言中添加了泛型和自动装箱之后,破坏了List接口
自动装箱和泛型成了java语言的一部分之后,谨慎重载显得更加重要了
8. 在java1.5发行版本中,新增了一个称作CharSequence的接口,用来为StringBuffer、StringBuilder、String、CharBuffer以及其他类似的类型提供公共接口,为实现这个接口,对它们全都进行了改造
9. 让更具体化的重载方法把调用转发给更一般化的重载方法可以确保参数列表存在父子类关系的重载方法返回相同的结果(参p169)
10. 一般情况下,对于多个具有相同参数数目的方法来说,应该尽量避免重载方法
11. 在没法避免使用重载方法的情况下,至少应该避免这样的情形:同一组参数只需经过类型转换就可以被传递给不同的重载方法
如果不能避免这种情形,那么就应该保证:当传递同样的参数时,所有重载方法的行为必须一致
第42条:慎用可变参数
1. 可变参数机制通过先创建一个数组,数组的大小为在调用位置所传递的参数数量,然后将参数值传到数组中,最后将数组传递给方法
2. 有时候,有必要编写需要1个或者多个某种类型参数的方法,而不是需要0个或者多个,后者可能会增加额外的参数个数检查,还可能导致运行时异常
3. 可变参数是为printf而设计的,它是在java1.5发行版本中添加到平台中的。printf和反射机制都从可变参数中极大地受益
4. 可以将以数组当做final参数的现有方法,改造成以可变参数代替,而不影响现有的客户端,但是可以并不意味着应该这么做
5. Arrays.asList方法现在“增强”为使用可变参数,将int类型的数组digits的对象引用集中到数组的单个元素数组中,并忠实地将它包装到List<int[]>实例中
6. 在java1.5发行版本中,Arrays类得到了补充完整的Arrays.toString方法,专门为了将任何类型的数组转变成字符串而设计的
7. 在重视性能的情况下,使用可变参数机制要特别小心
可变参数方法的每次调用都会导致进行一次数组分配和初始化
8. 简而言之,在定义参数数目不定的方法时,可变参数方法是一种很方便的方式,但是它们不应该被过度滥用。如果使用不当,会产生混乱的结果
第43条:返回零长度的数组或者集合,而不是null
1. 零长度数组是不可变的,而不可变对象有可能被自由地共享(见第15条),所以无需担心返回零长度数组比返回null多出的性能开销
2. Collection.toArray(T[])的规范保证:如果输入数组大到足够容纳这个集合,它就将返回这个输入数组
3. 返回类型为数组或集合的方法没理由返回null
第44条:为所有导出的API元素编写文档注释
1. 为了正确地编写API文档,必须在每个被导出的类、接口、构造器、方法和域声明之前增加一个文档注释。如果类是可序列化的,也应该对它的序列化形式编写文档
2. 方法的文档注释应该简洁地描述出它和客户端之间的约定
3. 文档注释应该列举出这个方法的所有前提条件(precondition)和后置条件(postcondition)
4. 每个未受检的异常都对应一个前提违例(precondition violation)。同样地,也可以在一些受影响的参数的@param标记中指定前提条件
5. 除了前提条件和后置条件之外,每个方法还应该在文档中描述它的副作用(side effect)
所谓副作用是指系统状态中可以观察到的变化(该变化不是为了获得后置条件而明确要求的变化)
6. 文档注释也应该描述类或者方法的线程安全性(thread safety)
7. 对于方法抛出的每个异常,无论是受检的还是未受检的,都应该有一个@throws标签
8. 按惯例,跟在@throws标签之后的文字应该包含单词“if”,紧接着是一个名词短语
9. 按惯例,@param、@return、@throws标签后面的短语或者子句都不用句点来结束
10. Javadoc工具会把文档注释翻译成HTML,文档注释中包含的任意HTML元素都会出现在结果HTML文档中
11. 为了产生包含HTML元字符的文档(例如“<”、“>”以及“&”)的最佳办法是用{@literal}标签将它们包围起来,这样就限制了HTML标记和嵌套的Javadoc标签的处理
12. 文档注释在源代码和产生的文档中都应该是易于阅读的
13. 为了避免混淆,同一个类或者接口中的两个成员或者构造器,不应该具有同样的概要描述
14. 方法和构造器的概要描述往往是个动词短语
15. 类、接口和域的概要描述往往是个名词短语
16. Java1.5发行版本中增加的三个特性在文档注释中需要特别小心:泛型、枚举和注解。
(1)当为泛型或者方法编写文档时,确保要在文档中说明所有的类型参数
(2)当为枚举类型编写文档时,要确保在文档中说明常量,以及类型还有任何公有的方法
(3)当为注解类型编写文档时,要确保在文档中说明所有成员,以及类型本身
对于该类型的概要描述,要使用一个动词短语,说明当程序元素具有这种类型的注解时它表示什么意思
17. 类的导出API有两个特征经常被人忽视,即线程安全性和可序列化性
如果类是可序列化的,就应该在文档中说明它的序列化形式
18. 为了降低文档注释中出错的可能性,一种简单的办法是通过一个HTML有效性检查器来运行由Javadoc产生的HTML文件
19. 简而言之,要为API编写文档,文档注释是最好、最有效的途径
20. 对于所有可导出的API元素来说,使用文档注释应该被看做是强制性的
21. 在文档注释内部出现任何HTML标签都是允许的,但是HTML元字符必须要经过转义
第8章 通用程序设计
第45条:将局部变量的作用域最小化
1. 要使局部变量的作用域最小化,最有力的方法就是在第一次使用它的地方声明。如果变量在使用之前进行声明,这只会造成混乱
2. 过早地声明局部变量不仅会使它的作用域过早地扩展,而且结束得也过于晚了
3. 几乎每个局部变量的声明都应该包含一个初始化表达式。如果你还没有足够的信息来对一个变量进行有意义的初始化,就应该推迟这个声明
4. 如果在循环终止之后不再需要循环变量的内容,for循环就优先于while循环
5. 使用for循环与使用while循环相比还有另外一个优势:更简短,从而增强了可读性
6. 最后一种“将局部变量的作用域最小化”的方法是:使方法小而集中
第46条:for-each循环优先于传统的for循环
1. java1.5发行版中引入的for-each循环,通过完全隐藏迭代器或者索引变量,避免了混乱和出错的可能。这种模式同样适用于集合和数组
2. 注意,利用for-each循环不会有性能损失,甚至用于数组也一样
3. for-each循环不仅让你遍历集合和数组,还让你遍历任何实现Iterable接口的对象
4. 总之,for-each循环在简洁性和预防bug方面有着传统的for循环无法比拟的优势,并且没有性能损失。
5. 有三种常见的情况无法使用for-each循环
(1)过滤:如果需要遍历集合,并删除选定的元素,就需要使用显式的迭代器,以便可以调用它的remove方法
(2)转换:如果需要遍历列表或者数组,并取代它部分或者全部的元素值,就需要列表迭代器或者数组索引,以便设定元素的值
(3)平行迭代:如果需要并行地遍历多个集合,就需要显式地控制迭代器或者索引变量,以便所有迭代器或者索引变量都可以得到同步前移
第47条:了解和使用类库
1. 通过使用标准类库,可以充分利用这些编写标准类库的专家的知识,以及在你之前的其他人的使用经验
2. 每个程序员都应该熟悉java.lang、java.util,某种程度上还有java.io中的内容
3. 总之,不要重新发明轮子
第48条:如果需要精确的答案,请避免使用float和double
1. float和double类型尤其不适合用于货币计算,因为要让一个float或者double精确地表示0.1是不可能的
2. 取而代之的是,可以使用BigDecimal、int或者long进行货币计算
3. 使用BigDecimal有两个缺点:与使用基本运算类型相比,这样做很不方便,而且很慢
4. 总而言之,对于任何需要精确答案的计算任务,请不要使用float或者double
(1)如果你想让系统来记录十进制小数点,并且不介意因为不使用基本类型而带来的不便,就请使用BigDecimal
(2)如果性能非常关键,并且你又不介意自己记录十进制小数点,而且所涉及的数值又不太大,就可以使用int或者long
第49条:基本类型优先于装箱基本类型
1. java有一个类型系统由两部分组成,包含基本类型和引用类型
2. 对装箱基本类型运用==操作符几乎总是错误的
3. 当在一项操作中混合使用基本类型和装箱基本类型时,装箱基本类型就会自动拆箱
4. 如果null对象引用被自动拆箱,就会得到一个NullPointerException异常
5. 那么什么时候应该使用装箱基本类型呢?以下是几个合理的用处
(1)作为集合中的元素、键和值
(2)参数化类型中,必须使用装箱基本类型作为类型参数
(3)在使用反射的方法调用时,必须使用装箱基本类型
6. 总之,当可以选择的时候,基本类型要优于装箱基本类型
7. 自动装箱减少了使用装箱基本类型的繁琐性,但是并没有减少它的风险
8. 当程序装箱了基本类型值时,会导致高开销和不必要的对象创建
第50条:如果其他类型更适合,则尽量避免使用字符串
1. 字符串不适合代替其他的值类型
2. 字符串不适合代替枚举类型
枚举类型比字符串更加适合用来表示枚举类型的常量
3. 字符串不适合代替聚集类型
如果一个实体有多个组件,用一个字符串来表示这个实体通常是很不恰当的
4. 字符串也不适合代替能力表(capabilities)
5. 若使用不当,字符串会比其他的类型更加笨拙、更不灵活、速度更慢,也更容易出错
6. 经常被错误地用字符串代替的类型包括基本类型、枚举类型和聚集类型
第51条:当心字符串连接的功能
1. 为连接n个字符串而重复地使用字符串连接操作符,需要n的平方级的时间,这是由于字符串不可变而导致的不幸结果
2. 为了获得可以接受的性能,请使用StringBuilder替代String
3. 不要使用字符串连接操作符来合并多个字符串,除非性能无关紧要
第52条:通过接口引用对象
1. 应该优先使用接口而不是类来引用对象
如果有合适的接口类型存在,那么对于参数、返回值、变量和域来说,就都应该使用接口类型进行声明
2. 如果代码中依赖于一个具体实现类的任何特殊属性,那么就要在声明变量的地方给这些需求建立相应的文档说明
3. 不存在适当接口类型的几种情形
(1)如果没有合适的接口存在,完全可以用类而不是接口来引用对象
更一般地讲,如果具体类没有相关联的接口,不管它是否表示一个值,你都没有别的选择,只有通过它的类来引用它的对象
(2)对象属于一个框架,而框架的基本类型是类,不是接口
如果对象属于这种基于类的框架(class-based framework),就应该用相关的基类来引用这个对象,而不是用它的实现类
java.util.TimerTask抽象类就属于这种情形
(3)类实现了接口,但是它提供了接口中不存在的额外方法
如果程序依赖于这些额外方法,这种类就应该只被用来引用它的实例,它很少应该被用作参数类型
4. 实际上,给定的对象是否具有适当的接口应该是很显然的。如果是,用接口引用对象就会使程序更加灵活;如果不是,则使用类层次结构中提供了必要功能的最基础的类
第53条:接口优先于反射机制
1. 核心反射机制(core reflection facility)java.lang.reflect,提供了“通过程序来访问关于已装载的类的信息”的能力(即内省机制)
2. Constructor、Method和Field实例使你能够通过反射机制操作它们的底层对等体。通过调用Constructor、Method和Field实例上的方法,可以构造底层类的实例、调用底层类的方法,并访问底层类的域。
例如,Method.invoke使你可以调用任何类的任何对象上的任何方法(遵从常规的安全限制)
3. 反射机制所需要付出的代价
(1)丧失了编译时类型检查的好处
(2)执行反射访问所需要的代码非常笨拙和冗长
(3)性能损失
4. 反射功能只是在设计时(design time)被用到。通常,普通应用程序在运行时不应该以反射方式访问对象
5. 类对于在运行时可能不存在的其他类、方法或者域的依赖性,用反射法进行管理,这种方法是合理的,但是很少使用
6. 如果你编写的程序必须要与编译时未知的类一起工作,如有可能,就应该仅仅使用反射机制来实例化对象,而访问对象时则使用编译时已知的某个接口或者超类
第54条:谨慎地使用本地方法
1. 从历史上看,本地方法主要有三种用途
(1)提供了“访问特定于平台的机制”的能力
例如访问注册表和文件锁
(2)提供了访问遗留代码库的能力
(3)本地方法可以通过本地语言,编写应用程序中注重性能的部分,以提高系统的性能
2. 使用本地方法来提高性能的做法不值得提倡
3. 使用本地方法之前务必三思
第55条:谨慎地进行优化
1. 不要因为性能而牺牲合理的结构。要努力编写好的程序而不是快的程序
2. 好的程序体现了信息隐藏(information hiding)的原则
只要有可能,它们就会把设计决策集中在单个模块中
3. 努力避免那些限制性能的设计决策
4. 要考虑API设计决策的性能后果
(1)使公有的类型成为可变的(mutable),这可能会导致大量不必要的保护性拷贝
(2)在适合使用组合模式的公有类中使用继承,会把这个类与它的超类永远地束缚在一起,从而人为地限制了子类的性能
(3)在API中使用实现类型而不是接口,会把你束缚在一个具体的实现上,即使将来出现更快的实现你也无法使用
5. 一般而言,好的API设计也会带来好的性能
为获得好的性能而对API进行包装,这是一种非常不好的想法
6. 在每次试图做优化之前和之后,要对性能进行测量
7. 性能剖析工具有助于你决定应该把优化的重心放在哪里
8. 在Java平台上对优化的结果进行测量,比在其他的传统平台上更有必要,因为Java程序设计语言没有很强的性能模型。各种基本操作的相对开销也没有明确定义,程序员所编写的代码与CPU执行的代码之间存在“语义沟”(semantic gap)
9. 总而言之,不要费力去编写快速的程序——应该努力编写好的程序,速度自然就会随之而来
10. 性能优化的第一步骤应该是检查所选择的算法
再多的底层优化也无法弥补算法的选择不当
第56条:遵守普遍接受的命名惯例
1. 包名称的命名惯例
(1)任何将在你的组织之外使用的包,其名称都应该以你的组织的Internet域名开头,并且顶级域名放在前面
(2)用户创建的包的名称绝不能以java和javax开头
(3)包名称的其余部分应该包括一个或者多个描述该包的组成部分。这些组成部分应该比较简短,通常不超过8个字符
(4)鼓励使用有意义的缩写形式
例如,使用util而不是utilities。只取首字母的缩写形式也是可以接受的,例如awt
2. 类和接口的命名惯例
(1)应该尽量避免缩写,除非是一些首字母缩写和一些通用的缩写
3. 方法和域的名称与类和接口的名称一样,都遵守相同的字面惯例
4. 注意,常量域是唯一推荐使用下划线的情形
5. 局部变量名称的字面命名惯例和成员名称类似,只不过它也允许缩写,单个字符和短字符序列的意义取决于局部变量所在的上下文环境
6. 类型参数名称通常由单个字母组成。这个字母通常是以下五种类型之一
(1)T表示任意的类型
(2)E表示集合的元素类型
(3)K表示映射的键类型
(4)V表示映射的值类
(5)X表示异常
7. 语法命名惯例比字面惯例更加灵活,也更有争议
(1)类(包括枚举类型)通常用一个名词或者名词短语命名
(2)执行某个动作的方法通常用动词或者动词短语来命名
(3)对于返回boolean值的方法,其名称往往以单词“is”开头
(4)如果方法返回被调用对象的一个非boolean的函数或者属性,它通常用名词、名词短语,或者以动词“get”开头的动词短语来命名
(5)转换对象类型的方法、返回不同类型的独立对象的方法,通常被称为toType
例如toString和toArray
(6)返回视图(view,视图的类型不同于接收对象的类型)的方法通常被称为asType
例如asList
(7)返回一个与被调用对象同值的基本类型的方法,通常被称为typeValue
例如intValue
(8)静态工厂的常用名称为valueOf、of、getInstance、newInstance、getType和NewType
8. 域名称的语法惯例没有很好地建立起来,也没有类、接口和方法名称的惯例那么重要,因为设计良好的API很少会包含暴露出来的域
9. boolean类型的域命名与boolean类型的访问方法很类似,但是省去了初始的“is”。其他类型的域通常用名词或者名词短语来命名
第9章 异常
第57条:只针对异常的情况才使用异常
1. 异常机制的设计初衷是用于不正常的情形,所以很少会有JVM实现试图对它们进行优化,使得与显式的测试一样快速
2. 把代码放在try-catch块中有可能会阻止现在JVM实现本来可能要执行的某些特定优化
3. 实际上,在现代JVM实现上,基于异常的模式比标准模式要慢得多
4. 顾名思义,异常应该只用于异常的情况下,它们永远不应该用于正常的控制流
5. 设计良好的API不应该强迫它的客户端为了正常的控制流而使用异常
如果类具有“状态相关(state-dependent)”的方法(即只有在特定的不可预知的条件下才可以被调用的方法),这个类往往也应该有个单独的“状态测试(state-testing)”方法,即指示是否可以调用这个状态相关的方法。例如Iterator接口的next()与hasNext()
6. 另一种提供单独的状态测试方法的做法是,如果“状态相关”的方法被调用时,该对象处于不适当的状态之中,则它就会返回一个可识别的值,比如null
7. 对于“状态测试方法”和“可识别的返回值”这两种做法,有些指导原则可以帮助你在两者之中做出选择
(1)如果对象将在缺少外部同步的情况下被并发访问,或者可被外界改变状态,那么使用可识别的返回值可能是很有必要的
(2)如果单独的“状态测试”方法必须重复“状态相关”方法的工作,从性能角度考虑,就应该使用可被识别的返回值
(3)如果所有其他方面都是等同的,那么“状态测试”方法则略优于可被识别的返回值,因为它具有更好的可读性,而且对于使用不当的情形,可能更易于检测和改正:如果忘了去调用状态测试方法,状态相关的方法就会抛出异常,使这个bug变得很明显;如果忘了去检查可识别的返回值,这个bug将很难会被发现
8. 总而言之,异常是为了在异常情况下使用而设计的。不要将它们用于普通的控制流,也不要编写迫使它们这么做的API
第58条:对可恢复的情况使用受检异常,对编程错误使用运行时异常
1. java语言提供了三种可抛出结构(throwable)
(1)受检异常(checked exception)
(2)运行时异常(run-time exception)
(3)错误(error)
2. 如果期望调用者能够适当地从异常情形中恢复,对于这种情况就应该使用受检的异常
通过抛出受检的异常,强迫调用者在一个catch子句中处理该异常,或者将它传播出去
3. API的设计者让API用户面对受检的异常,以此强制用户从这个异常条件中恢复
4. 如果程序抛出未受检的异常或者错误,往往就属于不可恢复的情形,继续执行下去有害无益
5. 用运行时异常来表明编程错误。大多数的运行时异常都表示前提违例(precondition violation)
6. 你实现的所有未受检的抛出结构都应该是RuntimeException的子类(直接或间接的)
7. 总而言之,对于可恢复的情况,使用受检异常;对于程序错误,则使用运行时异常
8. “解析异常的字符串表示法”的代码可能是不可移植的,也是非常脆弱的
9. 因为受检的异常往往指明了可恢复的条件,所以,对于这样的异常,提供一些辅助方法尤其重要,通过这些方法,调用者可以获得一些有助于恢复的信息
第59条:避免不必要地使用受检的异常
1. 过分使用受检的异常会使API使用起来非常不方便
2. 如果正确地使用API并不能阻止这种异常条件的产生,并且一旦产生异常,使用API的程序员立即采取有用的动作,这种负担就被认为是正当的。除非这两个条件都成立,否则更适合于使用未受检的异常
3. 异常受检的本质并没有为程序员提供任何好处,它反而需要付出努力,还使程序更为复杂
4. 把受检的异常变成未受检的异常的一种方法是,把这个抛出异常的方法分成两个方法,其中第一个方法返回一个boolean,表明是否应该抛出异常。不过这种重构并不总是恰当的,其本质上就是“状态测试方法”
第60条:优先使用标准的异常
1. 专家级程序员与缺乏经验的程序员的一个最主要区别在于,专家追求并且通常也能够实现高度的代码重用
2. 重用现有的异常有多方面的好处
(1)它使你的API更加易于学习和使用
(2)对于用到这些API的程序而言,它们的可读性会更好
(3)异常类越少,意味着内存印迹(footprint)就越小,装载这些类的时间开销也越少
3. 几个比较经常被重用的异常
(1)IllegalArgumentException
(2)IllegalStateException
(3)NullPointerException
(4)IndexOutOfBoundsException
(5)ConcurrentModificationException
如果一个对象被设计为专用于单线程或者与外部同步机制配合使用,一旦发现它正在(或已经)被并发地修改,就应该抛出这个异常
(6)UnsupportedOperationException
如果对象不支持所请求的操作,就会抛出这个异常
4. 一定要确保抛出异常的条件与该异常的文档中描述的条件一致。这种重用必须建立在语义的基础上,而不是建立在名称的基础之上
第61条:抛出与抽象相对应的异常
1. 如果方法抛出的异常与它所执行的任务没有明显的联系,这种情形将会使人不知所措。当方法传递由低层抽象抛出的异常时,往往会发生这种情况
2. 更高层的实现应该捕获低层的异常,同时抛出可以按照高层抽象进行解释的异常。这种做法被称为异常转译(exception translation)
3. 一种特殊的异常转译形式称为异常链(exception chaining),如果低层的异常对于调试导致高层异常的问题非常有帮助,使用异常链就很合适
throw new HigherLevelException(cause),其中cause为低层异常对象
4. 大多数标准的异常都有支持链的构造器
异常链不仅让你可以通过程序访问原因,它还可以将原因的堆栈轨迹集成到更高层的异常中
5. 尽管异常转译与不加选择地从低层传递异常的做法相比有所改进,但是它也不能被滥用
6. 处理来自低层异常的最好做法是,在调用低层方法之前确保它们会成功执行,从而避免它们抛出异常
7. 总而言之,如果不能阻止或者处理来自更低层的异常,一般的做法是使用异常转译
8. 异常链对高层和低层异常都提供了最佳的功能:它允许抛出适当的高层异常,同时又能捕获底层的原因进行失败分析
第62条:每个方法抛出的异常都要有文档
1. 描述一个方法所抛出的异常,是正确使用这个方法时所需文档的重要组成部分
2. 始终要单独地声明受检的异常,并且利用javadoc的@throws 标记,准确地记录下抛出每个异常的条件
3. 虽然java语言本身并不要求程序员为一个方法声明它可能会抛出的未受检异常,但是,如同受检异常一样,仔细地为它们建立文档是非常明智的
对于方法可能抛出的未受检异常,如果将这些异常信息很好地组织成列表文档,就可以有效地描述出这个方法被成功执行的前提条件
4. 使用javadoc的@throws 标记记录下一个方法可能抛出的每个未受检异常,但是不要使用throws关键字将未受检的异常包含在方法的声明中
5. 如果一个类中的许多方法出于同样的原因而抛出同一个异常,在该类的文档注释中对这个异常建立文档,这是可以接受的
6. 总而言之,要为你编写的每个方法所能抛出的每个异常建立文档
第63条:在细节消息中包含能捕获失败的信息
1. 异常类型的toString方法应该尽可能多地返回有关失败原因的信息,这一点特别重要
换句话说,异常的细节信息应该捕获住失败,便于以后分析
2. 为了捕获失败,异常的细节信息应该包含所有“对该异常有贡献”的参数和域的值
3. 虽然在异常的细节信息中包含所有相关的“硬数据(hard data)”是非常重要的,但是包含大量的描述信息往往没有什么意义
4. 与用户层次的错误信息不同,异常的字符串表示法主要是让程序员或者域服务人员用来分析失败原因的。因此,信息的内容比可理解性要重要得多
5. 为异常的“失败捕获”信息提供一些访问方法是合适的
第64条:努力使失败保持原子性
1. 一般而言,失败的方法调用应该使对象保持在被调用之前的状态
具有这种属性的方法被称为具有失败原子性(failure atomic)
2. 获得失败原子性最常见的办法是,在执行操作之前检查参数的有效性。这可以使得在对象的状态被修改之前,先抛出适当的异常。
3. 一种类似的获得失败原子性的办法是,调整计算处理过程的顺序,使得任何可能会失败的计算部分都在对象状态被修改之前发生
4. 第三种获得失败原子性的办法并不常用,做法是编写一段恢复代码(recovery code),由它来拦截操作过程中发生的失败,以及使对象回滚到操作开始之前的状态上。这种办法主要用于永久性的数据结构(基于磁盘的)
5. 最后一种获得失败原子性的办法是,在对象的一份临时拷贝上执行操作,当操作完成之后再用临时拷贝中的结果代替对象的内容
例如,Collections.sort在执行排序之前,首先把它的输入列表转到一个数组中
6. 错误(相对于异常)通常是不可恢复的,当方法抛出错误时,它们不需要努力保持失败原子性
7. 一般而言,作为方法规范的一部分,产生的任何异常都应该让对象保持在该方法调用之前的状态
第65条:不要忽略异常
1. 空的catch块会使异常达不到应有的目的,即强迫你处理异常的情况。至少,catch块也应该包含一条说明,解释为什么可以忽略这个异常
2. 只要将异常传播给外界,至少会导致程序迅速地失败,从而保留了有助于调试该失败条件的信息
第10章 并发
第66条:同步访问共享的可变数据
1. java语言规范保证读或者写一个变量是原子的(atomic),除非这个变量的类型为long或者double
2. 你可能听说过,为了提高性能,在读或者写原子数据的时候,应该避免使用同步。这个建议是非常危险而错误的
3. 为了在线程之间进行可靠的通信,也为了互斥访问,同步是必要的
这归因于java语言规范中的内存模型(memory model),它规定了一个线程所做的变化何时以及如何变成对其他线程可见
4. 如果对共享的可变数据的访问不能同步,其后果将非常可怕,即使这个变量是原子可读写的。
5. 要阻止一个线程妨碍另一个线程,建议做法是让第一个线程轮询(poll)一个boolean域,这个域一开始为false,但是可以通过第二个线程设置为true,以表示第一个线程将终止自己。如果读写boolean域不使用同步,那么程序很可能会出问题,即便boolean域的读写操作都是原子的。(参p230)
6. 实际上,如果读和写操作没有都被同步,同步就不会起作用
7. StopThread中被同步方法的动作即使没有同步也是原子的。换句话说,这些方法的同步只是为了它的通信效果,而不是为了互斥访问。(参p231)
8. 虽然volatile修饰符不执行互斥访问,但它可以保证任何一个线程在读取该域的时候都将看到最近刚刚被写入的值
9. 值得注意的是,增量操作符(++)不是原子的。它相当于执行了两项操作:首先读取值,并执行递增操作,然后回写
这个问题,可以用AtomicLong.getAndIncrement()来改进
10. 要么共享不可变的数据,要么压根不共享。换句话说,将可变数据限制在单个线程中
如果采用这一策略,那么对它建立文档就很重要,以便它可以随着线程的发展而得到维护
11. 将这种对象引用(effectively immutable object)从一个线程传递到其他的线程被称作安全发布(safe publication)
12. 安全发布对象引用有许多种方法
(1)可以将它保存在静态域中,作为类初始化的一部分
(2)可以将它保存在volatile域、final域或者通过正常锁定访问的域中
(3)可以将它放到并发的集合中
13. 简而言之,当多个线程共享可变数据的时候,每个读或者写数据的线程都必须执行同步
14. 未能同步共享可变数据会造成程序的活性失败(liveness failure)和安全性失败(safety failure)。这样的失败最难以调试
第67条:避免过度同步
1. 为了避免活性失败和安全性失败,在一个被同步的方法或者代码块中,永远不要放弃对客户端的控制
换句话说,在一个被同步的区域内部,不要调用设计成要被覆盖的方法,或者是由客户端以函数对象的形式提供的方法
2. 在同步区域内,不要调用外来方法
根据外来方法的作用,从同步区域中调用它会导致异常、死锁或者数据损坏
3. 我们正企图在遍历列表的过程中,将一个元素从列表中删除,这是非法的
4. 从同步区域中调用外来方法,在真实的系统中已经造成了许多死锁(参p236)
5. 可重入的锁简化了多线程的面向对象程序的构造,但是它们可能会将活性失败变成安全性失败
6. 将外来方法的调用移出同步代码块有两种方式
(1)给同步块中操作的数据对象或者数据结构拍快照,随后在同步块之外再针对快照的对象进行外部调用(参p237)
(2)java1.5之后的版本,可以使用并发集合,称之为CopyOnWriteArrayList,这是专门为此定制的
7. CopyOnWriteArrayList是ArrayList的一种变体,通过重新拷贝整个底层数据,在这里实现所有的写操作。由于内部数组永远不改动,因此迭代不需要锁定,速度也非常快
8. 在同步区域之外被调用的外来方法被称作“开放调用(open call)”。除了可以避免死锁之外,开放调用还可以极大地增加并发性。外来方法的运行时间可能会任意长
9. 通常,你应该在同步区域内做尽可能少的工作
(1)获得锁
(2)检查共享数据
(3)根据需要转换数据
(4)释放锁
10. 在这个多核的时代,过度同步的实际成本并不是指获取锁所花费的CPU时间,而是指失去了并行的机会,以及因为需要确保每个核都有一个一致的内存视图而导致的延迟。过度同步的另一项潜在开销在于,它会限制VM优化代码执行的能力
11. 如果一个可变的类要并发使用,应该使这个类变成是线程安全的
通过内部同步,你还可以获得明显比从外部锁定整个对象更高的并发性。否则(即非并发使用的情况),就不要在内部同步,让客户在必要的时候从外部同步。
12. 当你不确定的时候,就不要同步你的类,而是应该建立文档,注明它不是线程安全的
13. 如果你在内部同步了类,就可以使用不同的方法来实现高并发性,例如
(1)分拆锁(lock splitting)
(2)分离锁(lock striping)
(3)非阻塞(nonblocking)并发控制
14. 如果方法修改了静态域,那么你也必须同步对这个域的访问,即使它往往只用于单个线程。
客户要在这种方法上执行外部同步是不可能的,因为不可能保证其他不相关的客户也会执行外部同步
15. 简而言之,为了避免死锁和数据破坏,千万不要从同步区域内部调用外来方法
第68条:executor和task优先于线程
1. java1.5版本之后,java平台中增加了java.util.concurrent,这个包中包含了一个Executor Framework,这是一个很灵活的基于接口的任务执行工具
2. 如果你只想让一个线程来处理来自这个队列的请求,则使用如下
ExecutorService executor = Executors.newSingleThreadExecutor();
3. 如果你想让不止一个线程来处理来着这个队列的请求,则只要调用一个不同的静态工厂,这个工厂创建了一种不同的executor service,称作线程池(thread pool)
4. 如果你想来点特别的,你可以直接使用ThreadPoolExecutor类,这个类允许你控制线程池操作的几乎每个方面
5. 为特殊的应用程序选择executor service是很有技巧的
(1)在轻负载的服务器中,使用Executors.newCachedThreadPool通常是个不错的选择
(2)在高负载的服务器中,最好使用Executors.newFixedThreadPool,它为你提供了一个包含固定线程数据的线程池,或者为了最大限度地控制它,就直接使用ThreadPoolExecutor类
6. 你不仅应该尽量不要编写自己的工作队列,而且还应该尽量不直接使用线程
7. 现在关键的抽象不再是Thread了,它以前即充当工作单元,又是执行机制。现在工作单元和执行机制是分开的
8. 现在关键的抽象是工作单元,称作任务(task)。任务有两种
(1)Runnable
(2)Callable:与Runnable类似,但它会返回值
9. 执行任务的通用机制是executor service。从本质上讲,Executor Framework所做的工作是执行
10. Executor Framework也有一个可以代替java.util.Timer的东西,即ScheduledThreadPoolExecutor
Timer只用一个线程来执行任务,这在面对长期运行的任务时,会影响到定时的准确性。如果Timer唯一的线程抛出未被捕获的异常,Timer就会停止运行。ScheduledThreadPoolExecutor支持多个线程,并且可以优雅地从抛出的未受检异常的任务中恢复
第69条:并发工具优先于wait和notify
1. 既然正确地使用wait和notify比较困难,就应该用更高级的并发工具来代替
2. java.util.conrurrent中更高级的工具分成三类
(1)Executor Framework
(2)并发集合(Concurrent Collection)
并发集合为标准的集合接口提供了高性能的并发实现。为了提高并发性,这些实现在内部自己管理同步
(3)同步器(Synchronizer)
3. 这意味着客户无法原子地对并发集合进行方法调用
4. 除非不得已,否则应该优先使用ConcurrentHashMap,而不是使用Collections.synchronizedMap或者Hashtable
5. 应该优先使用并发集合,而不是使用外部同步的集合
6. 不出所料,大多数ExecutorService实现(包括ThreadPoolExecutor)都使用BlockingQueue
7. 同步器(Synchronizer)是一些使线程能够等待另一个线程的对象,允许它们协调动作
最常用的同步器是CountDownLatch和Semaphore
8. 对于间歇式的定时,始终应该优先使用System.nanoTime,而不是使用System.currentTimeMills。System.nanoTime更加准确也更加精确,它不受系统的实时始终调整所影响
9. 始终应该使用wait循环模式来调用wait方法,永远不要在循环之外调用wait方法。循环会在等待之前和之后测试条件
10. 如果条件已经成立,并且在线程等待之前,notify(或者notifyAll)方法已经被调用,则无法保证该线程将会从等待中苏醒过来
11. 当条件不成立时,有下面一些理由可使一个线程苏醒过来
(1)另一个线程可能已经得到了锁,并且从一个线程调用notify那一刻起,到等待线程苏醒过来这段时间中,得到锁的线程已经改变了受保护的状态
(2)条件并不成立,但是另一个线程可能意外地或恶意地调用了notify
(3)通知线程在唤醒等待线程时使用了notifyAll
(4)在没有通知的情况下,等待线程也可能(但很少)会苏醒过来。这被称为“伪唤醒”(spurious wakeup)
12. notify唤醒的是单个正在等待的线程,而notifyAll唤醒的则是所有正在等待的线程
13. 线程从被唤醒到得到锁这个过程,实际上还有一整段时间的(o)
14. notifyAll代替notify可以避免来自不相关线程的意外或恶意的等待。否则,这样的等待会“吞掉”一个关键的通知,使真正的接收线程无限地等待下去
15. 简而言之,直接使用wait和notify就像用“并发汇编语言”进行编程一样,而java.util.concurrent则提供了更高级的语言
16. 没有理由在新代码中使用wait和notify,即使有,也是极少的
17. 如果你在维护使用wait和notify的代码,务必确保始终是利用标准的模式从while循环内部调用wait
18. 一般情况下,你应该优先使用notifyAll,而不是使用notify,如果使用notify,请一定要小心,以确保程序的活性
第70条:线程安全性的文档化
1. 如果你没有在一个类的文档中描述其行为的并发性情况,那么使用这个类的程序员将不得不做出某些假设
2. 实际上,线程安全性有多个级别。一个类为了可被多个线程安全地使用,必须在文档中清楚地说明它所支持的线程安全级别
(1)不可变的(immutable)
即类实例是不可变的,例如String、Long和BigInteger
(2)无条件的线程安全(unconditionally thread-safe)
这个类的实例是可变的,但是这个类有着足够的内部同步
(3)有条件的线程安全(conditionally thread-safe)
(4)非线程安全(not thread-safe)
(5)线程对立的(thread-hostile)
这个类不能安全地被多个线程并发使用。线程对立的根源通常在于,没有同步地修改静态数据
3. 在文档中描述一个有条件的线程安全类要特别小心,你必须指明哪个调用序列需要外部同步,还要指明为了执行这些序列,必须获得哪一把锁
4. 除非从返回类型来看已经很明显,否则静态工厂必须在文档中说明被返回对象的线程安全性
5. 为了避免这种拒绝服务攻击(denial-of service,客户端超时地保持公有可访问锁),应该使用一个私有锁对象来代替同步的方法(隐含着一个公有可访问锁)
6. 注意,如无特殊情况,我们应该把锁对象封装在它所同步的对象中
7. 将lock域的可变性减到最小
8. 私有锁对象模式特别适用于那些专门为继承而设计的类
9. 如果你编写的是无条件的线程安全类,就应该考虑使用私有锁对象来代替同步的方法。这样可以防止客户端程序和子类的不同步干扰
第71条:慎用延迟初始化
1. 对于延迟初始化,最好的建议是:除非绝对必要,否则就不要这么做
2. 延迟初始化实际上降低了性能
3. 如果利用延迟优化来破坏初始化的循环,就要使用同步访问方法
synchronized FieldType getField(){
if(field == null){
field = computeFieldValue();
}
return field;
}
4. 如果出于性能的考虑而需要对静态域使用延迟初始化,就使用lazy initialization holder class模式
这种模式的魅力在于,getField方法不需要使用synchronized,并且只执行一个域访问,因此延迟初始化实际上并没有增加任何访问成本
5. 如果出于性能的考虑而需要对实例域使用延迟初始化,就使用双重检查模式(double-check idiom)
这种模式避免了在域被初始化之后访问这个域时的锁定开销
6. 在java1.5版本发行之前,double-check idiom的功能很不稳定,因为volatile修饰符的语义不够强,难以支持它。
如今,双重检查模式是延迟初始化一个实例域的方法
7. 对于可以接受重复初始化的实例域,也可以考虑使用单重检查模式(single-check idiom)
8. 简而言之,大多数的域应该正常地进行初始化,而不是延迟初始化
第72条:不要依赖于线程调度器
1. 任何依赖于线程调度器来达到正确性或者性能要求的程序,很有可能都是不可移植的
2. 如果线程没有在做有意义的工作,就不应该运行
3. 使runnable任务保持适当的小,彼此独立。但是,也不能太小,否则分配的开销也会影响到性能
4. 线程不应该一直处于忙等状态(busy-wait)
5. 线程优先级是java平台上最不可移植的特征
6. 应该使用Thread.sleep(1)代替Thread.yield来进行并发测试。千万不要使用Thread.sleep(0)
7. 简而言之,不要让应用程序的正确性依赖于线程调度器
第73条:避免使用线程组
1. 除了线程、锁和监视器之外,线程系统还提供了一个基本的抽象,即线程组(thread group)
2. 总而言之,线程组并没有提供太多有用的功能,而且它们提供的许多功能还都是有缺陷的。我们最好把线程组看作是一个不成功的试验,因此可以忽略掉它们
3. 如果你正在设计的一个类需要处理线程的逻辑组,或许就应该使用线程池executor(见第68条)
第11章 序列化
1. 将一个对象编码成一个字节流,称作将该对象序列化(serializing);相反的处理过程被称作反序列化(deserializing)
2. 序列化技术为远程通信提供了标准的线路级(wire-level)对象表示法,也为JavaBean组件结构提供了标准的持久化数据格式。
第74条:谨慎地实现Serializable接口
1. 实现Serializable接口而付出的最大代价是,一旦一个类被发布,就大大降低了“改变这个类的实现”的灵活性
如果一个类实现了Serializable接口,它的字节流编码(或许说序列化形式)就变成了它的导出API的一部分
2. 如果你接受了默认的序列化形式,这个类中私有的和包级私有的实例域将都变成导出的API的一部分,这不符合“最低限度地访问域”的实践准则,从而它就失去了作为信息隐藏工具的有效性
3. 如果你接受了默认的序列化形式,并且以后又要改变这个类的内部表示法,结果可能导致序列化形式的不兼容
4. 设计良好的序列化形式也许会给类的演变带来限制,但是设计不好的序列化形式则可能会使类根本无法演变
5. 序列化会使类的演变收到限制,这种限制的一个例子与流的唯一标识符有关,通常它也被称为序列化版本UID(serial version UID)
每个可序列化的类都有一个唯一标识号与它相关联
6. 如果你没有声明一个显式的序列版本UIC,兼容性将会遭到破坏,在运行时导致InvalidClassException异常
7. 为了保证类序列化的兼容性,我们应该显式地指定Serial Version UID,并且确定以后不要再改变(o)
8. 实现Serializable的第二个代价是,它增加了出现bug和安全漏洞的可能性
反序列化机制是一个“隐藏的构造器”,具备与其他构造器相同的特点。依靠默认的反序列化机制,很容易使对象的约束关系遭到破坏,以及遭受到非法访问
9. 实现Serializable的第三个代价是,随着类发行新的版本,相关的测试负担也增加了
当一个可序列化的类被修订的时候,很重要的一点是,要检查是否可以“在新版本中序列化一个实例,然后在旧版本中反序列化”,反之亦然。除了二进制兼容性以外,你还必须测试语义兼容性
10. 如果在最初编写一个类的时候,就精心设计了自定义的序列化形式,测试的要求就可以有所降低,但是也不能完全没有测试
11. 根据经验,比如Date和BigInteger这样的值类应该实现Serializable,大多数的集合类也应该如此。代表活动实体的类,比如线程池,一般不应该实现Serializable
12. 为了继承而设计的类应该尽可能少地去实现Serializable接口,用户的接口也应该尽可能少地继承Serializable接口
13. 对于为继承而设计的不可序列化的类,你应该考虑提供一个无参构造器
如果一个专门为了继承而设计的类,不是可序列化的,就不可能编写出可序列化的子类。特别是,如果超类没有提供可访问的无参构造器,子类也不可能做到可序列化
14. 最好在所有的约束关系都已经建立的情况下再创建对象
盲目地为一个类增加无参构造器和单独的初始化方法,而它的约束关系仍由其他的构造器来建立,这样做会使该类的状态空间更加复杂,并且增加出错的可能性
15. 内部类不应该实现Serializable
内部类使用编译器产生的合成域(synthetic field)来保存指向外围实例(enclosing instance)的引用,以及保存来自外围作用域的局部变量的值。“这些域如何对应到类定义中”并没有明确的规定,就好像没有指定匿名类和局部类的名称一样。因此内部类的默认序列化形式是定义不清楚的。然而,静态成员类(static member class)却可以实现Serializable接口
16. 实现Serializable接口就是个很严肃的承诺,我们必须认真对待
17. 在“允许子类实现Serializable接口”或“禁止子类实现Serializable接口”两者之间的一个折中设计方案是,提供一个可访问的无参构造器。这种设计方案允许(但不要求)子类实现Serializable接口
第75条:考虑使用自定义的序列化形式
1. 如果没有先认真考虑默认的序列化形式是否合适,则不要贸然接受
2. 对于一个对象来说,理想的序列化形式应该只包含该对象所表示的逻辑数据,而逻辑数据与物理表示法应该是各自独立的
3. 如果一个对象的物理表示法等同于它的逻辑内容,可能就适合于使用默认的序列化形式
4. 即使你确定了默认的序列化形式是合适的,通常还必须提供一个readObject方法以保证约束关系和安全性
5. @serial标签告诉Javadoc工具,把这些文档信息放在有关序列化形式的特殊文档页中
6. 当一个对象的物理表示法与它的逻辑数据内容有实质性的区别时,使用默认序列化形式会有以下4个缺点
(1)它使这个类的导出API永远地束缚在该类的内部表示法上
(2)它会消耗过多的空间
(3)它会消耗过多的时间
序列化逻辑并不了解对象图的拓扑关系,所以它必须要经过一个昂贵的图遍历(traversal)过程
(4)它会引起栈溢出
7. 对于StringList类,合理的序列化形式可以非常简单,只需先包含链表中字符串的数目,然后紧跟着这些字符串即可。这样就构成了StringList所表示的逻辑数据,与它的物理表示法细节脱离
8. transient修饰符表明这个实例域将从一个类的默认序列化形式中省略掉
9. 如果所有的实例域都是瞬时的,从技术角度而言,不调用defaultWriteObject和defaultReadObject也是允许的,但是不推荐这样做
10. 对于StringList,默认的序列化形式不够灵活,并且执行效果不佳,但是序列化和反序列化StringList实例会产生对原始对象的忠实拷贝
11. 对于散列表而言,接受默认的序列化形式将会构成一个严重的bug
对散列表对象进行序列化和反序列化操作所产生的对象,其约束关系会遭到严重的破坏
12. 无论你是否使用默认的序列化形式,当defaultWriteObject方法被调用的时候,每一个未被标记为transient的实例域都会被序列化
13. 在决定将一个域做成非transient的之前,请一定确信它的值将是该对象逻辑状态的一部分。如果你正在使用一种自定义的序列化形式,大多数实例域,或者所有的实例域则都应该被标记为transient
14. 如果你正在使用默认的序列化形式,并且把一个或者多个域标记为transient,则要记住,当一个实例被反序列化的时候,这些域将被初始化为它们的默认值
15. 不管你选择了哪种序列化形式,都要为自己编写的每个可序列化的类声明一个显式的序列版本UID
如果没有提供显式的序列版本UID,就需要在运行时通过一个高开销的计算过程产生一个序列版本UID
16. 如果你想修改一个没有序列版本UID的现有的类,并希望新的版本能够接受现有的序列化实例,就必须使用那个自动为旧版本生成的值。如通过在旧版本的类上运行serialver工具,可以得到这个数值——被序列化的实例为之存在的那个数值
17. 如果你想为一个类生成一个新的版本,这个类与现有的类不兼容,那么你只需修改序列版本UID声明中的值即可。
18. 只有当默认的序列化形式能够合理地描述对象的逻辑状态时,才能使用默认的序列化形式
19. 你应该分配足够多的时间来设计类的序列化形式,就好像分配足够多的时间来设计它的导出方法一样
20. 正如你无法在将来的版本中去掉导出方法一样,你也不能去掉序列化形式中的域,它们必须被永久地保留下去,以确保序列化兼容性
第76条:保护性地编写readObject方法
1. 不严格地说,readObject是一个“用字节流作为唯一参数”的构造器
当面对一个人工伪造的字节流时,readObject产生的对象会违反它所属的类的约束条件,这时问题就产生了
2. 为了避免攻击者创建无效的对象实例,可以重写readObject方法,该方法首先调用defaultReadObject,然后检查被反序列化后的对象的有效性
3. 攻击者可以通过伪造字节流来创建不可变类的一个可变对象实例
4. 一旦攻击者获得了一个不可变类的可变对象实例,他就可以将这个实例传递给一个“安全性依赖于不可变类的不可变性”的类,从而造成更大的危害。这种推断并不牵强:实际上,有许多类的安全性就依赖于String的不可变性
5. 当一个对象被反序列化的时候,对于客户端不应该拥有的对象引用,如果哪个域包含了这样的对象引用,就必须要做保护性拷贝,这是非常重要的
6. 对于每个可序列化的不可变类,如果它包含了私有的可变组件,那么在它的readObject方法中,必须要对这些组件进行保护性拷贝
7. 注意,保护性拷贝是在有效性检查之前进行的,而且,我们没有使用Date的clone方法来执行保护性拷贝
8. 不要使用writeUnshared和readUnshared方法。它们通常比保护性拷贝更快,但是它们不提供必要的安全性保护
9. 对于非final的可序列化的类,在readObject方法和构造器之间还有其他类似的地方。readObject方法不可以调用可被覆盖的方法,无论是直接调用还是间接调用都不可以。如果违反了这条规则,并且覆盖了该方法,被覆盖的方法将在子类的状态被反序列化之前先运行
10. 总而言之,每当你编写readObject方法的时候,都要这样想:你正在编写一个公有的构造器,无论给它传递什么样的字节流,它都必须产生一个有效的实例。
11. 编写健壮的readObject方法的指导方针
(1)对于对象引用域必须保持为私有的类,要保护性地拷贝这些域中的每个对象
(2)对于任何约束条件,如果检查失败,则抛出一个InvalidObjectException异常。这些检查动作应该跟在所有的保护性拷贝之后
(3)如果整个对象图在被反序列化之后必须进行验证,就应该使用ObjectInputValidation接口
(4)无论是直接方式还是间接方式,都不要调用类中任何可被覆盖的方法
第77条:对于实例控制,枚举类型优先于readResolve
1. 如果一个Singleton类的声明中加上了“implements Serializable”的字样,那么它就不再是一个Singleton了。无论该类使用了默认的序列化形式,还是自定义的序列化形式
2. 任何一个readObject方法,不管是显式的还是默认的,它都会返回一个新建的实例
3. readResolve特性允许你用readObject创建的实例代替另一个实例
4. 事实上,如果依赖readResolve进行实例控制,带有对象引用类型的所有实例域则都必须声明为transient的
5. 这种攻击有点复杂,但是背后的思想却很简单:如果Singleton包含一个非transient的对象引用域,这个域的内容就可以在Singleton的readResolve方法运行之前被反序列化。当对象引用域的内容被反序列化时,它就允许一个精心制作的流“盗用”指向最初被反序列化的Singleton的引用
6. 从历史上看,readResolve方法被用于所有可序列化的实例受控(instance-controlled)的类
自从java1.5发行以来,它就不再是可序列化的类中维持实例控制的最佳方法了
7. readResolve的可访问性(accessibility)很重要
8. 总而言之,你应该尽可能得使用枚举类型来实施实例控制的约束条件。如果做不到,同时又需要一个即可序列化又是实例受控的类,就必须提供一个readResolve方法,并确保该类的所有实例域都为基本类型,或者是transient的
第78条:考虑用序列化代理代替序列化实例
1. 决定实现Serializable接口,会增加出错和出现安全问题的可能性,因为它导致实例要利用语言之外的机制来创建,而不是用普通的构造器
2. 正如保护性拷贝方法一样,序列化代理模式可以阻止伪字节流的攻击以及内部域的盗用攻击
3. 序列化代理模式允许反序列化实例有着与原始序列化实例不同的类
4. 实际上,EnumSet就使用了序列化代理模式
5. 序列化代理模式有两个局限性
(1)它不能与可以被客户端扩展的类兼容
(2)它也不能与对象图中包含循环的某些类兼容
如果你企图从一个对象的序列化代理的readResolve方法内部调用这个对象中的方法,就会得到一个ClassCastException异常,因为你还没有这个对象,只有它的序列化代理
6. 序列化代理模式所增强的功能和安全性是有代价的,它可能比保护性拷贝的开销要来得大
7. 总而言之,每当你发现自己必须在一个不能被客户端扩展的类上编写readObject或者writeObject方法的时候,就应该考虑使用序列化代理模式。