温馨提示:该文章篇幅较长,选择目录中的具体项跳转到目标章节较方便~
引言
Java语言支持四种类型:接口、类、数组、基本类型
接口、类、数组被称为引用类型,类实例和数组是对象,而基本类型的值则不是对象。
方法的签名由它的名称和所有参数类型组成,签名不包括它的返回类型。
创建和销毁对象
第1条:考虑用静态工厂方法代替构造器
例:
public static Boolean valueOf(boolean b){
return b ? Boolean.TRUE : Boolean.FALSE ;
}
注意: 静态工厂方法与设计模式中的工厂方法模式不同。
提供静态工厂方法而不是公有的构造器的优势:
静态工厂方法与构造器不同的第一大优势在于,它们有名称。
例如:构造器BigInteger(int, int, Random)返回的BigInteger可能为素数,如果用名为BigInteger.probablePrime的静态工厂方法来表示,显然更为清楚。
静态工厂方法与构造器不同的第二大优势在于,不必在每次调用它们的时候都创建一个新对象。
例如:不可变类可以使用预先构建好的实例,或者将构建好的实例缓存起来,进行重复利用,从而避免创建不必要的重复对象。
静态工厂方法与构造器不同的第三大优势在于,它们可以返回原返回类型的任何子类型的对象。
这项技术适用于基于接口的框架,因为在这种框架中,接口为静态工厂方法提供了自然返回类型。(可参考本人的另一篇博文java服务提供者框架介绍)
静态工厂方法与构造器不同的第四大优势在于,在创建参数化类型实例的时候,它们使代码变得更加简洁。
例:在调用参数化类的构造器时,即使类型参数很明显,也必须指明。这通常要求得接连两次提供类型参数:
Map<String, List<String>> m = new HashMap<String, List<String>>();
随着类型参数变得越来越长,越来越复杂,这一冗长的说明也很快变得痛苦起来。但是有了静态工厂方法,编译器就可以替你找到类型参数,这被称作type inference(类型推导)。例如,假设HashMap提供了这个静态工厂:
public static HashMap<K, V> newInstance() {
return new HashMap<K, V>();
}
你就可以用下面这句简洁的代码代替上面这段繁琐的声明:
Map<String, List<String>> m = HashMap.newInstance();
遗憾的是,到发行版本1.6为止,标准的集合实现如HashMap并没有工厂方法,但是可以把这些方法放在你自己的工具类中。更重要的是,可以把这样的静态工厂放在你自己的参数化的类中。
静态工厂方法的缺点:
静态工厂方法的主要缺点在于,类如果不含公有的或者受保护的构造器,就不能被子类化。
例如,要想将Collections Framework中的任何方便的实现类子类化,这是不可能的。但是这样也许会因祸得福,因为它鼓励程序员使用复合(composition),而不是继承。
静态工厂方法的第二个缺点在于,它们与其他的静态方法实际上没有任何区别。
在API文档中,它们没有像构造器那样在API文档中明确标识出来,因此,对于提供了静态工厂方法而不是构造器的类来说,要想查明如何实例化一个类,这是非常困难的。
你可以通过在类或者接口注释中关注静态工厂,并遵守标准的命名习惯,来缩小这一劣势。下面是静态工厂方法的一些惯用名称:valueOf, of, getInstance, newInstance, getType, newType
第2条:遇到多个构造器参数时要考虑用构建器
遇到多个构造器参数的解决方案:
一、重叠构造器模式:但是当有许多参数的时候,客户端代码会很难编写,并且仍然较难以阅读。
二、JavaBeans模式:在构造过程中JavaBean可能处于不一致的状态;JavaBeans模式阻止了把类做成不可变的可能。
三、Builder模式:不直接生成想要的对象,而是让客户端利用所有必要的参数调用构造器(或者静态工厂),得到一个builder对象。然后客户端在builder对象上调用类似于setter的方法,来设置每个相关的可选参数。最后,客户端调用无参的build方法来生成不可变的对象。这个builder是它构建的类的静态成员类。
下面就是它的示例:
// Builder Pattern
public class NutritionFacts {
private final int servingSize;
private final int servings;
private final int calories;
private final int fat;
private final int sodium;
private final int carbohydrate;
public static class Builder {
// Required parameters
private final int servingSize;
private final int servings;
// Optional parameters - initialized to default values
private int calories = 0;
private int fat = 0;
private int carbohydrate = 0;
private int sodium = 0;
public Builder(int servingSize, int servings) {
this.servingSize = servingSize;
this.servings = servings;
}
public Builder calories(int val)
{ calories = val; return this; }
public Builder fat(int val)
{ fat = val; return this; }
public Builder carbohydrate(int val)
{ carbohydrate = val; return this; }
public Builder sodium(int val)
{ sodium = val; return this; }
public NutritionFacts build() {
return new NutritionFacts(this);
}
}
private NutritionFacts(Builder builder) {
servingSize = builder.servingSize;
servings = builder.servings;
calories = builder.calories;
fat = builder.fat;
sodium = builder.sodium;
carbohydrate = builder.carbohydrate;
}
public static void main(String[] args) {
NutritionFacts cocaCola = new NutritionFacts.Builder(240, 8).
calories(100).sodium(35).carbohydrate(27).build();
}
}
builder的setter方法返回builder本身,以便可以把调用链接起来。
NutritionFacts cocaCola = new NutritionFacts.Builder(240, 8).
calories(100).sodium(35).carbohydrate(27).build();
Builder模式的确也有它自身的不足。为了创建对象,必须先创建它的构建器。虽然创建构建器的开销在实践中可能不那么明显,但是在某些十分注重性能的情况下,可能就是个问题了。Builder模式还比telescoping constructor(重叠构造器)模式更加冗长,因此它只在有足够参数的时候才使用,比如4个或者更多个参数。但是记住,将来你可能需要添加参数。如果一开始就使用构造器或者静态工厂,等到类需要多个参数时才添加构建器,就会无法控制,那些过时的构造器或者静态工厂显得十分不协调。因此,通常最好一开始就使用构建器。
简而言之,如果类的构造器或者静态工厂中具有多个参数,设计这种类时,Builder模式就是种不错的选择,特别是当大多数参数都是可选的时候。与使用传统的telescoping constructor模式相比,使用Builder模式的客户端代码将更易于阅读和编写,builders也比JavaBeans更加安全。
第3条:用私有构造器或者枚举类型强化Singleton属性
// Enum singleton - the preferred approach
public enum Elvis {
INSTANCE;
public void leaveTheBuilding() {
System.out.println("Whoa baby, I'm outta here!");
}
// This code would normally appear outside the class!
public static void main(String[] args) {
Elvis elvis = Elvis.INSTANCE;
elvis.leaveTheBuilding();
}
}
这种方法在功能上与公有域方法(public static Elvis getInstance()
)相近,但是它更加简洁,无偿地提供了序列化机制,绝对防止多次实例化,即使是在面对复杂的序列化或者反射攻击的时候(享有特权的客户端可以借助于AccessibleObject.setAccessible方法,通过反射机制调用私有构造器。)。虽然这种方法还没有被广泛采用,但是单元素的枚举类型已经成为实现Singleton的最佳方法。
第4条:通过私有构造器强化不可实例化的能力
很多工具类(utility class)不是要被实例化的:实例没有任何意义。
// Noninstantiable utility class
public class UtilityClass {
// Suppress default constructor for noninstantiability
private UtilityClass() {
throw new AssertionError();
}
}
AssertionError不是绝对必要,但是它可以避免不小心在类的内部调用构造器。它保证该类在任何情况下都不会被实例化。
第5条:避免创建不必要的对象
一般来说,最好是重用对象而不是在每次需要的时候就创建一个相同功能的新对象。重用方式既更加快速,也更为流行。如果对象是不可变的(immutable),它就始终可以被重用。
String s = new String("stringette");
//改进
String s = "stringette" ;
除了重用不可变的对象之外,也可以重用那些已知不会被修改的可变对象。
/*
这个类建立了一个模型:其中有一个人,并有一个isBabyBoomer方法,用来检验这个人是否为一个"baby boomer(生育高峰期出生的小孩)",换句话说,就是检验这个人是否出生于1946至1964年间:
*/
// Creates lots of unnecessary duplicate objects
import java.util.*;
public class Person {
private final Date birthDate;
public Person(Date birthDate) {
// Defensive copy - see Item 39
this.birthDate = new Date(birthDate.getTime());
}
// Other fields, methods omitted
// DON'T DO THIS!
public boolean isBabyBoomer() {
// Unnecessary allocation of expensive object
Calendar gmtCal =
Calendar.getInstance(TimeZone.getTimeZone("GMT"));
gmtCal.set(1946, Calendar.JANUARY, 1, 0, 0, 0);
Date boomStart = gmtCal.getTime();
gmtCal.set(1965, Calendar.JANUARY, 1, 0, 0, 0);
Date boomEnd = gmtCal.getTime();
return birthDate.compareTo(boomStart) >= 0 &&
birthDate.compareTo(boomEnd) < 0;
}
}
isBabyBoomer每次被调用的时候,都会新建一个Calendar、一个TimeZone和两个Date实例,这是没有必要的。下面的版本使用了一个静态的初始化器(initializer),避免了这种效率低下的情况:
// Doesn't creates unnecessary duplicate objects
import java.util.*;
class Person {
private final Date birthDate;
public Person(Date birthDate) {
// Defensive copy - see Item 39
this.birthDate = new Date(birthDate.getTime());
}
// Other fields, methods
/**
* The starting and ending dates of the baby boom.
*/
private static final Date BOOM_START;
private static final Date BOOM_END;
static {
Calendar gmtCal =
Calendar.getInstance(TimeZone.getTimeZone("GMT"));
gmtCal.set(1946, Calendar.JANUARY, 1, 0, 0, 0);
BOOM_START = gmtCal.getTime();
gmtCal.set(1965, Calendar.JANUARY, 1, 0, 0, 0);
BOOM_END = gmtCal.getTime();
}
public boolean isBabyBoomer() {
return birthDate.compareTo(BOOM_START) >= 0 &&
birthDate.compareTo(BOOM_END) < 0;
}
}
要优先使用基本类型而不是装箱基本类型,要当心无意识的自动装箱。
public class Sum {
// Hideously slow program! Can you spot the object creation?
public static void main(String[] args) {
Long sum = 0L;
for (long i = 0; i < Integer.MAX_VALUE; i++) {
sum += i;
}
System.out.println(sum);
}
}
这段程序算出的答案是正确的,但是比实际情况要更慢一些,只因为打错了一个字符。变量sum被声明成Long而不是long,意味着程序构造了大约2^31个多余的Long实例(大约每次往Long sum中增加long时构造一个实例)。
不要错误地认为本条目所介绍的内容暗示着”创建对象的代价非常昂贵,我们应该要尽可能地避免创建对象”。相反,由于小对象的构造器只做很少量的显式工作,所以,小对象的创建和回收动作是非常廉价的,特别是在现代的JVM实现上更是如此。通过创建附加的对象,提升程序的清晰性、简洁性和功能性,这通常是件好事。
反之,通过维护自己的对象池(object pool)来避免创建对象并不是一种好的做法,除非池中的对象是非常重量级的。真正正确使用对象池的典型对象示例就是数据库连接池。建立数据库连接的代价是非常昂贵的,因此重用这些对象非常有意义。而且,数据库的许可可能限制你只能使用一定数量的连接。但是,一般而言,维护自己的对象池必定会把代码弄得很乱,同时增加内存占用(footprint),并且还会损害性能。现代的JVM实现具有高度优化的垃圾回收器,其性能很容易就会超过轻量级对象池的性能。
与本条目对应的是第39条中有关”保护性拷贝(defensive copying)”的内容。本条目提及”当你应该重用现有对象的时候,请不要创建新的对象”,而第39条则说”当你应该创建新对象的时候,请不要重用现有的对象”。注意,在提倡使用保护性拷贝的时候,因重用对象而付出的代价要远远大于因创建重复对象而付出的代价。必要时如果没能实施保护性拷贝,将会导致潜在的错误和安全漏洞;而不必要地创建对象则只会影响程序的风格和性能。
第6条:消除过期的对象引用
如果一个栈先是增长,然后再收缩,那么,从栈中弹出来的对象将不会被当作垃圾回收,即使使用栈的程序不再引用这些对象,它们也不会被回收。这是因为,栈内部维护着对这些对象的过期引用(obsolete reference)。所谓的过期引用,是指永远也不会再被解除的引用。
这类问题的修复方法很简单:一旦对象引用已经过期,只需清空这些引用即可。对于Stack类而言,只要一个单元被弹出栈,指向它的引用就过期了。
public Object pop() {
if (size == 0)
throw new EmptyStackException();
Object result = elements[--size];
elements[size] = null; // Eliminate obsolete reference
return result;
}
清空对象引用应该是一种例外,而不是一种规范行为。消除过期引用最好的方法是让包含该引用的变量结束其生命周期。如果你是在最紧凑的作用域范围内定义每一个变量,这种情形就会自然而然地发生。
一般而言,只要类是自己管理内存,程序员就应该警惕内存泄漏问题。一旦元素被释放掉,则该元素中包含的任何对象引用都应该被清空。
内存泄漏的另一个常见来源是缓存。一旦你把对象引用放到缓存中,它就很容易被遗忘掉,从而使得它不再有用之后很长一段时间内仍然留在缓存中。(当所要缓存的项的生命周期是由该键的外部引用而不是由值决定时,可用WeakHashMap解决该问题:在 WeakHashMap 中,当某个键不再正常使用时,将自动移除其条目;LinkedHashMap类利用它的removeEldestEntry方法可以很容易地清除掉没用的缓存项)
内存泄漏的第三个常见来源是监听器和其他回调。如果你在实现的是客户端注册回调却没有显式地取消注册的API,除非你采取某些动作,否则它们就会积聚。确保回调立即被当作垃圾回收的最佳方法是只保存它们的弱引用(weak reference),例如,只将它们保存成WeakHashMap中的键。
第7条:避免使用终结方法
终结方法(finalizer)通常是不可预测的,也是很危险的,一般情况下是不必要的。
Java语言规范不仅不保证终结函数会被及时地执行,而且根本就不保证它们会被执行。
还有一点:使用终结函数有一个非常严重的(Severe)性能损失。在我的机器上,创建和销毁一个简单对象的时间大约为5.6ns。增加一个终结函数使时间增加到了2,400ns。换句话说,用终结函数创建和销毁对象慢了大约430倍。
如果类的对象中封装的资源(例如文件或者线程)确实需要终止,应该怎么做才能不用编写终结方法呢?只需提供一个显式的终止方法,并要求该类的客户端在每个实例不再有用的时候调用这个方法。显式的终止方法通常与try-finally结构结合起来使用,以确保及时终止。在finally子句内部调用显式的终止方法,可以保证即使在使用对象的时候有异常抛出,该终止方法也会被执行:
// try-finally block guarantees execution of termination method
Foo foo = new Foo(...);
try {
// Do what must be done with foo
...
} finally {
foo.terminate(); // Explicit termination method
}
除非是作为安全网,或者是为了终止非关键的本地资源,否则请不要使用终结方法。在这些很少见的情况下,既然使用了终结方法,就要记住调用super.finalize (因为”终结方法链(finalizer chaining)”并不会被自动执行)。如果用终结方法作为安全网,要记得记录终结方法的非法用法。最后,如果需要把终结方法与公有的非final类关联起来,请考虑使用终结方法守卫者(把终结方法放在一个匿名的类中,该匿名类的唯一用途就是终结它的外围实例),以确保即使子类的终结方法未能调用super.finalize,该终结方法也会被执行。
对于所有方法都通用的方法
第8条:覆盖equals时请遵守通用约定
需要覆盖equals:如果类具有自己特有的“逻辑相等”概念,而且超类还没有覆盖equals以实现期望的行为,这时我们就需要覆盖equals方法。
不需要覆盖equals:用实例受控确保“每个值至多只存在一个对象”的类。枚举类型就属于这种类。对于这样的类而言,逻辑相同与对象等同是一回事。
equals方法实现等价关系通用约定(equivalence relation):
- 自反性(reflexive):对于任何非null的引用值x,x.equals(x)必须返回true。
- 对称性(symmetric):对于任何非null的引用值x和y,当且仅当y.equals(x)返回true时,x.equals(y)必须返回true。
- 传递性(transitive):对于任何非null的引用值x、y和z,如果x.equals(y)返回true,并且y.equals(z)也返回true,那么x.equals(z)也必须返回true。
- 一致性(consistent):对于任何非null的引用值x和y,只要equals的比较操作在对象中所用的信息没有被修改,多次调用x.equals(y)就会一致地返回true,或者一致地返回false。
- 非空性(Non-nullity):对于任何非null的引用值x,x.equals(null)必须返回false。
实现高质量equals方法的诀窍:
- 使用==操作符检查“参数是否为这个对象的引用”。
- 使用instanceof操作符检查“参数是否为正确的类型”。
- 把参数转换成正确的类型。
- 对于该类的每个“关键(significant)”域,检查参数中的域是否与该对象中对应的域相匹配。
- 当你编写完成了equals方法之后,应该会问自己三个问题:它是否是对称的、传递的、一致的。
最后的一些告诫:
- 覆盖equals时总要覆盖hashcode
- 不要企图让equals方法过于智能
- 不要将equals声明中的Object对象替换为其他的类型
尽量不要省略@Override。
第9条:覆盖equals时总要覆盖hashCode
Object.hashCode的通用约定
- 在引用程序的执行期间,只要对象的equals方法的比较操作所用到的信息没有被修改,那么对这同一个对象调用多次,hashCode方法都必须始终如一地返回同一个整数。在一个应用程序的多次执行过程中,每次执行所返回的整数可以不一致。
- 如果两个对象根据equals方法比较是相等的,那么调用这两个对象中任意一个对象的hashCode方法都必须产生同样的整数结果。
- 如果两个对象根据equals方法比较是不相等的,那么调用这两个对象中任意一个对象的hashCode方法,则不一定要产生不同的整数结果。但是程序员应该知道,给不相等的对象产生截然不同的整数结果,有可能提高散列表(hash table)的性能。
写完了hashCode方法之后,问问自己“相等的实例是否都具有相等的散列码”。
不要试图从散列码计算中排除掉一个对象的关键部分来提高性能。
第10条:始终要覆盖toString
Object提供的toString方法的实现 : 类的名称+@+散列码的无符号十六进制表示
当对象被传递给println,printf,字符串连接符(+)以及assert或者被调试器打印出来时,toString方法会被自动调用。
在实现toString的时候,必须要做出一个很重要的决定:是否在文档中指定返回值的格式。
无论你是否决定指定格式,都应该在文档中明确地表明你的意图;无论是否指定格式,都为 toString返回值中包含的所有信息,提供一种编码式的访问途径。如果没有提供,即使你已经指明了字符串的格式是可以变化的,这个字符串格式也成了事实上的API。
第11条:谨慎地覆盖clone
Object中clone方法的定义是:
protected native Object clone() throws CloneNotSupportedException;
首先它是保护的,其次它是native(本地)的,也就是说它是通过其他语言编写的代码,是看不到源码的,最后它可能抛出CloneNotSupportedException,在类不支持克隆时。
Cloneable接口:
public interface Cloneable {
}
它其实是空空的,具体方法一个也没有。那么它到底做了什么呢?它决定了Object中受保护的clone方法实现的行为:如果一个类实现了Cloneable,则Object的clone方法返回该对象的逐域拷贝,否则的话抛出一个CloneNotSupportedException异常。这是接口的一种极端非典型的用法,不值得效仿。
那么既然实现了Cloneable接口后,就可以调用Object中的clone方法了(Cloneable接口改变了超类中一个受保护的方法的行为),那我们的目的不就达到了吗?干吗还要改写clone呢?
Object的clone方法,只能逐域拷贝那些原语类型,对于类仅仅是地址赋值,换句话说,它只是逐域在做 = 操作。这样并不是完全的克隆,所以我们需要改写clone方法。
Object中关于克隆的约定:
1) x.clone() != x ,将会为 true
2) x.clone().getClass() == x.getClass() ,将会为 true
3) x.clone().equals(x) , 将会为 true
“将会为true”,但是这也不是一个绝对要求。拷贝往往会导致创建一个新实例,但同时也会要求拷贝内部的数据结构。这个过程中没有调用构造函数。
“没有调用构造函数”和“x.clone().getClass() == x.getClass() ” 的综合,导致结果就是:如果你改写一个非final类的clone方法,则应该返回一个通过调用super.clone而得到的对象。这其实也相当于给我们提供了一个改写clone方法的“处方”:不要使用构造函数来创建类,而是使用超类的clone方法。
如果对象中的域引用了可变的对象,使用上述这种简单的clone实现可能会导致灾难性的后果。
例:
// A cloneable version of Stack
import java.util.Arrays;
public class Stack implements Cloneable {
private Object[] elements;
private int size = 0;
private static final int DEFAULT_INITIAL_CAPACITY = 16;
public Stack() {
this.elements = new Object[DEFAULT_INITIAL_CAPACITY];
}
public void push(Object e) {
ensureCapacity();
elements[size++] = e;
}
public Object pop() {
if (size == 0)
throw new EmptyStackException();
Object result = elements[--size];
elements[size] = null; // Eliminate obsolete reference
return result;
}
public boolean isEmpty() {
return size == 0;
}
@Override public Stack clone() {
try {
Stack result = (Stack) super.clone();
return result;
} catch (CloneNotSupportedException e) {
throw new AssertionError();
}
}
// Ensure space for at least one more element.
private void ensureCapacity() {
if (elements.length == size)
elements = Arrays.copyOf(elements, 2 * size + 1);
}
// To see that clone works, call with several command line arguments
public static void main(String[] args) {
Stack stack = new Stack();
for (String arg : args)
stack.push(arg);
Stack copy = stack.clone();
while (!stack.isEmpty())
System.out.print(stack.pop() + " ");
System.out.println();
while (!copy.isEmpty())
System.out.print(copy.pop() + " ");
}
}
这样得到的Stack实例,size域中具有正确的值,但是elements域将引用与原始Stack实例相同的数组。修改原始的实例会破坏被克隆对象中的约束条件,反正亦然。很快你就会发现,这个程序将产生无意义的结果,或者抛出NullPointerException。
为了使Stack类中的clone方法正常地工作,它必须要拷贝栈的内部信息。
@Override public Stack clone() {
try {
Stack result = (Stack) super.clone();
// 在elements数组中递归地调用clone
result.elements = elements.clone();
return result;
} catch (CloneNotSupportedException e) {
throw new AssertionError();
}
}
实际上,clone方法就是另一个构造器,你必须确保它不会伤害到原始的对象,并确保正确地创建被克隆对象中的约束条件。
如果elements域是final的,上述方案就不能正常工作,因为clone方法是被禁止给elements域赋新值的。clone架构与引用可变对象的final域的正常用法是不相兼容的,除非在原始对象与克隆对象之间可以安全地共享此可变对象。为了使类成为可克隆的,可能有必要从某些域中去掉final修饰符。
最好提供某些其他途径来代替对象拷贝,或者干脆不提供这样的功能。一个好的代替方法是提供一个拷贝构造器:
public Yum( Yum yum);
或拷贝工厂:
public static Yum newInstance( Yum yum);
对于一个专门为了继承而设计的类,如果你未能提供行为良好的受保护的clone方法,它的子类就不可能实现Cloneable接口。
第12条:考虑实现Comparable接口
compareTo方法并不是Object类定义的方法。它是Comparable接口中的唯一的一个方法。它和Object类的equals方法类似,只是它允许指定自定义的比较,而不是简单的相等性比较;并且它是通用的。
类实现了Comparable接口,就表明它的实例具有内在的排序关系。例:为实现了Comparable接口的对象数组进行排序:Arrays.sort(a);
compareTo方法的通用约定与equals方法的类似:
(sgn表示数学中的signum函数,它根据表达式的值为负值、零和正值分别返回-1、0、1)
- 实现者必须确保所有的x和y都满足sgn( x.compareTo ( y )) == -sgn( y.compareTo( x ))。(这也暗示着,当且仅当 y.compareTo( x )抛出异常时,x.compareTo ( y )才必须抛出异常。)
- 实现者还必须确保这个比较关系是可传递的:( x.compareTo ( y )>0 && y.compareTo( z )>0 ) 暗示着 x.compareTo ( z )>0
- 最后,实现者必须确保 x.compareTo ( y )==0 暗示着所有的z都满足( sgn(x.compareTo(z)) == sgn(y.compareTo(z)))
- 强烈建议(x.compareTo(y)==0) == x.equals(y) ,但这并非绝对必要。一般而言,任何实现了Comarable接口的类,若违反了这个条件,应该明确予以说明。推荐这样的说法:“注意:该类具有内在的排序功能,但是与equals不一致。”
与equals不同的是,在跨越不同类的时候,compareTo可以不做比较:如果两个被比较的对象引用不同类的对象,compareTo可以抛出ClassCastException异常。
类和接口
第13条:使类和成员的可访问性最小化
对于公有类的成员,当访问级别从包级私有变成保护级别时,会大大增强可访问性。受保护的成员是类的导出的API的一部分,必须永远得到支持。导出的类的受保护成员也代表了该类对于某个实现细节的公开承诺。受保护的成员应该尽量少用。
除了公有静态final域的特殊情形之外,公有类都不应该包含公有域。并且要确保公有静态final域所引用的对象都是不可变的。
第14条:在公有类中使用访问方法而非公有域
公有类永远都不应该暴露可变的域。虽然还是有问题,但是让公有类暴露不可变的域其危害比较小。但是,有时候会需要用包级私有的或者私有的嵌套类来暴露域,无论这个类是可变的还是不可变的。
第15条:使可变性最小化
不可变的类比可变的类更容易设计、实现和使用。它们不容易出错,且更安全。
为了使类成为不可变,要遵循下面五条规则:
- 为了使类成为不可变,要遵循下面五条规则:
- 保证类不会被扩展。
- 使所有的域都是final的。
- 使所有的域都成为私有的。
- 确保对于任何可变组件的互斥访问。
不可变对象本质上是线程安全的,它们不要求同步。
不可变对象可以自由地共享。
“不可变对象可以被自由的共享”导致的结果是,永远也不需要进行保护性拷贝。
不仅可以共享不可变对象,甚至也可以共享它们的内部信息。
不可变对象为其他对象提供了大量的构件。
不可变对象真正唯一的缺点是,对于每一个值都需要一个单独的对象。
让不可变对象变成final的另外一个方法就是,让类的所有构造器都变成私有的或者包级私有的,并添加公有的静态工厂(static factory)来替代公有构造器。
除非有很好的理由要让类成为可变的类,否则就应该是不可变的。
如果类不能被做成是不可变的,仍然应该尽可能地限制它的可变性。
除非有令人信服的理由要使域变成是非final的,否则要使每个域都是final的。
第16条:复合优先于继承
继承违背了封装原则,只有当子类真正是超类的子类型时,才适合用继承。换句话说,对于两个类A和B,只有当两者之间确实存在” is-a “关系的时候,类B才应该扩展类A。
在java平台类库中,有许多明显违反这条原则的地方。例如:栈( Stack )并不是向量( Vector ),所以Stack不应该扩展Vector 。同样的,属性列表也不是散列表,所以Properties不应该扩展HashTable。在这两种情况下,复合模式才是恰当的。
继承机制会把超类API中的所有缺陷传播到子类中,而复合则允许设计新的API来隐藏这些缺陷。
如果子类和超类处在不同的包中,并且超类并不是为了继承而设计的,那么继承将会导致脆弱性,可以用复合和转发机制来代替继承,尤其是当存在适当的接口可以实现包装类的时候。包装类比子类更加健壮,而且功能也更加强大。
第17条:要么为继承而设计,并提供文档说明,要么就禁止继承
好的API文档应该描述一个给定的方法做了什么工作,而不是描述它是如何做到的。
对于为了继承而设计的类,唯一的测试方法就是编写子类。
为了允许继承,类还必须遵守一些其他约束。构造器决不能调用可被覆盖的方法,无论是直接调用还是间接调用。
如果你决定在一个为了继承而设计的类中实现Cloneable或者Serializable接口,就应该意识到,因为clone和readObject方法在行为上非常类似于构造器,所以类似的限制也是适用的:无论是clone还是readObject,都不可以调用可覆盖的方法,不管是以直接的还是间接的方式。
如果你决定在一个为了继承而设计的类中实现Serializable,并且该类有一个readResolve或者writeReplace方法,就必须使readResolve或者writeReplace成为受保护的方法,而不是私有的方法。如果这些方法是私有的,子类将会不声不响地忽略掉这两个方法。这正是“为了允许继承,而把实现细节变成类的API的一部分“的另一种情形。
对于那些并非为了安全地进行子类化而设计和编写文档的类,要禁止子类化。有两种办法可以禁止子类化:把这个类声明为final的;把所有的构造器都变成私有的,或者包级私有的,并增加一些公有的静态工厂来替代构造器。
用”直接调用可覆盖方法的私有辅助方法“来代替”可覆盖方法的每个自用调用“。
第18条:接口优于抽象类
接口和抽象类的区别:
- 抽象类允许包含某些方法的实现,接口则不允许。
- 为了实现由抽象类定义的类型,类必须成为抽象类的一个子类;任何一个类只要定义了所有必要的方法,并且遵守通用约定,就被允许实现一个接口,而不管这个类处于类层次的哪个位置。
接口的优势:
- 现有的类可以很容易被更新,以实现新的接口。
- 接口是定义mixin(混合类型)的理想选择。
- 接口允许我们构造非层次结构的类型框架。
通过对你导出的每个重要接口都提供一个抽象的骨架实现(skeletal implementation)类,把接口和抽象类的优点结合起来。接口的作用仍然是定义类型,但是骨架实现类接管了所有与接口实现相关的工作。按照惯例,骨架实现类被称为AbstractInterface。如:AbstractSet。
简单实现:像骨架实现,它实现了接口,并且是为了继承而设计的,但是区别在于它不是抽象的,它是最简单的可能的有效实现,你可以原封不动地使用,也可以看情况将它子类化。例如: AbstractMap.SimpleEntry。
设计公有的接口要非常谨慎。接口一旦被公开发行,并且已被广泛实现,再想改变这个接口几乎是不可能的。
简而言之,接口通常是定义允许多个实现的类型的最佳途径。这条规则有个例外,即当演变的容易性比灵活性和功能更为重要的时候。在这种情况下,应该使用抽象类来定义类型,但前提是必须理解并且可以接受这些局限性。如果你导出了一个重要的接口,就应该坚决考虑同时提供骨架实现类。最后,应该尽可能谨慎地设计所有的公有接口,并通过编写多个实现来对它们进行全面的测试。
第19条:接口只用于定义类型
当类实现接口时,接口就充当可以引用这个类的实例的类型。
常量接口:不包含任何方法,只包含静态的final域,每个域都导出一个常量。使用这些常量的类实现这个接口,以避免用类名来修饰常量名。
例:
// Constant interface antipattern - don not use!
public interface PhysicalConstants {
// Avogadro's number (1/mol)
static final double AVOGADROS_NUMBER = 6.02214199e23;
// Boltzmann constant (J/K)
static final double BOLTZMANN_CONSTANT = 1.3806503e-23;
// Mass of the electron (kg)
static final double ELECTRON_MASS = 9.10938188e-31;
}
常量接口模式是对接口的不良使用。
第20条:类层次优先于标签类
标签类过于冗长、容易出错,并且效率低下。
例:标签类
// Tagged class - vastly inferior to a class hierarchy!
class Figure {
enum Shape { RECTANGLE, CIRCLE };
// Tag field - the shape of this figure
final Shape shape;
// These fields are used only if shape is RECTANGLE
double length;
double width;
// This field is used only if shape is CIRCLE
double radius;
// Constructor for circle
Figure(double radius) {
shape = Shape.CIRCLE;
this.radius = radius;
}
// Constructor for rectangle
Figure(double length, double width) {
shape = Shape.RECTANGLE;
this.length = length;
this.width = width;
}
double area() {
switch(shape) {
case RECTANGLE:
return length * width;
case CIRCLE:
return Math.PI * (radius * radius);
default:
throw new AssertionError();
}
}
}
第21条:用函数对象表示策略
为了在java中实现策略模式,要声明一个接口来表示该策略,并且为每个具体策略声明一个实现了该接口的类。当一个具体策略只被使用一次时,通常使用匿名类来声明和实例化这个具体策略类。当一个具体策略是设计用来重复使用的时候,它的类通常就要被实现为私有的静态成员类,并通过公有的静态final域被导出,其类型为该策略接口。
//策略接口
public interface Comparator<T> {
public int compare(T t1, T t2);
}
//匿名类实现
Arrays.sort(stringArray,new Comparator<String>() {
@Override
public int compare(String o1, String o2) {
return 0;
}
});
//私有静态成员类实现
public class Host {
private static class StrLenCmp implements Comparator<String>{
@Override
public int compare(String o1, String o2) {
return o1.length() - o2.length();
}
}
public static final Comparator<String> STRING_LENGTH_COMPARATOR = new StrLenCmp();
}
第22条:优先考虑静态成员类
嵌套类有四种:静态成员类(static member class)、非静态成员类(nonstatic member class)、匿名类(anonymous class)和局部类(local class)。除了第一种之外,其他三种都称为内部类(inner class)。
如果嵌套类的实例可以在它的外围类的实例之外独立存在,这个嵌套类就必须是静态成员类;在没有外围实例的情况下,要想创建非静态成员类的实例是不可能的。
匿名类可以出现在代码中任何允许存在表达式的地方。当且仅当匿名类出现在非静态的环境中时,它才有外围实例。但是即使它们出现在静态的环境中,也不可能拥有任何静态成员。
匿名类常见用法:动态地创建函数对象(见第21条);创建过程对象,如Runnable,Thread;在静态工厂方法的内部(见第18条intArrayAsList)。
局部类与其他三种嵌套类中的每一种都有一些共同的属性。与成员类一样,局部类有名字,可以被重复地使用。与匿名类一样,只有当局部类是在非静态环境中定义的时候,才有外围实例,它们也不能包含静态成员。与匿名类一样,它们必须非常简短,以便不会影响到可读性。
如果一个嵌套类需要在单个方法之外仍然是可见的,或者它太长了,不适合方法内部,就应该使用成员类。如果成员类的每个实例都需要一个指向其外围实例的引用,就要把成员类做成非静态的;否则就做成静态的。假设这个嵌套类属于一个方法的内部,如果你需要在一个地方创建实例,并且已经有了一个预置的类型可以说明这个类的特征,就把它做成匿名类;否则,就做成局部类。
泛型
第23条:请不要在新代码中使用原生态类型
原生态类型:不带任何实际类型参数的泛型名称。如:与 List<E>
对应的原生态类型是List 。
如果使用原生态类型,就失掉了泛型在安全性和表述性方面的优势。
泛型有子类型化的规则,List<String>
是原生态类型List的一个子类型,而不是参数化类型List<Object>
的子类型。
不要在新代码中使用原生态类型,这条规则有两个例外,两者都源于“泛型信息可以在运行时被擦除”:
- 在类文字中必须使用原生态类型。如:List.class,String[].class,int.class合法;
List<String>.class
,List<?>.class
不合法。 - 在参数化类型而非无限制通配符类型上使用instanceof操作符是非法的。用无限制通配符代替原生态类型,对instanceof操作符的行为不会产生任何影响。在这种情况下,
<?>
就显得多余了。下面是利用泛型来使用instanceof操作符的首选方法:
if(o instanceof Set){
Set<?> m = Set<?> o;
}
注意,一旦确定这个o是个 Set,就必须将它转换成通配符类型Set<?>
,而不是转换成原生态类型Set。这是个受检的转换,因此不会导致编译期警告。
第24条:消除非受检警告
要尽可能地消除每一个非受检警告。
如果无法消除警告,同时可以证明引起警告的代码是类型安全的,(只有在这种情况下才)可以用一个@SuppressWarnings(“unchecked”)注解来禁止这条警告。如果忽略而不是禁止明知道是安全的非受检警告,那么当新出现一条真正有问题的警告时,你也不会注意到。新出现的警告就会淹没在所有的错误警告当中。
SuppressWarnings注解可以用在任何粒度的级别中,从单独的局部变量声明到整个类都可以。应该始终在尽可能小的范围中使用SuppressWarnings。
每当使用@SuppressWarnings(“unchecked”)注解时,都要添加一条注释,说明为什么这么做是安全的。
第25条:列表优先于数组
数组是协变的,如果Sub为Super的子类型,那么数组类型Sub[]就是Super[]的子类型;泛型是不可变的,对于任意两个不同的类型Type1和Type2,
List<Type1>
既不是List<Type2>
的子类型,也不是其超类型。数组是具体化的,在运行时才知道并检查它们的元素类型约束;泛型则是通过擦除实现的,只在编译时强化它们的类型信息,并在运行时丢弃(或者擦除)它们的元素类型信息。擦除就是使泛型可以与没有使用泛型的代码随意进行交互。
由于上述这些根本的区别,数组和泛型不能很好地混合使用。例如,创建泛型、参数化类型或者类型参数的数组是非法的。非法的数组创建表达式:new List<E>[]
,new List<String>[]
,new E[]
。
从技术的角度来说,像E
,List<E>
,和List<Sting>
这样的类型应称作不可具体化的类型。不可具体化的类型是指其运行时表示法包含的信息比它的编译时表示法包含的信息更少的类型。唯一可具体化的参数化类型是无限制的通配符类型,如List<?>
和Map<?,?>
。虽然不常用,但是创建无限制通配符类型的数组是合法的。
第26条:优先考虑泛型
使用泛型比使用需要在客户端代码中进行转化的类型来得更安全,也更容易。在设计新类的时候,要确保它们不需要这种转换就可以使用。这通常意味着要把类做成是泛型的。
第27条:优先考虑泛型方法
泛型方法像泛型一样,使用起来比要求客户端转换输入参数并返回值的方法来得更安全,也更容易。
第28条:利用有限制通配符来提升API的灵活性
public class Stack<E>{
public Stack();
public void push(E e);
public E pop();
public boolean isEmpty();
public void pushAll(Iterable<E> src){
for(E e:src){
push(e);
}
}
}
则以下用法出错:
Stack<Number> numberStack = new Stack<Number>();
Iterable<Integer> integers=...;
numberStack.pushAll(integers);
因为参数化类型是不可变的,Iterable<Integer>
不是Iterable<Number>
的子类。
修改:
public void pushAll(Iterable<? extends E> src){
for(E e:src){
push(e);
}
}
E的子类型的集合(包括E):? extends E;
E的超类型的集合(包括E):? super E。
如果参数化类型表示一个T生产者, 就使用<? extends T>
;
如果参数化类型表示一个T消费者, 就使用<? super T>
。
所有的comparable和comparator都是消费者,使用时Comparable<? super T>
优先于Comparable<T>
,Comparator<? super T>
优先于Comparator<T>
。
类型参数和通配符之间具有双重性,许多方法都可以利用其中一个或者另一个进行声明。
public static <E> void swap(List<E> list,int i,int j);
public static void swap(List<?> list,int i,int j);
在公共API中,第二种更好一些,因为它更简单。
一般来说,如果类型参数只在方法声明中出现一次,就可以用通配符取代它。如果是无限制的类型参数,就用无限制的通配符取代它;如果是有限制的类型参数,就用有限制的通配符取代它。
但是将swap方法用于第二种声明会有问题:
public static void swap(List<?> list,int i,int j){
list.set(i,list.set(j,list.get(i)));
}
list的类型为List<?>
,你不能把null之外的任何值放到List<?>
中。
改进:编写私有的辅助方法来捕捉通配符类型
public static void swap(List<?> list,int i,int j){
swapHelper(list,i,j);
}
private static <E> void swapHelper(List<E> list,int i,int j){
list.set(i,list.set(j,list.get(i)));
}
swap的这种实现允许我们导出比较好的基于通配符的声明,同时在内部利用更加复杂的泛型方法。swap方法的客户端不一定要面对更加复杂的swapHelper声明,但是它们的确从中受益。
第29条:优先考虑类型安全的异构容器
// Typesafe heterogeneous container
import java.util.*;
public class Favorites {
// Typesafe heterogeneous container pattern - implementation
private Map<Class<?>, Object> favorites =
new HashMap<Class<?>, Object>();
public <T> void putFavorite(Class<T> type, T instance) {
if (type == null)
throw new NullPointerException("Type is null");
favorites.put(type, instance);
}
public <T> T getFavorite(Class<T> type) {
return type.cast(favorites.get(type));
}
// Typesafe heterogeneous container pattern - client
public static void main(String[] args) {
Favorites f = new Favorites();
f.putFavorite(String.class, "Java");
f.putFavorite(Integer.class, 0xcafebabe);
f.putFavorite(Class.class, Favorites.class);
String favoriteString = f.getFavorite(String.class);
int favoriteInteger = f.getFavorite(Integer.class);
Class<?> favoriteClass = f.getFavorite(Class.class);
System.out.printf("%s %x %s%n", favoriteString,
favoriteInteger, favoriteClass.getName());
}
}
存在的限制:
(1) 恶意的客户端可以轻松地破坏Favorites实例的类型安全,只要它以原生态形式使用Class对象。 改进:
public <T> void putFavorite(Class<T> type, T instance) {
if (type == null)
throw new NullPointerException("Type is null");
favorites.put( type, type.cast(instance) );
}
(2) 不能用在不可具体化的类型中。不完全令人满意的解决方法:有限制的类型令牌(bounded type token):
// Use of asSubclass to safely cast to a bounded type token
import java.lang.annotation.*;
import java.lang.reflect.*;
public class PrintAnnotation {
// Use of asSubclass to safely cast to a bounded type token
static Annotation getAnnotation(AnnotatedElement element,
String annotationTypeName) {
Class<?> annotationType = null; // Unbounded type token
try {
annotationType = Class.forName(annotationTypeName);
} catch (Exception ex) {
throw new IllegalArgumentException(ex);
}
return element.getAnnotation(
annotationType.asSubclass(Annotation.class));
}
// Test program to print named annotation of named class
public static void main(String[] args) throws Exception {
if (args.length != 2) {
System.out.println(
"Usage: java PrintAnnotation <class> <annotation>");
System.exit(1);
}
String className = args[0];
String annotationTypeName = args[1];
Class<?> klass = Class.forName(className);
System.out.println(getAnnotation(klass, annotationTypeName));
}
}
枚举和注解
第30条:用enum代替int常量
java的枚举本质上是int值。
// Enum type with data and behavior
//每个枚举常量后面括号中的值就是传递给构造器的参数
public enum Planet {
MERCURY(3.302e+23, 2.439e6),
VENUS (4.869e+24, 6.052e6),
EARTH (5.975e+24, 6.378e6),
MARS (6.419e+23, 3.393e6),
JUPITER(1.899e+27, 7.149e7),
SATURN (5.685e+26, 6.027e7),
URANUS (8.683e+25, 2.556e7),
NEPTUNE(1.024e+26, 2.477e7);
private final double mass; // In kilograms
private final double radius; // In meters
private final double surfaceGravity; // In m / s^2
// Universal gravitational constant in m^3 / kg s^2
private static final double G = 6.67300E-11;
// Constructor
Planet(double mass, double radius) {
this.mass = mass;
this.radius = radius;
surfaceGravity = G * mass / (radius * radius);
}
public double mass() { return mass; }
public double radius() { return radius; }
public double surfaceGravity() { return surfaceGravity; }
public double surfaceWeight(double mass) {
return mass * surfaceGravity; // F = ma
}
}
java中printf的换行为%n。
特定于常量的方法实现:
public enum Operation{
PLUS { double apply(double x,double y){ return x+y; } },
MINUS { double apply(double x,double y){ return x-y; } },
TIMES { double apply(double x,double y){ return x*y; } },
DIVIDE { double apply(double x,double y){ return x/y; } };
abstract double apply(double x,double y);
}
下面的Operation覆盖了toString来返回通常与该操作关联的字符:
// Enum type with constant-specific class bodies and data
import java.util.*;
public enum Operation {
PLUS("+") {
double apply(double x, double y) { return x + y; }
},
MINUS("-") {
double apply(double x, double y) { return x - y; }
},
TIMES("*") {
double apply(double x, double y) { return x * y; }
},
DIVIDE("/") {
double apply(double x, double y) { return x / y; }
};
private final String symbol;
Operation(String symbol) { this.symbol = symbol; }
@Override public String toString() { return symbol; }
abstract double apply(double x, double y);
// Test program to perform all operations on given operands
public static void main(String[] args) {
double x = Double.parseDouble(args[0]);
double y = Double.parseDouble(args[1]);
for (Operation op : Operation.values())
System.out.printf("%f %s %f = %f%n",
x, op, y, op.apply(x, y));
}
}
编写一个fromString方法,将定制的字符串表示法变回相应的枚举:
// Implementing a fromString method on an enum type
private static final Map<String, Operation> stringToEnum
= new HashMap<String, Operation>();
static { // Initialize map from constant name to enum constant
for (Operation op : values())
stringToEnum.put(op.toString(), op);
}
// Returns Operation for string, or null if string is invalid
public static Operation fromString(String symbol) {
return stringToEnum.get(symbol);
}
策略枚举:
// The strategy enum pattern
enum PayrollDay {
MONDAY(PayType.WEEKDAY), TUESDAY(PayType.WEEKDAY),
WEDNESDAY(PayType.WEEKDAY), THURSDAY(PayType.WEEKDAY),
FRIDAY(PayType.WEEKDAY),
SATURDAY(PayType.WEEKEND), SUNDAY(PayType.WEEKEND);
private final PayType payType;
PayrollDay(PayType payType) { this.payType = payType; }
double pay(double hoursWorked, double payRate) {
return payType.pay(hoursWorked, payRate);
}
// The strategy enum type
private enum PayType {
WEEKDAY {
double overtimePay(double hours, double payRate) {
return hours <= HOURS_PER_SHIFT ? 0 :
(hours - HOURS_PER_SHIFT) * payRate / 2;
}
},
WEEKEND {
double overtimePay(double hours, double payRate) {
return hours * payRate / 2;
}
};
private static final int HOURS_PER_SHIFT = 8;
abstract double overtimePay(double hrs, double payRate);
double pay(double hoursWorked, double payRate) {
double basePay = hoursWorked * payRate;
return basePay + overtimePay(hoursWorked, payRate);
}
}
}
如果多个枚举常量同时共享相同的行为,则考虑策略枚举。
第31条:用实例域代替序数
永远不要根据枚举的序数导出与它关联的值(ordinal),而是要将它保存在一个实例域中:
// Enum with integer data stored in an instance field
public enum Ensemble {
SOLO(1), DUET(2), TRIO(3), QUARTET(4), QUINTET(5),
SEXTET(6), SEPTET(7), OCTET(8), DOUBLE_QUARTET(8),
NONET(9), DECTET(10), TRIPLE_QUARTET(12);
private final int numberOfMusicians;
Ensemble(int size) { this.numberOfMusicians = size; }
public int numberOfMusicians() { return numberOfMusicians; }
}
ordinal:大多数程序员都不需要这个方法,它是设计成用来像EnumSet和EnumMap这种基于枚举的通用数据结构的。除非你在编写的是这种数据结构,否则最好完全避免使用ordinal方法。
第32条:用EnumSet代替位域
// EnumSet - a modern replacement for bit fields
import java.util.*;
public class Text {
public enum Style { BOLD, ITALIC, UNDERLINE, STRIKETHROUGH }
// Any Set could be passed in, but EnumSet is clearly best
public void applyStyles(Set<Style> styles) {
// Body goes here
}
// Sample use
public static void main(String[] args) {
Text text = new Text();
text.applyStyles(EnumSet.of(Style.BOLD, Style.ITALIC));
}
}
第33条:用EnumMap代替序数索引
// Using a nested EnumMap to associate data with enum pairs
//Map(起始阶段,Map(目标阶段,过渡阶段))
import java.util.*;
public enum Phase {
SOLID, LIQUID, GAS;
public enum Transition {
MELT(SOLID, LIQUID), FREEZE(LIQUID, SOLID),
BOIL(LIQUID, GAS), CONDENSE(GAS, LIQUID),
SUBLIME(SOLID, GAS), DEPOSIT(GAS, SOLID);
private final Phase src;
private final Phase dst;
Transition(Phase src, Phase dst) {
this.src = src;
this.dst = dst;
}
// Initialize the phase transition map
private static final Map<Phase, Map<Phase,Transition>> m =
new EnumMap<Phase, Map<Phase,Transition>>(Phase.class);
static {
for (Phase p : Phase.values())
m.put(p,new EnumMap<Phase,Transition>(Phase.class));
for (Transition trans : Transition.values())
m.get(trans.src).put(trans.dst, trans);
}
public static Transition from(Phase src, Phase dst) {
return m.get(src).get(dst);
}
}
// Simple demo program - prints a sloppy table
public static void main(String[] args) {
for (Phase src : Phase.values())
for (Phase dst : Phase.values())
if (src != dst)
System.out.printf("%s to %s : %s %n", src, dst,
Transition.from(src, dst));
}
}
最好不要用序数来索引数组,而要使用EnumMap。如果你所表示的这种关系是多维的,就使用EnumMap<...,EnumMap<...>>
。应用程序的程序员在一般情况下都不使用Enum.ordinal。
第34条:用接口模拟可伸缩的枚举
// Emulated extensible enum using an interface
public interface Operation {
double apply(double x, double y);
}
// Emulated extensible enum using an interface - Basic implementation
public enum BasicOperation implements Operation {
PLUS("+") {
public double apply(double x, double y) { return x + y; }
},
MINUS("-") {
public double apply(double x, double y) { return x - y; }
},
TIMES("*") {
public double apply(double x, double y) { return x * y; }
},
DIVIDE("/") {
public double apply(double x, double y) { return x / y; }
};
private final String symbol;
BasicOperation(String symbol) {
this.symbol = symbol;
}
@Override public String toString() {
return symbol;
}
}
// Emulated extension enum - extended implementation
import java.util.*;
public enum ExtendedOperation implements Operation {
EXP("^") {
public double apply(double x, double y) {
return Math.pow(x, y);
}
},
REMAINDER("%") {
public double apply(double x, double y) {
return x % y;
}
};
private final String symbol;
ExtendedOperation(String symbol) {
this.symbol = symbol;
}
@Override public String toString() {
return symbol;
}
// Test class to exercise all operations in "extension enum"
public static void main(String[] args) {
double x = Double.parseDouble(args[0]);
double y = Double.parseDouble(args[1]);
test(ExtendedOperation.class, x, y);
System.out.println(); // Print a blank line between tests
test2(Arrays.asList(ExtendedOperation.values()), x, y);
}
// test parameter is a bounded type token (Item 29)-有限制的令牌环
private static <T extends Enum<T> & Operation> void test(
Class<T> opSet, double x, double y) {
for (Operation op : opSet.getEnumConstants())
System.out.printf("%f %s %f = %f%n",
x, op, y, op.apply(x, y));
}
// test parameter is a bounded wildcard type (Item 28)-有限制的通配符
private static void test2(Collection<? extends Operation> opSet,
double x, double y) {
for (Operation op : opSet)
System.out.printf("%f %s %f = %f%n",
x, op, y, op.apply(x, y));
}
}
<T extends Enum<T> & Operation>
确保了Class对象既表示枚举又表示Operation的子类型。
虽然无法编写可扩展的枚举类型,却可以通过编写接口以及实现该接口的基础枚举类型,对它进行模拟。
第35条:注解优先于命名模式
命名模式:被用来表明有些程序元素需要通过某种工具或者框架进行特殊处理。如:JUnit测试框架原本要求它的用户一定要用test作为测试方法名称的开头。
缺点:
- 文字拼写错误会导致失败,且没有任何提示。
- 无法确保它们只用于相应的程序元素上。
- 没有提供将参数值与程序元素关联起来的好方法。
// Marker annotation type declaration
import java.lang.annotation.*;
/**
* Indicates that the annotated method is a test method.
* Use only on parameterless static methods.
* 只用于无参的静态方法
*/
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface Test {
}
@Retention(RetentionPolicy.RUNTIME)元注解表明,test注解应该在运行时保留。如果没有保留,测试工具就无法知道Test注解。
@Target(ElementType.METHOD)元注解表明,Test注解只在方法声明中才是合法的:它不能运用到类声明、域声明或者其他程序元素上。
标记注解:没有参数,只是“标注”被注解的元素。
// Program containing marker annotations
public class Sample {
@Test public static void m1() { } // Test should pass
public static void m2() { }
@Test public static void m3() { // Test Should fail
throw new RuntimeException("Boom");
}
public static void m4() { }
@Test public void m5() { } // INVALID USE: nonstatic method
public static void m6() { }
@Test public static void m7() { // Test should fail
throw new RuntimeException("Crash");
}
public static void m8() { }
}
没有用Test注解进行标注的4个方法会被测试工具忽略。
// Annotation type with an array parameter
import java.lang.annotation.*;
/**
* Indicates that the annotated method is a test method that
* must throw the any of the designated exceptions to succeed.
* 带参数的注解
*/
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface ExceptionTest {
//参数数组
Class<? extends Exception>[] value();
//单个参数
//Class<? extends Exception> value();
}
// Program containing annotations with a parameter
import java.util.*;
public class Sample2 {
@ExceptionTest(ArithmeticException.class)
public static void m1() { // Test should pass
int i = 0;
i = i / i;
}
@ExceptionTest(ArithmeticException.class)
public static void m2() { // Should fail (wrong exception)
int[] a = new int[0];
int i = a[1];
}
@ExceptionTest(ArithmeticException.class)
public static void m3() { } // Should fail (no exception)
// Code containing an annotation with an array parameter
@ExceptionTest({ IndexOutOfBoundsException.class,
NullPointerException.class })
public static void doublyBad() {
List<String> list = new ArrayList<String>();
// The spec permits this method to throw either
// IndexOutOfBoundsException or NullPointerException
list.addAll(5, null);
}
}
测试运行类:
// Program to process marker annotations
import java.lang.reflect.*;
public class RunTests {
public static void main(String[] args) throws Exception {
int tests = 0;
int passed = 0;
Class testClass = Class.forName(args[0]);//测试Sample类
for (Method m : testClass.getDeclaredMethods()) {
if (m.isAnnotationPresent(Test.class)) {
tests++;
try {
m.invoke(null);
passed++;
} catch (InvocationTargetException wrappedExc) {
Throwable exc = wrappedExc.getCause();
System.out.println(m + " failed: " + exc);
} catch (Exception exc) {
System.out.println("INVALID @Test: " + m);
}
//捕捉到InvocationTargetException之外的异常,表明是Test注解的无效用法
}
// Array ExceptionTest processing code
if (m.isAnnotationPresent(ExceptionTest.class)) {
tests++;
try {
m.invoke(null);
System.out.printf("Test %s failed: no exception%n", m);
} catch (Throwable wrappedExc) {
Throwable exc = wrappedExc.getCause();
Class<? extends Exception>[] excTypes =
m.getAnnotation(ExceptionTest.class).value();
int oldPassed = passed;
for (Class<? extends Exception> excType : excTypes) {
if (excType.isInstance(exc)) {
passed++;
break;
}
}
if (passed == oldPassed)
System.out.printf("Test %s failed: %s %n", m, exc);
}
}
}
System.out.printf("Passed: %d, Failed: %d%n",
passed, tests - passed);
}
}
PS:
对象 instanceof 类
类.isInstance(对象)
第36条:坚持使用Override注解
应该在你想要覆盖超类声明的每个方法中使用Override注解。
第37条:用标记接口定义类型
标记接口有两点胜过标记注解:
- 标记接口定义的类型是由被标记类的实例实现的;标记注解则没有定义这样的类型。
- 它们可以被更加精确地进行锁定。
标记接口例子:Serializable接口表明实例可以通过ObjectOutputStream进行处理。
标记注解胜过标记接口的优点:
- 可以通过默认的方式添加一个或者多个注解类型元素,给已被使用的注解类型添加更多的信息。随着时间的推移,简单的标记注解类型可以演变成更加丰富的注解类型。这种演变对于标记接口而言则是不可能的,因为它通常不可能在实现接口之后再给它添加方法。
- 它们是更大的注解机制的一部分。因此,标记注解在那些支持注解作为编程元素之一的框架中间同样具有一致性。
总而言之,标记接口和标记注解都各有用处。如果想要定义一个任何新方法都不会与之关联的类型,标记接口就是最好的选择。如果想要标记程序元素而非类和接口,考虑到未来可能要给标记添加更多的信息,或者标记要适用于已经广泛使用了注解类型的框架,那么标记注解就是正确的选择。如果你发现自己在编写的是目标为ElementType.TYPE的标记注解类型,就要花点时间考虑清楚,它是否真的应该为注解类型,想想标记接口是否会更加合适。
方法
第38条:检查方法的有效性
每当编写方法或者构造器的时候,应该考虑它的参数有哪些限制。应该把这些限制写到文档中,并且在每个方法体的开头处,通过显式的检查来实施这些限制。
第39条:必要时进行保护性拷贝
对于参数类型可以被不可信任方子类化的参数,请不要使用clone方法进行保护性拷贝。
// Broken "immutable" time period class
import java.util.*;
public final class Period {
private final Date start;
private final Date end;
/**
* @param start the beginning of the period
* @param end the end of the period; must not precede start
* @throws IllegalArgumentException if start is after end
* @throws NullPointerException if start or end is null
*/
public Period(Date start, Date end) {
if (start.compareTo(end) > 0)
throw new IllegalArgumentException(
start + " after " + end);
this.start = start;
this.end = end;
}
// Repaired constructor - makes defensive copies of parameters
// Stops first attack
// public Period(Date start, Date end) {
// this.start = new Date(start.getTime());
// this.end = new Date(end.getTime());
//
// if (this.start.compareTo(this.end) > 0)
// throw new IllegalArgumentException(start +" after "+ end);
// }
public Date start() {
return start;
}
public Date end() {
return end;
}
// Repaired accessors - make defensive copies of internal fields
// Stops second attack
// public Date start() {
// return new Date(start.getTime());
// }
//
// public Date end() {
// return new Date(end.getTime());
// }
public String toString() {
return start + " - " + end;
}
// Remainder omitted
}
// Two attacks on the internals of an "immutable" period
import java.util.*;
public class Attack {
public static void main(String[] args) {
// Attack the internals of a Period instance
Date start = new Date();
Date end = new Date();
Period p = new Period(start, end);
end.setYear(78); // Modifies internals of p!
System.out.println(p);
// Second attack on the internals of a Period instance
start = new Date();
end = new Date();
p = new Period(start, end);
p.end().setYear(78); // Modifies internals of p!
System.out.println(p);
}
}
有经验的程序员通常使用Date.getTime()返回的long基本类型作为内部的时间表示法,而不是使用Date对象引用,主要因为Date是可变的。
如果类具有从客户端得到或者返回到客户端的可变组件,类就必须保护性地拷贝这些组件。如果拷贝的成本受到限制,并且类信任它的客户端不会不恰当地修改组件,就可以在文档中指明客户端的职责是不得修改受到影响的组件,以此来代替保护性拷贝。
第40条:谨慎设计方法签名
- 谨慎地选择方法的名称
- 不要过于追求提供便利的方法
- 避免过长的参数列表。目标是四个参数或者更少。相同类型的长参数序列格外有害。
缩短过长的参数列表的方法:
- 把方法分解成多个方法,每个方法只需要这些参数的一个子集。
- 创建辅助类,用来保存参数的分组。
- 从对象构建到方法调用都采用Builder模式。
对于参数类型,要优先使用接口而不是类。
对于boolean参数,要优先使用两个元素的枚举类型。
第41条:慎用重载
对于重载方法的选择是静态的,对于被覆盖方法的选择是动态的。
// Broken! - What does this program print?
import java.util.*;
import java.math.*;
public class CollectionClassifier {
public static String classify(Set<?> s) {
return "Set";
}
public static String classify(List<?> lst) {
return "List";
}
public static String classify(Collection<?> c) {
return "Unknown Collection";
}
public static void main(String[] args) {
Collection<?>[] collections = {
new HashSet<String>(),
new ArrayList<BigInteger>(),
new HashMap<String, String>().values()
};
for (Collection<?> c : collections)
System.out.println(classify(c));
}
}
输出结果为3个”Unknown Collection”,因为classify方法被重载了,而要调用哪个重载方法是在编译时做出决定的。对于for循环中的三次迭代,参数的编译时类型都是Collection<?>
。
应该避免胡乱地使用重载机制。
安全而保守的策略是,永远不要导出两个具有相同参数数目的重载方法。如果方法使用可变参数,保守的策略是根本不要重载它。
第42条:慎用可变参数
可变参数机制通过先创建一个数组,数组的大小为在调用位置所传递的参数数量,然后将参数值传到数组中,最后将数组传递给方法。
// Sample uses of varargs
public class Varargs {
// Simple use of varargs
static int sum(int... args) {
int sum = 0;
for (int arg : args)
sum += arg;
return sum;
}
// The WRONG way to use varargs to pass one or more arguments!
// static int min(int... args) {
// if (args.length == 0)
// throw new IllegalArgumentException("Too few arguments");
// int min = args[0];
// for (int i = 1; i < args.length; i++)
// if (args[i] < min)
// min = args[i];
// return min;
// }
// The right way to use varargs to pass one or more arguments
static int min(int firstArg, int... remainingArgs) {
int min = firstArg;
for (int arg : remainingArgs)
if (arg < min)
min = arg;
return min;
}
public static void main(String[] args) {
System.out.println(sum(1, 2, 3, 4, 5, 6, 7, 8, 9, 10));
System.out.println(min(1, 2, 3, 4, 5, 6, 7, 8, 9, 10));
}
}
第43条:返回零长度的数组或集合,而不是null
private final List<Cheese> cheesesInStock = ...;
private static final Cheese[] EMPTY_CHEESE_ARRAY = new Cheese[0];
/**
* @return an array containing all of the cheeses in the shop
*/
public Cheese[] getCheeses(){
return cheesesInStock.toArray(EMPTY_CHEESE_ARRAY);
}
Collection.toArray(T[])的规范保证:如果输入数组大到足够容纳这个集合,它就将返回这个输入数组。因此,这种做法永远也不会分配零长度的数组。
同样地,集合值的方法也可以做成在每当需要返回空集合时都返回同一个不可变的空集合。Collection.emptySet,emptyList和emptyMap方法提供的正是你所需要的,如下所示:
public List<Cheese> getCheeseList(){
if(cheesesInStock.isEmpty()){
return Collection.emptyList();//Always returns same list
}
else
return new ArrayList<Cheese>(cheesesInStock);
}
返回类型为数组或集合的方法没有理由返回null,而是返回一个零长度的数组或集合。
第44条:为所有导出的API元素编写文档注释
为了正确地编写API文档,必须在每个被导出的类、接口、构造器、方法和域声明之前增加一个文档注释。为了编写出可维护的代码,还应该为那些没有被导出的类、接口、构造器、方法和域编写文档注释。
方法的文档注释应该简洁地描述出它和客户端之间的约定:前提条件、后置条件、副作用、线程安全性。
按惯例,@param,@return或者@throws标签后面的短语或者子句都不用句点来结束。
Javadoc的{@code}标签的作用:使该代码片段以代码字体呈现;限制HTML标记和嵌套的Javadoc标签在代码片段中进行处理。
为了将多个代码示例包含在一个文档注释中,要使用包在HTML的<pre>
标签里面的Javadoc{@code}标签:<pre>{@code 多个代码示例}</pre>
。
当”this”被用在实例方法的文档注释中时,它应该始终是指方法调用所在的对象。
为了产生包含HTML元字符的文档,比如小于号(<),大于号(>)以及“与”号(&),必须采取特殊的动作:用{@literal}标签将它们包围起来。除了它不以代码字体渲染文本之外,其余方面就像{@code}标签一样。例如:
* The triangle inequality is {@literal |x+y|<|x|+|y|}.
文档注释在源代码和产生的文档中都应该是易于阅读的。如果无法让两者都易读,产生的文档的可读性要优先于源代码的可读性。
每个文档注释的第一句话成了该注释所属元素的概要描述。概要描述必须独立地描述目标元素的功能。为了避免混淆,同一个类或者接口中的两个成员或者构造器,不应该具有同样的概要描述。特别要注意重载的情形。
注意所期待的概要描述中是否包括句点,因为句点会过早地终止这个描述。问题在于,概要描述在后面接着空格、跳格或者行终止符的第一个句点(或者在第一个块标签处)结束。最好的解决方法是,将讨厌的句点以及任何与{@literal}关联的文本都包含起来。
规范指出,概要描述很少是个完整的句子。对于方法和构造器而言,概要描述应该是个完整的动词短语(包含任何对象),它描述了该方法所执行的动作;对于类、接口和域,概要描述应该是一个名词短语,它描述了该类或者接口的实例,或者域本身所代表的事物。
当为“泛型”或者方法编写文档时,确保要在文档中说明所有的类型参数。当为”枚举”类型编写文档时,要确保在文档中说明常量,以及类型,还有任何公有的方法。为”注解”类型编写文档时,要确保在文档中说明所有成员,以及类型本身。
从Java1.5发行版本开始,包级私有的文档注释就应该放在一个称作package-info.java的文件中,而不是放在package.html中。除了包级私有的文档注释之外,package-info.java也可以(单并非必需)包含包声明和包注解。
类是否是线程安全的,应该在文档中对它的线程安全级别进行说明;如果类是可序列化的,就应该在文档中说明它的序列化形式。
Javadoc具有“继承”方法注释的能力。如果API元素没有文档注释,Javadoc将会搜索最为适用的文档注释,接口的文档注释优先于超类的文档注释。也可以利用{@inheritDoc}标签从超类型中继承文档注释的部分内容。但是继承机制使用起来需要一些小技巧,并具有一些局限性。
为了降低文档注释中出错的可能性,一种简单的办法是通过一个HTML有效性检查器来运行由Javadoc产生的HTML文件。
在文档注释内部出现任何HTML标签都是允许的,但是HTML元字符必须要经过转义。
通用程序设计
第45条:将局部变量的作用域最小化
java允许你在任何可以出现语句的地方声明变量。
要使局部变量的作用域最小化,最有力的方法就是在第一次使用它的地方声明。
几乎每个局部变量的声明都应该包含一个初始化表达式。如果你还没有足够的信息来对一个变量进行有意义的初始化,就应该推迟这个声明,直到可以初始化为止。
第46条:for-each循环优先于传统的for循环
// Can you spot the bug?
import java.util.*;
enum Suit { CLUB, DIAMOND, HEART, SPADE }
enum Rank { ACE, DEUCE, THREE, FOUR, FIVE, SIX, SEVEN, EIGHT,
NINE, TEN, JACK, QUEEN, KING }
class Card {
final Suit suit;
final Rank rank;
Card(Suit suit, Rank rank) {
this.suit = suit;
this.rank = rank;
}
}
public class NestedIteration {
public static void main(String[] args) {
Collection<Suit> suits = Arrays.asList(Suit.values());
Collection<Rank> ranks = Arrays.asList(Rank.values());
List<Card> deck = new ArrayList<Card>();
for (Iterator<Suit> i = suits.iterator(); i.hasNext(); )
for (Iterator<Rank> j = ranks.iterator(); j.hasNext(); )
deck.add(new Card(i.next(), j.next()));
// Preferred idiom for nested iteration on collections and arrays
// for (Suit suit : suits)
// for (Rank rank : ranks)
// deck.add(new Card(suit, rank));
}
}
for循环存在的问题:在迭代器上对外部的集合(suits)调用了太多次next方法。
for-each循环不仅让你遍历集合和数组,还让你遍历任何实现Iterable接口的对象。
有三种常见的情况无法使用for-each循环:
- 过滤-如果需要遍历集合,并删除选定的元素,就需要使用显式的迭代器,以便可以调用它的remove方法。
- 转换-如果需要遍历列表或者数组,并取代它部分或者全部的元素值,就需要列表迭代器或者数组索引,以便设定元素的值。
- 平行迭代-如果需要并行地遍历多个集合,就需要显式地控制迭代器或者索引变量,以便所有迭代器或者索引变量都可以得到同步前移。
// Same bug as NestIteration.java (but different symptom)!!
import java.util.*;
enum Face { ONE, TWO, THREE, FOUR, FIVE, SIX }
public class DiceRolls {
public static void main(String[] args) {
Collection<Face> faces = Arrays.asList(Face.values());
for (Iterator<Face> i = faces.iterator(); i.hasNext(); )
for (Iterator<Face> j = faces.iterator(); j.hasNext(); )
System.out.println(i.next() + " " + j.next());
//平行迭代的情况
// Preferred idiom for nested iteration on collections and arrays
// for (Face face1 : faces)
// for (Face face2 : faces)
// System.out.println(face1 + " " + face2);
}
}
第47条:了解和使用类库
使用标准类库的好处:
- 通过使用标准类库,可以充分利用这些编写标准类库的专家的知识,以及在你之前的其他人的使用经验。
- 不必浪费时间为那些与工作不太相关的问题提供特别的解决方案。就像大多数程序员一样,应该把时间花在应用程序上,而不是底层的细节上。
- 它们的性能往往会随着时间的推移而不断提高,无需你做任何努力。
- 可以使自己的代码融入主流。这样的代码更易读、更易维护、更易被大多数的开发人员重用。
在每个重要的发行版本中,都会有许多新的特性被加入到类库中,所以与这些新特性保持同步是值得的。
第48条:如果需要精确的答案,请避免使用float和double
float和double类型尤其不适合用于货币计算,因为要让一个float或者double精确地表示0.1(或者10的任何其他负数次方值)是不可能的。正确方法是使用BigDecimal、int或者long进行货币计算。
第49条:基本类型优先于装箱基本类型
基本类型和装箱基本类型之间的三个主要区别:
- 基本类型只有值,而装箱基本类型则具有与它们的值不同的同一性。换句话说,两个装箱基本类型可以具有相同的值和不同的同一性。
- 基本类型只有功能完备的值,而每个装箱基本类型除了它对应基本类型的所有功能值之外,还有个非功能值:null。
- 基本类型通常比装箱基本类型更节省时间和空间。
对装箱基本类型运用==操作符几乎总是错误的。
当在一项操作中混合使用基本类型和装箱基本类型时,装箱基本类型就会自动拆箱。当值为null时,拆箱会抛出NullPointerException。
装箱基本类型的合理用处:
- 作为集合中的元素、键和值。
- 在参数化类型中,必须使用装箱基本类型作为类型参数。
- 在进行反射的方法调用时,必须使用装箱基本类型。
第50条:如果其他类型更适合,则尽量避免使用字符串
不应该使用字符串的情形:
- 字符串不适合代替其他的值类型。
- 字符串不适合代替枚举类型。
- 字符串不适合代替聚集类型。
- 字符串也不适合代替能力表。
第51条:当心字符串连接的性能
为连接n个字符串而重复地使用字符串连接操作符,需要n的平方级的操作时间。
为了获得可以接受的性能,请使用StringBuilder代替String。
性能最差的方式:
public static String concat1(String s1, String s2, String s3, String s4, String s5, String s6) {
String result = "";
result += s1;
result += s2;
result += s3;
result += s4;
result += s5;
result += s6;
return result;
}
最好的方式:
public static String concat2(String s1, String s2, String s3, String s4, String s5, String s6) {
return s1 + s2 + s3 + s4 + s5 + s6;
}
第二种方式Java会自动使用StringBuilder.append()函数来进行连接。
第52条:通过接口引用对象
如果有合适的接口类型存在,那么对于参数、返回值、变量和域来说,就都应该使用接口类型进行声明。
如果对象属于基于类的框架,就应该用相关的基类(往往是抽象类)来引用这个对象,而不是用它的实现类。
第53条:接口优先于反射机制
反射机制允许一个类使用另一个类,即使当前者被编译的时候后者还根本不存在。然而,这种能力也要付出代价:
- 丧失了编译时类型检查的好处,包括异常检查。
- 执行反射访问所需要的代码非常笨拙和冗长。
- 性能损失。反射方法调用比普通方法调用慢了许多。
反射方法只是在设计时被用到。通常,普通应用程序在运行时不应该以反射方式使用对象。
简而言之,反射机制是一种功能强大的机制,对于特定的复杂系统编程任务,它是非常必要的,但它也有一些缺点。如果你编写的程序必须要与编译时未知的类一起工作,如有可能,就应该仅仅使用反射机制来实例化对象,而访问对象时则使用编译时已知的某个接口或者超类。
第54条:谨慎地使用本地方法
从历史上看,本地方法主要有三种用途:
- 提供了“访问特定于平台的机制”的能力。
- 提供了访问遗留代码库的能力。
- 可以通过本地语言,编写应用程序中注重性能的部分,以提高系统的性能。
使用本地方法有一些严重的缺点:
- 因为本地语言是不安全的,所以,使用本地方法的应用程序也不再能免受内存毁坏错误的影响。
- 因为本地语言是与平台相关的,使用本地方法的应用程序也不再是可自由移植的
- 在进入和退出本地代码时,需要相关的固定开销,所以,如果本地代码只是做少量的工作,本地方法就可能降低性能。
最后一点,需要”胶合代码“的本地方法编写起来单调乏味,并且难以阅读。
总而言之,在使用本地方法之前务必三思。极少数情况下会需要使用本地方法来提高性能。如果你必须要使用本地方法来访问底层的资源,或者遗留代码库,也要尽可能少用本地代码,并且要全面进行测试。本地代码中的一个Bug就有可能破坏整个应用程序。
第55条:谨慎地进行优化
三条与优化有关的格言:
很多计算上的过失都被归咎于效率(没有必要达到的效率),而不是任何其他的原因——甚至包括盲目地做傻事。(William A. Wulf)
不要去计较效率上的一些小小的得失,在97%的情况下,不成熟的优化才是一切问题的根源。(Donald E. Knuth)
在优化方面,我们应该遵守两条规则:
规则1:不要进行优化。
规则2(仅针对专家):还是不要进行优化——也就是说,在你还没有绝对清晰的未优化方案之前,请不要进行优化。(M.A. Jackson)
总而言之,不要费力去编写快速的程序——应该努力编写好的程序,速度自然会随之而来。在设计系统的时候,特别是在设计API、线路层协议和永久数据格式的时候,一定要考虑性能的因素。在构建完系统之后,要测量它的性能。如果它足够快,你的任务就完成了。如果不够快,则可以在性能剖析器的帮助下,找到问题的根源,然后设法优化系统中相关的部分。第一个步骤是检查所选择的算法:再多的低层优化也无法弥补算法的选择不当。必要时重复这个过程,在每次改变之后都要测量性能,直到满意为止。
第56条:遵守普遍接受的命名惯例
命名惯例分为两大类:字面的、语法的。
异常
第57条:只针对异常的情况才使用异常
异常是为了在异常情况下使用而设计的,不要将它们用于普通的控制流,也不要编写迫使它们这么做的API。
第58条:对可恢复的情况使用受检异常,对编程错误使用运行时异常
Java程序设计语言提供了3种可抛出结构:受检的异常、运行时异常和错误。
第59条:避免不必要地使用受检的异常
第60条:优先使用标准的异常
重用现有异常的好处:
- 使你的API更加易于学习和使用,因为它与程序员已经熟悉的习惯用法是一致的。
- 对于用到这些API的程序而言,它们的可读性更好。
- 异常类越少,意味着内存印迹就越小,装载这些类的时间开销也越少。
常用的异常:
异常 | 使用场合 |
---|---|
IllegalArgumentException | 非null的参数值不正确 |
IllegalStateException | 对于方法调用而言,对象状态不合适 |
NullPointerException | 在禁止使用null的情况下参数值为null |
IndexOutOfBoundsException | 下标参数值越界 |
ConcurrentModificationException | 在禁止并发修改的情况下,检测到对象的并发修改 |
UnsupportedOperationException | 对象不支持用户请求的方法 |
所有错误的方法调用都可以被归结为非法参数或者非法状态。
第61条:抛出与抽象相对应的异常
更高层的实现应该捕获低层的异常,同时抛出可以按照高层抽象进行解释的异常。这种做法被称为异常转译(exception translation)。
如果不能阻止或者处理来自更低层的异常,一般的做法是使用异常转译,除非低层方法碰巧可以保证它所抛出的所有异常对高层也合适才可以将异常从低层传播到高层。异常链对高层和低层异常都提供了最佳的功能:它允许抛出适当的高层异常,同时又能捕获低层的原因进行失败分析。
第62条:每个方法抛出的异常都要有文档
始终要单独地声明受检的异常,并且利用Javadoc的@throws标记,准确地记录下抛出每个异常的条件。如果一个方法可能抛出多个异常类,则不要使用“快捷方式”声明它会抛出这些异常类的某个超类。
每个方法的文档应该描述它的前提条件,在文档中记录下未受检的异常是满足前提条件的最佳做法。
使用Javadoc的@throws标签记录下一个方法可能抛出的每个未受检异常,但是不要使用@throws关键字将未受检的异常包含在方法的声明中。
如果一个类中的许多方法出于同样的原因而抛出同一个异常,在该类的文档注释中对这个异常建立文档,而不是为每个方法单独建立文档。
要为你编写的每个方法所能抛出的每个异常建立文档。对于未受检的和受检的异常,以及对于抽象的和具体的方法也都一样。要为每个受检异常提供单独的throws子句,不要为未受检的异常提供throws子句。如果没有为可以抛出的异常建立文档,其他人就很难或者根本不可能有效地使用你的类和接口。
第63条:在细节消息中包含能捕获失败的信息
异常类型的toString方法应该尽可能多地返回有关失败原因的信息。
为了捕获失败,异常的细节信息应该包含所有“对该异常有贡献”的参数和域的值。
为了确保在异常的细节消息中包含足够的能捕获失败的信息,一种方法是在异常的构造器而不是字符串细节消息中引入这些信息。
第64条:努力使失败保持原子性
一般而言,失败的方法调用应该使对象保持在被调用之前的状态。具有这种属性的方法被称为具有失败原子性。
实现方法:
- 设计一个不可变的对象。
- 对于在可变对象上执行操作的方法,在执行方法之前检查参数的有效性。这可以使得在对象的状态被修改之前,先抛出适当的异常。
//Stack.pop
public Object pop(){
if(size==0)
throw new EmptyStackExcption();
Object result = elements[--size];
elements[size] = null;
return result;
}
- 与上述类似的方法,调整计算处理过程的顺序,使得任何可能会失败的计算部分都在对象状态被修改之前发生。
- 一种不常用的方法,编写一段恢复代码。这种办法主要用于永久性的(基于磁盘的)数据结构。
- 在对象的一份临时拷贝上执行操作,当操作完成之后再用临时拷贝中的结果代替对象中的内容。例如,Collections.sort在执行排序之前,首先把它的输入列表转到一个数组中,以便降低在排序的内循环中访问元素所需要的开销。这是出于性能考虑的做法,但是,它增加了一项优势:即使排序失败,它也能保证输入列表保持原样。
虽然一般情况下都希望实现失败原子性,但并非总是可以做到。
错误(相对于异常)通常是不可恢复的,当方法抛出错误时,它们不需要努力保持失败原子性。
第65条:不要忽略异常
空的catch块会使异常达不到应有的目的,即强迫你处理异常的情况。忽略异常就如同忽略火警信号一样。至少,catch块也应该包含一条声明,解释为什么可以忽略这个异常。
本条目中的建议同样适用于受检异常和未受检的异常。正确地处理异常能够彻底挽回失败。只要将异常传播给外界,至少会导致程序迅速地失败,从而保留了有助于调试该失败条件的信息。
并发
第66条:同步访问共享的可变数据
如果没有同步,一个线程的变化就不能被其他线程看到。同步不仅可以阻止一个线程看到对象处于不一致的状态之中,它还可以保证进入同步方法或同步代码块的每个线程,都看到由同一个锁保护的之前所有的修改效果。
Java语言规范保证读或者写一个变量是原子的,除非这个变量的类型为long或者double。
如果读和写操作没有都被同步,同步就不会起作用。
简而言之,当多个线程共享可变数据的时候,每个读或者写数据的线程都必须执行同步。
第67条:避免过度同步
在一个被同步的区域内部,不要调用设计成要被覆盖的方法,或者是由客户端以函数对象的形式提供的方法。从包含该同步区域的类的角度来看,这样的方法是外来的。这个类不知道该方法会做什么事情,也无法控制它。根据外来方法的作用,从同步区域中调用它会导致异常、死锁或者数据损坏。
在同步区域之外被调用的外来方法被称作“开放调用”。除了可以避免死锁之外,开放调用还可以极大地增加并发性。
通常,你应该在同步区域内做尽可能少的工作。
第68条:executor和task优先于线程
executor:
SingleThreadExecutor
ThreadPoolExecutor
CachedThreadPool
FixedThreadPool
ScheduledThreadPoolExecutor
task:
Runnable
Callable
第69条:并发工具优先于wait和notify
并发集合为标准的集合接口(如List,Queue和Map)提供了高性能的并发实现。为了提供高并发性,这些实现在内部自己管理同步。因此,并发集合中不可能排除并发活动;将它锁定没有什么作用,只会使程序的速度变慢。
除非不得已,否则应该优先使用ConcurrentHashMap,而不是使用Collections.synchronizedMap或者HashTable。只要用并发Map代替老式的同步Map,就可以极大地提升并发应用程序的性能。更一般地,应该优先使用并发集合,而不是使用外部同步的集合。
对于间歇式的定时,始终应该优先使用System.nanoTime,而不是使用System.currentTimeMills。System.nanoTime更加准确也更加精确,它不受系统的实时时钟的调整所影响。
使用wait方法的标准模式:
//the standard idiom for using the wait method
synchronized(obj){
while(<condition does not hold>){
obj.wait();//release lock, and requires on wakeup
}
//perform action appropriate to condition
}
始终应该使用while循环模式来调用wait方法;永远不要在循环之外调用wait方法。循环会在等待之前和之后测试条件。
一般情况下,你应该优先使用notifyAll,而不是使用notify。
第70条:线程安全性的文档化
一个类为了可被多个线程安全地使用,必须在文档中清楚地说明它所支持的线程安全性级别。
常见的几种线程安全性级别:
- 不可变的——这个类的实例是不变的。(String, Long, BigInteger)
- 无条件的线程安全——这个类的实例是可变的,但是这个类有着足够的内部同步,所以它的实例可以被并发使用,无需任何外部同步。(Random, ConcurrentHashMap)
- 有条件的线程安全——除了有些方法为进行安全的并发而需要使用外部同步之外,这种线程安全级别与无条件的线程安全相同。(Collections.synchronized包装返回的集合,它们的迭代器(iterator)要求外部同步)
- 非线程安全——这个类的实例是可变的。(ArrayList, HashMap)
- 线程对立的——这个类不能安全地被多个线程并发使用,即使所有的方法调用都被外部同步包围。线程对立的根源通常在于,没有同步地修改静态资源。
类的线程安全说明通常放在它的文档注释中,但是带有特殊线程安全属性的方法则应该在它们自己的文档注释中说明它们的属性。
有条件的线程安全类必须在文档中指明“哪个方法调用序列需要外部同步,以及在执行这些序列的时候要获得哪把锁”。如果你编写的是无条件的线程安全类,就应该考虑用私有锁对象来代替同步的方法。这样可以防止客户端程序(拒绝服务攻击,超时地保持公有可访问锁)和子类(出于不同的目的而使用相同的锁,子类和基类可能会“相互绊住对方的脚”)的不同步干扰,让你能够在后续的版本中灵活地对并发控制采用更加复杂的方法。
第71条:慎用延迟初始化
延迟初始化就像一把双刃剑。它降低了初始化类或者创建实例的开销,却增加了访问被延迟初始化的域的开销。根据延迟初始化的域最终需要初始化的比例、初始化这些域要多少开销,以及每个域多久被访问一次,延迟初始化(就像其他的许多优化一样)实际上降低了性能。
如果域只在类的实例部分被访问,并且初始化这个域的开销很高,可能就值得进行延迟初始化。要确定这一点,唯一的办法就是测量类在用和不用延迟初始化时的性能差别。
在大多数情况下,正常的初始化要优先于延迟初始化。
正常初始化实例域的一个典型声明:
//normal initialization of an instance field
private final FieldType field = computeFieldValue();
如果利用延迟优化来破坏初始化的循环,就要使用同步访问方法。
//lazy initialization of instance field - synchronized accessor
private FieldType field;
synchronized FieldType getField(){
if(field == null)
field = computeFieldValue();
return field;
}
如果出于性能的考虑而需要对静态域使用延迟初始化,就使用 lazy initialization holder class模式。这种模式保证了类要到被用到的时候才会被初始化:
// Lazy initialization holder class idiom for static fields
private static class FieldHolder {
static final FieldType field = computeFieldValue();
}
static FieldType getField() { return FieldHolder.field; }
当getField方法第一次被调用时,它第一次读取FieldHolder.field,导致FieldHolder类得到初始化。这种模式的魅力在于,getField方法没有被同步,并且只执行一个域访问,因此延迟初始化实际上并没有增加任何访问成本。现代的VM将在初始化该类的时候,同步域的访问。一旦这个类被初始化,VM将修补代码,以便后续对该域的访问不会导致任何测试或者同步。
如果出于性能的考虑而需要对实例域使用延迟初始化,就使用双重检查模式。这种模式避免了在域被初始化之后访问这个域时的锁定开销。这种模式的背后思想是:两次检查域的值,第一次检查时没有锁定,看看这个域是否被初始化了;第二次检查时有锁定。域声明为volatile很重要。
// Double-check idiom for lazy initialization of instance fields
private volatile FieldType field;
FieldType getField() {
FieldType result = field;
if (result == null) { // First check (no locking)
synchronized(this) {
result = field;
if (result == null) // Second check (with locking)
field = result = computeFieldValue();
}
}
return result;
}
局部变量result的作用是确保field只在已经被初始化的情况下读取一次。虽然这不是严格要求,但是可以提升性能,并且因为给低级的并发编程应用了一些标准,因此更加优雅。
有时候,你可能需要延迟初始化一个可以接受重复初始化的实例域。如果处于这种情况,就可以使用单重检查模式。注意field仍然被声明为volatile。
// Single-check idiom - can cause repeated initialization!
private volatile FieldType field;
private FieldType getField() {
FieldType result = field;
if (result == null)
field = result = computeFieldValue();
return result;
}
本条目中的所有初始化方法都适用于基本类型的域,以及对象引用域。(null换成对应的默认初始值)
如果你不在意是否每个线程都重新计算域的值,并且域的类型为基本类型,而不是long或者double类型,就可以选择从单重检查模式的域声明中删除volatile修饰符。这种变体称之为racy single-check idiom。它加快了某些架构上的域访问,代价是增加了额外的初始化(访问该域的每个线程都进行一次初始化)。
第72条:不要依赖于线程调度器
任何依赖于线程调度器来达到正确性或者性能要求的程序,很有可能都是不可移植的。(因为不同操作系统所采用的调度策略大相径庭)
线程优先级是Java平台上最不可移植的特征。线程优先级可以用来提高一个已经能够正常工作的程序的服务质量,但永远不应该用来“修正”一个原本并不能工作的程序。
应该使用Thread.sleep(1)代替Thread.yield来进行并发测试。
第73条:避免使用线程组
线程组并没有提供太多有用的功能,而且它们提供的许多功能都是有缺陷的。因为线程组已经过时了,所以根本没有必要修正。如果你正在设计的一个类需要处理线程的逻辑组,或许就应该使用线程池。
序列化
第74条:谨慎地实现Serializable接口
实现Serializable接口而付出的最大代价是,一旦一个类被发布,就大大降低了“改变这个类的实现”的灵活性。如果你接受了默认的序列化形式,这个类中私有的和包级私有的实例域都将变成导出的API的一部分,这不符合“最低限度地访问域”的实践准则,从而它就失去了作为信息隐藏工具的有效性。
自动地产生的标识号(serial version UID)会受到类名称、它所实现的接口的名称、以及所有公有的和受保护的成员的名称所影响。因此,如果你没有声明一个显式的序列版本UID,兼容性将会遭到破坏,在运行时导致InvalidClassException异常。
实现Serializable的第二个代价是,它增加了出现Bug和安全漏洞的可能性。无论你是接受了默认的行为,还是覆盖了默认的行为,反序列化机制都是一个“隐藏的构造器”,具备与其他构造器相同的特点。依靠默认的反序列化机制,很容易使对象的约束关系遭到破坏,以及遭受到非法访问。
实现Serializable的第三个代价是,随着类发行新的版本,相关的测试负担也增加了。当一个可序列化的类被修订的时候,很重要的一点是,要检查是否可以“在新版本中序列化一个实例,然后在旧版本中反序列化”,反之亦然。因此,测试所需要的工作量与“可序列化的类的数量和发行版本号”的乘积成正比。
每当你实现一个类的时候,都需要权衡一下所付出的代价和带来的好处。根据经验,比如Date和BigInteger这样的值类应该实现 Serializable,大多数的集合类也应该如此。代表活动实体的类,比如线程池,一般不应该实现Serializable。
为了继承而设计的类应该尽可能少地去实现Serializable接口,用户的接口也应该尽可能少地继承Serializable接口。
在为了继承而设计的类中,真正实现了Serializable接口的有Throwable类、Component和HttpServlet抽象类。
如果超类没有提供可访问的无参构造器,子类也不可能做到可序列化。因此,对于为继承而设计的不可序列化的类,你应该考虑提供一个无参构造器。
给”不可序列化但可扩展的类“增加无参构造器的方法:
// Nonserializable stateful class allowing serializable subclass
import java.util.concurrent.atomic.*;
public abstract class AbstractFoo {
private int x, y; // Our state
// This enum and field are used to track initialization
private enum State { NEW, INITIALIZING, INITIALIZED };
private final AtomicReference<State> init =
new AtomicReference<State>(State.NEW);
public AbstractFoo(int x, int y) { initialize(x, y); }
// This constructor and the following method allow
// subclass's readObject method to initialize our state.
protected AbstractFoo() { }
protected final void initialize(int x, int y) {
if (!init.compareAndSet(State.NEW, State.INITIALIZING))
throw new IllegalStateException(
"Already initialized");
this.x = x;
this.y = y;
// Do anything else the original constructor did
init.set(State.INITIALIZED);
}
// These methods provide access to internal state so it can
// be manually serialized by subclass's writeObject method.
protected final int getX() { checkInit(); return x; }
protected final int getY() { checkInit(); return y; }
// Must call from all public and protected instance methods
private void checkInit() {
if (init.get() != State.INITIALIZED)
throw new IllegalStateException("Uninitialized");
}
// Remainder omitted
}
// Serializable subclass of nonserializable stateful class
import java.io.*;
public class Foo extends AbstractFoo implements Serializable {
private void readObject(ObjectInputStream s)
throws IOException, ClassNotFoundException {
s.defaultReadObject();
// Manually deserialize and initialize superclass state
int x = s.readInt();
int y = s.readInt();
initialize(x, y);
}
private void writeObject(ObjectOutputStream s)
throws IOException {
s.defaultWriteObject();
// Manually serialize superclass state
s.writeInt(getX());
s.writeInt(getY());
}
// Constructor does not use the fancy mechanism
public Foo(int x, int y) { super(x, y); }
private static final long serialVersionUID = 1856835860954L;
}
内部类不应该实现Serializable,因为内部类的默认序列化形式是定义不清楚的。然而,静态成员类却可以实现Serializable接口。
第75条:考虑使用自定义的序列化形式
如果一个对象的物理表示法等同于它的逻辑内容,可能就适合于使用默认的序列化形式。即使你确定了默认的序列化形式是合适的,通常还必须提供一个readObject方法以保证约束关系和安全性。
当一个对象的物理表示法与它的逻辑数据内容有实质性的区别时,使用默认序列化形式会有以下4个缺点:
- 它使这个类的导出API永远束缚在该类的内部表示法上。
- 它会消耗过多的空间。
- 它会消耗过多的时间。
- 它会引起栈溢出。
// StringList with a reasonable custom serialized form
import java.io.*;
public final class StringList implements Serializable {
private transient int size = 0;
private transient Entry head = null;
// No longer Serializable!
private static class Entry {
String data;
Entry next;
Entry previous;
}
// Appends the specified string to the list
public final void add(String s) {
// Implementation omitted
}
/**
* Serialize this {@code StringList} instance.
*
* @serialData The size of the list (the number of strings
* it contains) is emitted ({@code int}), followed by all of
* its elements (each a {@code String}), in the proper
* sequence.
*/
private void writeObject(ObjectOutputStream s)
throws IOException {
s.defaultWriteObject();
s.writeInt(size);
// Write out all elements in the proper order.
for (Entry e = head; e != null; e = e.next)
s.writeObject(e.data);
}
private void readObject(ObjectInputStream s)
throws IOException, ClassNotFoundException {
s.defaultReadObject();
int numElements = s.readInt();
// Read in all elements and insert them in list
for (int i = 0; i < numElements; i++)
add((String) s.readObject());
}
private static final long serialVersionUID = 93248094385L;
// Remainder omitted
}
如果所有的实例域都是瞬时的(transient),从技术角度而言,不调用defaultWriteObject和defaultReadObject也是允许的,但是不推荐这样做。调用defaultWriteObject和defaultReadObject得到的序列化形式允许在以后的发行版本中增加非transient的实例域,并且还能保持向前或者向后兼容性。
在决定将一个域做成非transient的之前,请一定要确信它的值将是该对象逻辑状态的一部分。如果你正在使用一种自定义的序列化形式,大多数实例域,或者所有的实力域都应该被标记为transient。
如果你正在使用默认的序列化形式,并且把一个或者多个域标记为transient,则要记住,当一个实例被反序列化的时候,这些域将被初始化为它们的默认值。
无论你是否使用默认的序列化形式,如果在读取整个对象状态的任何其他方法上强制任何同步,则也必须在对象序列化上强制这种同步。
//writeObject for synchronized class with default serialized form
private synchronized void writeObject(ObjectOutputStream s)
throws IOException {
s.defaultWriteObject();
}
不管你选择了哪种序列化形式,都要为自己编写的每个可序列化的类声明一个显式的序列版本UID。这样可以避免序列版本UID成为潜在的不兼容根源。而且,这样做也会带来小小的性能好处(不需要通过计算产生)。
private static final long serialVersionUID = randomLongValue;
第76条:保护性地编写readObject方法
readObject方法实际上相当于另一个公有的构造器,如同其他的构造器一样,它也要求注意同样的所有注意事项。构造器必须检查其参数的有效性,并且在必要的时候对参数进行保护性拷贝,同样地,readObject方法也需要这样做。否则容易受到攻击。
当一个对象被反序列化的时候,对于客户端不应该拥有的对象引用,如果哪个域包含了这样的对象引用,就必须要做保护性拷贝。因此,对于每个可序列化的不可变类,如果它包含了私有的可变组件,那么在它的readObject方法中,必须要对这些组件进行保护性拷贝。
// readObject method with defensive copying and validity checking
// This will defend against BogusPeriod and MutablePeriod attacks.
private void readObject(ObjectInputStream s)
throws IOException, ClassNotFoundException {
s.defaultReadObject();
// Defensively copy our mutable components
start = new Date(start.getTime());
end = new Date(end.getTime());
// Check that our invariants are satisfied
if (start.compareTo(end) > 0)
throw new InvalidObjectException(start +" after "+ end);
}
同时也要注意到,对于final域,保护性拷贝是不可能的。
编写出更加健壮的readObject方法的指导方针:
- 对于对象引用域必须保持为私有的类,要保护性地拷贝这些域中的每个对象。不可变类的可变组件就属于这一类别。
- 对于任何约束条件,如果检查失败,则抛出一个InvalidObjectException异常。这些检查动作应该跟在所有的保护性拷贝之后。
- 如果整个对象图在被反序列化之后必须进行验证,就应该使用ObjectInputValidation接口。
- 无论是直接方式还是间接方式,都不要调用类中任何可被覆盖的方法。
第77条:对于实例控制,枚举类型优先于readResolve
如果依赖readResolve进行实例控制,带有对象引用类型的所有实例域都必须声明为transient类型的。
如果将一个可序列化的实例受控的类编写成枚举,就可以绝对保证除了所声明的常量之外,不会有别的实例。
readResolve的可访问性很重要。如果readResolve方法是受保护的或者公有的,并且子类没有覆盖它,对序列化过的子类实例进行反序列化,就会产生一个超类实例,这样有可能导致ClassCastException异常。
总而言之,你应该尽可能地使用枚举类型来实施实例控制的约束条件。如果做不到(它的实例在编译时还不知道),同时又需要一个既可序列化又是实例控制的类,就必须提供一个readResolve方法,并确保该类的所有实例域都为基本类型,或者是transient的。
第78条:考虑用序列化代理代替序列化实例
为可序列化的类设计一个私有的静态嵌套类,精确地表示外围类的实例的逻辑状态。这个嵌套类被称作序列化代理,它应该有一个单独的构造器,其参数类型就是那个外围类。这个构造器只从它的参数中复制数据:它不需要进行任何一致性检查或者保护性拷贝。从设计的角度来看,序列化代理的默认序列化形式是外围类最好的序列化形式。外围类及其序列代理都必须声明实现Serializable接口
// Period class with serialization proxy
import java.util.*;
import java.io.*;
public final class Period implements Serializable {
private final Date start;
private final Date end;
/**
* @param start the beginning of the period
* @param end the end of the period; must not precede start
* @throws IllegalArgumentException if start is after end
* @throws NullPointerException if start or end is null
*/
public Period(Date start, Date end) {
this.start = new Date(start.getTime());
this.end = new Date(end.getTime());
if (this.start.compareTo(this.end) > 0)
throw new IllegalArgumentException(
start + " after " + end);
}
public Date start () { return new Date(start.getTime()); }
public Date end () { return new Date(end.getTime()); }
public String toString() { return start + " - " + end; }
// Serialization proxy for Period class
private static class SerializationProxy implements Serializable {
private final Date start;
private final Date end;
SerializationProxy(Period p) {
this.start = p.start;
this.end = p.end;
}
private static final long serialVersionUID =
234098243823485285L; // Any number will do (Item 75)
// readResolve method for Period.SerializationProxy
private Object readResolve() {
return new Period(start, end); // Uses public constructor
}
}
// writeReplace method for the serialization proxy pattern
private Object writeReplace() {
return new SerializationProxy(this);
}
// readObject method for the serialization proxy pattern
private void readObject(ObjectInputStream stream)
throws InvalidObjectException {
throw new InvalidObjectException("Proxy required");
}
}
序列化代理的局限性:
- 它不能与可以被客户端扩展的类兼容。( 见第17条)
- 它不能与对象图中包含循环的某些类兼容:如果你企图从一个对象的序列化代理的readResolve方法内部调用这个对象中的方法,就会得到一个ClassCastException异常,因为你还没有这个对象,只有它的序列化代理。
总而言之,每当你发现自己必须在一个不能被客户端扩展的类上编写readObject或者writeObject方法的时候,就应该考虑使用序列化代理模式。要想稳健地将带有重要约束条件的对象序列化时,这种模式可能是最容易的方法。