摘自《Effective Java》

Effective Java

一、创建和销毁对象

1.考虑使用静态工厂方法替代构造器

  • 静态工厂方法与构造器不同的第一大优势在于,它们有名称,不必和类名相同。
  • 静态工厂方法与构造器不同的第二大优势在于不必在每次调用他们的时候都创建一个新的对象。
  • 静态工厂方法与构造器不同的第三大优势在于他们可以返回原类型的任何子类型对象。
  • 静态工厂方法的第四大优势在于,在创建参数化类型实例的时候,它们是代码变得更加简洁。
  • 静态工厂方法的主要缺点在于类如果不含公有的活着受保护的构造器,就不能被子类化。
  • 静态工厂方法第二个缺点在于它们与其他的静态方法实际上没有任何区别。

2.遇到多个构造器参数时要考虑用构建器

3.用私有构造器或着枚举类强化Singleton属性

4.通过私有构造器强化不可实例化的能力

5.避免创建不必要的对象

6.消除过期的对象引用

  • 只要类是自己管理内存,程序员就应该警惕内存泄露问题。
  • 内存泄漏的另一个常见来源是缓存。
  • 内存的第三个常见来源是监听器和其他回调。确保回调立即被当作垃圾回收的最佳方法是只保存它们的弱引用。

7.避免使用终结方法

  • 终结方法(finalizer)通常是不可预测的,也是危险的,一般情况下是不必要的。
  • 使用终结方法有非常严重的(Severe)性能损失。

二、对于所有对象都通用的方法

8.覆盖equals时请遵守通用约定

  • 类的每个实例本质都是唯一的。
  • 不关心类是否提供了“逻辑相等(logical equality)“的测试功能。
  • 超类已经覆盖了equals,从超类继承过来的行为对于子类也是合适的。
  • 类是私有的或是包级私有的,可以确定它的equals方法永远不会被调用。
  • 在覆盖equals方法时,必须遵守:自反性(reflexive)、对称性(symmetric)、传递性(transitive)、一致性(consistent)和对于任何非null的引用,equals(null)必须返回false

里氏替换原则(Liskov substitution principle)认为,一个类型的任何重要属性也将适用它的子类型,因此为该类型编写的任何方法,在它的子类型上也应该同样运行的很好。

  • 覆盖equals时总要覆盖hashCode。
  • 不要企图让equals方法过于智能。
  • 不要将equals声明中的Object对象替换为其他的类型。

9.覆盖equals总要覆盖hashCode

  • 在应用程序的执行期间,只要对象的equals方法比较操作所用到的信息没有被修改,那么对这同一个对象调用多次,hashCode方法都必须始终如一地返回同一个整数。在同一个应用程序的多次执行过程中,每次执行所返回的整数可以不一致。
  • 如果两个对象根据equals(Object)方法比较是相等的,那么调用这两个对象中任意一个对象的hashCode方法都必须产生同样的整数结果。(违反第二条)
  • 如果两个对象根据equals(Object)方法比较是不相等的,那么调用这两个对象中任意一个对象的hashCode方法,则不一定产生不同的整数结果。但是程序猿应该知道,给不相等的对象产生截然不同的整数结果,有可能提高散列(hash table)的性能。

10.始终要覆盖toString()

11.谨慎覆盖clone()

  • 如果专门为了继承而设计的类,覆盖了clone方法,覆盖版本的clone方法就应该模拟Object.clone的行为:他应该声明为protected、抛出CloneNotSupportedException异常,并且该类不应该实现Cloneable接口。
  • 如果用线程安全的类实现Cloneable接口,要记得它的clone方法必须得到同步。
  • 任何实现Cloneable接口的类都应该用一个公有的方法覆盖clone,首先调用super.clone,再修正任何需要修正的域。
  • 另一种实现对象拷贝的好方法是提供一个拷贝构造器(copy constructor)或者拷贝工厂(copy factory)。

12.考虑实现Comparable接口

三、类和接口

13.使类和成员的可访问性最小化

  • 尽可能的使每个类或者成员不被外界访问。
  • 实例域绝不能使公有的。
  • 类具有共有的静态final数组域,或者返回这种域的访问方法,这种几乎总是错误的。
  • 公有类都不应该包含公有域,除了公有静态final域的特殊情形外。
  • 确保公有静态final域所引用的对象都是不可变的。

14.在公有类中使用访问方法而非公有域

  • 如果类可以在它所在的包的外部进行访问,就提供访问方法。
  • 如果类是包级私有的,或者私有的嵌套类,直接暴露它的数据域并没有本质的错误。

15.使可变性最小化

不可变类只是实例不能被修改的类。每个实例中包含的信息都必须在创建该实例的时候提供,并在对象的整个生命周期固定不变。

  • 不要提供任何会修改对象状态的方法。
  • 保证类不被扩展。
  • 使所有的域都是final的。
  • 使所有的域都成为私有的。
  • 确保对于任何可变组件的互斥访问。

16.复合优先于继承

  • 与方法调用不同的是,继承打破了封装性。

17.要么为继承而设计,并提供文档说明,要么就禁止继承。

  • 构造器绝不能调用可被覆盖的方法。
  • 无论是clone还是readObject,都不可以调用可覆盖的方法,不管是以直接还是间接的方式。

18.接口优于抽象类

  • 现有的类可以很容易被更新,以实现新的接口。
  • 接口是定义mixin(混合类型)的理想选择。
  • 接口允许构造非层次结构的类型框架。
  • 接口使得安全的增强类的功能成为可能。
  • 通过对你导出的每个重要接口都提供一个抽象的骨架实现类,把接口和抽象类的优点结合起来。

19.接口只用于定义类型

  • 常量接口模式是对接口的不良使用。

20.类层级优于标签类

21.用函数对象表示策略

策略模式

22.优先考虑静态成员类

静态类成员:Map中的Entry
非静态类成员:Iterator
匿名类:无法实例化,无法声明实现接口,扩展类,无法调用任何成员除了从它的超类继承的,必须简洁,常用来作为函数对象,即函数表达式;另一种是创建过程对象(Runable)
局部类:声明局部变量的地方都可以声明局部类。

如果声明成员类不要求访问外围实例,就要始终把static修饰符放在它的声明中。

四、泛型

23.请不要在新代码中使用原生态类型

  • 如果使用原生态类型,就失掉了泛型在安全性和表述性方面的所有优势。
  • 泛型有子类化的规则,虽然可以将List传递给List的参数,但是不能将它传给类型List的参数。
  • 如果使用像List这样的原生态类型,就会失掉类型安全性,但是如果使用像List这样的参数化类型,则不会。
  • 在类文字中必须使用原生态类型。
  • 在参数化类型而非无限制通配符类型上使用instanceof操作法是非法的。

24.消除非受检警告

  • 尽可能消除每一个非受检警告
  • 如果无法消除警告,同时可以证明引起警告的代码是类型安全的,只有这种情况下可以用@SuppressWarnings(“unchecked”)注解来禁止这条警告。
  • 应该在尽可能小的范围内使用SuppressWarnings注解。
  • 每当使用SuppressWarnings注解,都要增加注释,说明为什么这么做是安全的。

25.列表优于数组

JDK1.5的泛型有一个很重要的设计原则:如果一段代码在编译时系统没有产生:“[unchecked]未经检查的转换“警告,则程序在运行时不会引发”ClassCastException“异常。

数组是协变的(convariant): 如果Sub为Super的子类型,那么数组类型Sub[]就是Super[]的子类型。
数组是具体化的(reified): 数组在运行时才知道并检查他们的元素类型约束。

泛型时不可变的(invariant): 对于任意两个不同的类型Type1和Type2,List<Type1>既不是List<Type2>的子类型,也不是List<Type2>的超类型。

泛型只在编译时强化它们的类型信息,并在运行时丢弃(或者擦除)它们的元素类型信息。擦除就是使泛型可以与没有使用泛型的代码随意进行互用。

从技术角度来说,像EList<E>List<String>这样的类型应称作不可具体化(non-reifiable)的类型。不可具体化类型是指其运行时表示法包含的信息比它编译时表示法包含的信息更少的类型。唯一可具体化的(reifiable)参数化类型时无限制通配符类型,如List<?>和Map<?,?>。创建无限制通配类型的数组是合法的;不可具体化的类型的数组转换只能在特殊情况下使用。

26.优先考虑泛型

  • 不能创建不可具体化的(non-reifiable)类型的数组。解决方案是新建Object数组强制转换为不可具体化类型,确保未受检的转换是安全的,就要尽可能小的范围中禁止警告。

27.优先考虑泛型方法

28.利用有限制通配符来提升API的灵活性

  • 为了获得最大限度的灵活性,要在表示生产者或者消费者的输入参数上使用通配符类型。
  • 如果类型参数只在方法声明中出现一次,就可以用通配符取代它,如果是无限制的类型参数,就用无限制的通配符取代它。
  • 如果类的用户必须考虑通配符类型,类的API或许就会出错。
  • 不要用通配符类型作为返回类型,除了为用户提供额外的灵活性之外,它还会强制用户在客户端代码中使用通配符类型。

29.优先考虑类型安全的异构容器

1
2
3
4
public class Favorites{
public <T> void putFavorite(Class<T> type, T instance);
public <T> T getFavorite(Class<T> type);
}

不像普通的map,它的所有键都是不同类型的,因此Favorites称作类型安全的异构容器(typesafe heterogeneous container)。

集合API说明了泛型的一般用法,限制你每个容器只能有固定数目的类型参数,但是可以通过将类型参数放在键上而不是容器上来避开这一限制。

五、枚举和注解

30.用enum代替int常量

只有极少数的枚举受益于将多种行为与单个方法关联。在这种相对少见的情况下,特定于常量的方法要优先于启用自有值的枚举。

如果多个枚举常量同时共享相同的行为,则考虑策略枚举(strategy enum)。

31.用实例代替序数索引

永远不要根据枚举的序数导出与它关联的值,而是要将它保存在一个实例域中:

1
2
3
4
5
6
public enum Ensemble{
SOLO(1),DUET(2);
private final int numberOfMusicians;
Ensemble(int size){this.numberOfMusicians = size;}
public int numberOfMusicians(){return numberOfMusicians;}
}

32.用EnumSet代替位域

33.用EnumMap代替序数索引

34.用接口模拟可伸缩的枚举

35.注解优先于命名模式

36.坚持使用Override注解

37.用标记接口定义类型

六、方法

38.检查参数的有效性

每当编写方法或者构造器时,应该考虑它的参数有哪些限制。应该把这些限制写到文档中,并且在这个方法体的开头处,通过显示的检查来实施这些限制。
非公有的方法通常应该使用断言(assertion)来检查它们的参数:

1
2
3
4
5
6
private static void sort(long a[], int offset, int length){
assert a != null;
assert offset >= 0 && offset <= a.length;
assert length >= 0 && length <= a.length - offset;
...
}

39.必要时进行保护性拷贝

对于构造器的每个可变参数进行保护性拷贝(defensive copy)是必要的。
保护性拷贝是在检查参数的有效性之前进行的,并且有效性检查是针对拷贝之后的对象,而不是针对原始的对象。

1
2
3
4
5
6
public Period(Date start, Date end){
if(start.compareTo(end) > 0)
throw new IllegalArgumentException(start + " after " + end);
this.start = start;
this.end = end;
}

上面代码中虽然增加约束条件,但是Date在此对象外部还是可以被修改的,因为Date是引用传递,所以为避免这种问题,使用备份对象,而不是使用原始对象。

1
2
3
4
5
6
7
public Period(Date start, Date end){
this.start = new Date(start.getTime);
this.end = new Date(end.getTime);
if(start.compareTo(end) > 0)
throw new IllegalArgumentException(start + " after " + end);

}

对于参数类型可以被不可信任方子类化的参数,请不要使用clone方法进行保护性拷贝。
如果类中提供了对其可变内部成员的访问能力,则使它返回可变内部域的保护性拷贝即可。

例如:

1
2
3
4
Date start = new Date();
Date end = new Date();
Period p = new Period(start, end);
p.end().setYear(78);//end()方法返回Date对象,Period中提供setYear改变Date属性。

为了保证传入Period中的start或者end不被改变,只需要修改Period中获取start和end的访问方法即可。

1
2
3
4
5
6
7
8
public Date start(){
return new Date(start.getTime());
}

public Date end(){

return new Date(end.getTime());
}

参数的保护性拷贝不仅仅针对不可变类。如果客户端提供的对象是可变的且该对象不允许在你的类对象中变化,就必须对该对象进行保护性拷贝。

如果拷贝成本受到限制,并且信任它的客户端不会不恰当的修改组件,就可以在文档中声明客户端的职责是不得修改受到影响的组件,以此来代替保护性拷贝。

40.谨慎设计方法签名

谨慎的选择方法的名称;
不要过于追求提供便利的方法。只有当一项操作被经常用到的时候,才考虑为它提供快捷方式(shorthand)。如果不确定还是不提供快捷为好;
避免过长的参数列表;

41.慎用重载

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
public class CollectionClassifier{

public static String classify(Set<?> s){
return "set";
}
public static String classify(List<?> s){
return "list";
}
public static String classify(Collection<?> s){
return "unknow";
}

public static void main(String[] args){
Collection<?>[] collections = {
new HashSet<String>(),
new ArrayList<BigInteger>(),
new HashMap<String, String>().value()
};
for(Collection<?> c : collections)
System.out.println(classify(c));
}
}

上述代码输出结果是打印“unknow”三次。

42.慎用可变参数

定义参数数目不定的方法时,可变参数方法是一种很方便的方式,但是它们不应该被过度滥用,使用不当,将产生混乱的结果。

43.返回零长度的数组或者集合,而不是null

44.为所有导出的API元素编写文档注释

七、通用程序设计

45.将局部变量的作用域最小化

要使用局部变量的作用域最小化,最有力的方法就是在第一次使用它的地方声明。
几乎每个局部变量的声明都应该包含一个初始化表达式。

46.for-each循环优先于传统的for循环

虽然for-each循环在简洁性和预防bug方面比传统的for循环有优势,且没有性能损失。但是有三种常见情况无法使用for-each:

  • 过滤: 如果需要遍历集合,并且删除选定的元素,就需要使用显式的迭代器。
  • 转换: 如果需要遍历列表或者数组,并取代它部分或者全部的元素值,就需要列表迭代器或者数组索引。
  • 平行迭代: 如果需要并行的遍历多个集合,就需要显式的控制迭代器或者索引变量。

47.了解和使用类库

每个程序员都应该熟悉java.lang、java.util、java.io中的内容。

48.如果需要精确的答案,请避免使用float和double

使用BigDecimal、int或者long进行计算。如果数值防伪不超过9位十进制数字,就可以使用int;如果不超过18位就可以使用long;如果数值超过18位就必须使用BigDecimal。

BigDecimal缺点:不方便,慢。

49.基本类型优先于装箱基本类型

基本类型与装箱基本类型之间主要区别:

  • 基本类型只有值,而装箱基本类型则具有与它们的值不同的同一性。
  • 基本类型只有功能完备的值,而每个装箱基本类型除了它对应基本类型的所有功能值之外,还有非功能值:null。
  • 基本类型通常比装箱基本类型更节省空间和时间。

第一个是作为集合中的元素、键和值。你不能将基本类型放在集合中,因此必须使用装箱基本类型(在代码中不用自己去装箱,如果把基本类型放入集合中,集合会自动把基本类型装箱)。不能放在集合中的原因是:基本类型存放在栈上,集合中的引用则存在堆或方法取上。

在参数化类型中,必须使用装箱基本类型作为参数,Java不允许使用基本类型,因为Java泛型要求使用的是对象类型,基本类型不是对象类型。

50.如果其他类型更适合,则尽量避免使用字符串

字符串不适合代替其他的值类型。
字符串不适合代替枚举类型。
字符串不适合代替聚集类型。
字符串也不适合代替能力表。

51.当心字符串连接的性能

52.通过接口引用对象

如果有适合的接口类型存在,那么对于参数、返回值、变量和域来说,就都应该使用接口类型进行声明。

如果没有合适的接口存在,完全可以用类而不是接口来引用对象:

  • 如果具体类没有相关联的接口,不管它是否表示一个值,你都没有别的选择,只有通过它的类来引用它的对象。
  • 对象属于一个框架,而框架的基本类型是类,不是接口。如果对象属于这种基于类的框架(class-based
  • framework),就应该用相关的积累(base class)来引用这个对象,而不是用它的实现类。

53.接口优先于反射机制

反射机制:

  • 丧失了编译时类型检查的好处,包括异常检查。如果程序企图用反射方式调用不存在的活不可访问的方法,在运行时它将会失败,除非采用了特别的预防措施。
  • 执行反射访问所需的代码非常笨拙和冗长。编写这样的代码非常乏味,阅读起来也很困难。
  • 性能损失。反射方法调用比普通方法调用慢了许多。

通常普通应用程序在运行时不应该以反射方式访问对象。
对于有些程序,它们必须用到在编译时无法获取的类,但是在编译时存在适当的接口或者超类,通过它们可以引用这个类。如果是这种情况,就可以以反射方式创建实例,然后通过它们的接口或者超类,以正常的方式访问这些实例。如果适当的构造器不带参数,甚至根本不需要使用java.lang.reflectClass.newInstance方法就已经提供了所需的功能。

54.谨慎的使用本地方法

55.谨慎的优化

很多计算上的过失都被归咎于效率(没有必要达到的效率),而不是任何其他的原因—甚至包括盲目的做傻事。
不要取计较效率上的一些小小的得失,在97%的情况下,不成熟的优化才是一切问题的根源。
在优化方面,我们应该遵守两条规则:
规则1: 不要进行优化。
规则2: 还是不要进行优化—也就是说,在你还没有绝对清晰的未优化方案之前,请不要优化。

不要因为性能而牺牲合理的结构。要努力编写好的程序而不是快的程序。好的程序体现了信息隐藏的原则:只要有可能,它们就会把设计决策几种在单个模块中,因此,可以改变单个决策,而不会影响到系统的其他部分。

在设计的过程中考虑性能问题。努力避免限制性能的设计决策。

56.遵守普遍接受的命名惯例

八、异常

57.只针对异常的情况才使用异常

异常机制的设计初衷是用于不正常的情形,所以很少会有JVM实现试图对它们进行优化,使得与显式的测试一样快速。

把代码放在try-catch块中反而阻止了现代JVM实现本来可能要执行的某些特定的优化。

对数组进行遍历的标准模式并不会导致冗余的检查。有些现代的JVM实现会将它们优化掉。

58.对可恢复的情况使用受检异常,对编程错误使用运行时异常

59.避免不必要的使用受检异常

60.优先使用标准异常

61.抛出域抽象相对应的异常

更高层次的实现应该捕获低层的异常,同时抛出可以按照高层抽象进行解释的异常。
如果不能阻止或者处理来自低层的异常,一般做法是使用异常转译,除非低层方法碰巧可以保证它抛出的所有异常对高层也合适才可以将异常从低层传播到高层。异常链对高层和低层异常都提供了最佳功能:它允许抛出适当的高层异常,同时又能捕获低层的原因进行失败分析。

62.每个方法抛出的异常都要有文档

始终要单独的声明受检异常,并且利用Javadoc的@throws标记,准确的记录下抛出的每个异常的条件。

使用Javadoc的@throws标签记录下一个方法可能抛出的每个受检异常,但是不要使用throws关键字将未受检异常包含在方法的声明中。

如果一个类中的许多方法处于同样的原因而抛出同一个异常,则该类的文档注释中对这个异常建立文档,是可以接受的。

63.在细节消息中包含能捕获失败的信息

为了捕获失败,异常的细节信息应该包含所有“对该异常有贡献”的参数和域的值。

64.努力使失败保持原子性

一般而言,失败的方法调用应该使对象保持在被调用之前的状态。

65.不要忽略异常

九、并发

66.同步访问共享的可变数据

Java语言规范保证读或写一个变量是原子的,除非这个变量的类型为long或者double[JLS,17.4.7]。对于这句话不要误解,虽然语言规范保证了线程在读取原子数据的时候,不会看到任意的数值,但是它并不保证一个线程写入的值对于另一个线程将是可见的。为了在线程之间进行可靠的通信,也为了互斥访问,同步是必要的。

67.避免过度同步

为了避免死锁和数据破坏,千万不要从同步区域内部调用外来方法,要尽量限制同步区域内部的工作量。

68.executor和task优先于线程

69.并发工具优先于wait和notify

java.util.concurrent中更高级的的工具分成三类:Execcutor Framework、并发集合(Concurrent Collection)以及同步器(Synchronizer)。

同步器是一些使线程能够等待另一个线程的对象,允许它们协调动作。常用的是CountDownLatch和Semaphore。不常用的是CyclicBarrier和Exchanger。

对于间歇式的定时,始终应该优先使用System.nanoTime,而不是使用System.currentTimeMills。System.nanoTime更加准确也更加精确,它不受系统的实时时钟的调整所影响。

如果你在维护使用wait和notify的代码,务必确保始终是利用标准的模式从while循环内部调用wait。一般情况下,你应该优先使用notifyAll,而不是使用notify。如果使用notify,请一定小心,以确保程序的活性。

70.线程安全性的文档化

一个类为了可被多个线程安全的使用,必须在文档中清楚的说明它所支持的线程安全级别。

  • 不可变的(immutable)这个类是不可变的。所以不需要外部同步。例如:String、Long、BigInteger。
  • 无条件的线程安全(unconditionally thread-safe)这个类的实例是可变的,但是这个类有着足够的内部同步,所以,它的实例可以被并发使用,无需任何外部同步。例如:Random、ConcurrentHashMap。
  • 有条件的线程安全(conditionally thread-safe)除了有些方法为进行安全的并发使用而需要外部同步之外,这种线程安全级别与无条件的线程安全相同。例如:Collections.synchronized包装返回的集合,它们的迭代器(iterator)要求外部同步。
  • 非线程安全(not thread-safe)这个类的实例是可变的。为了并发地使用它们,客户端必须利用自己选择的外部同步包围每个方法调用。例如:ArrayList、HashMap。
  • 线程对立(thread-hostile)这个类不能安全的被多个线程并发使用,即使所有的方法调用都被外部同步包围。

71.慎用延迟初始化

延迟初始化(lazy initialization)是延迟到需要域的值时才将它初始化的这种行为。
大多数的域应该正常地进行初始化,而不是延迟初始化。如果为了达到性能目的,或者为了破坏有害的初始化循环,必须延迟初始化一个域则:
对于实例域,就使用双重检查模式(double-check idiom);

1
2
3
4
5
6
7
8
9
10
11
12
13
private volatile FieldType field;
FieldType getField(){
FieldType result = field;
if(result == null){
synchronized(this){
result = field;
if(result == null){
field = result = computeFieldValue();
}
}
}
return result;
}

对于静态域,则使用lazy initialization holder class idiom;

1
2
3
4
5
private static class FieldHolder{
static final FieldType field = computeFieldValue();

}
static FieldType getField(){return FieldHolder.field;}

对于可以接受重复初始化的实例域,也可以考虑使用单重检查模式(single-check idiom)。

1
2
3
4
5
6
7
8
9
10
private volatile FieldType field;

private FieldType getField(){

FieldType result = field;
if(result == null){
field = result = computeFieldValue();
}
return result;
}

72.不要依赖于线程调度器

73.避免使用线程组

线程组的初衷是作为一种隔离applet的机制,当然是出于安全的考虑。线程组并没有提供太多有用的功能,而且它们提供的许多功能还都是有缺陷的。

十、序列化

74.谨慎的实现Serializable接口

实现Serializable接口而付出的最大代价是,一旦一个类被发布,就大大降低了“改变这个类的实现”的灵活性。

如果接受了默认的序列化形式,并且以后要改变这个类的内部表示法,结果可能导致序列化形式的不兼容。

第二个代价是,它增加了出现Bug和安全漏洞的可能性。

实现Serializable第三个代价是,随着类发行新的版本,相关的测试负担也增加了。

为了继承而设计的类应该尽可能少的趋势线Serializable接口,用户的接口也应该尽可能少的继承Serializable接口。如果违反了这条规则,扩展这个类或者实现该接口的程序员就会背上沉重的负担。然而有些情况下,这条规则确实适合的。例如:如果一个类或者接口存在的目的主要是为了参加到某个框架中,该框架要求所有的参与者都必须实现Serializable接口,那么对于类或者接口来说实现扩展Serializable是有意义的。

如果一个专门为了继承而设计的类不是可序列化的,就不可能编写出可序列化的子类。特别是,如果超类没有提供可访问的无参构造器,子类也不可能做到序列化。对于未继承而设计的不可序列化的类,你应该提供一个无参构造器。

75.考虑使用自定义的序列化形式

如果一个对象的物理表示法等同于它的逻辑内容,可能就适合于使用默认的序列化形式。例如:

1
2
3
4
5
6
7
8
9
10
public class Name implements Serializable{
private final String lastName;

private final String firstName;

private final String middleName;

...

}

即使你确定了默认的序列化形式是合适的,通常还必须提供一个readObject方法保证约束关系和安全性。

当一个对象的物理表示法与它的逻辑数据内容有实质性的区别时,使用默认序列化形式会有以下4个缺点:

  • 它使这个类的导出API永远地束缚在该类的内部表示法上。
  • 它会消耗过多的时间。
  • 它会消耗过多空间。
  • 它会引起栈溢出。

如果所有的实例域都是瞬时的(transient),从技术角度而言,不调用DefaultWriteObject和defaultReadObject也是允许的,但是不推荐这样做。
在决定将一个域做成非transient的之前,请一定要确信它的值将是该对象逻辑状态的一部分。
如果在读取整个对象状态的任何其他方法上强制任何同步,则也必须在对象序列化上强制这种同步。
不管你选择了哪种序列化形式,都要为自己编写的每个可序列化的类声明一个显式的序列版本UID(serial version UID)。

76.保护性的编写readObject方法

记得回来看反序列化代码

当一个对象被反序列化的时候,对于客户端不应该拥有的对象引用,如果哪个域包含了这样的对象引用,就必须要做保护性拷贝,这是非常重要的。保护性拷贝在有效性检查之前进行。

不要使用writeUnshared和readUnshared方法。

对于非final的可序列化类,readObject方法不可以调用可被覆盖的方法,无论是直接调用还是间接调用都不可以。如果违反了规则,并覆盖了该方法,被覆盖的方法将在子类的状态被序列化之前先运行,程序很可能失败。

readObject方法指导:

  • 对于对象引用域必须保持为私有的类,要保护性的拷贝这些域中的每个对象。不可变类的可变组件就属于这一类别。
  • 对于任何约束条件,如果检查失败,则抛出一个InvalidObjectException异常。这些检查动作应该跟在所有的保护性拷贝之后。
  • 如果整个对象图在被反序列化之后必须进行验证,就应该使用ObjectInputValidation接口。
  • 无论是直接方式还是间接方式,都不要调用类中任何可被覆盖的方法。

77.对于实例控制,枚举类型优先于readResolve

如果这个类的声明加上了“implements Serializable”的字样,它就不再是一个单例类。无论使用默认的序列化形式,还是自定义的序列化形式,都会返回一个新建的实例,这个新建实例不用于该类初始化时创建的实例。

readResolve特性允许你用readObject创建的实例代替另一个实例。对于一个正在被序列化的对象,如果它的类定义了一个readResolve方法,并且具备正确的声明,那么在反序列化后,新建对象上的readResolve方法就会被调用,该方法返回的对象引用将被返回,取代新建对象,指向新建对象的引用不需要再被保留,因此立即成为垃圾回收对象。

如果readResolve方法忽略被反序列化的对象,只返回该类初始化时创建的实例。如果依赖readResolve进行实例控制,带有对象引用类型的所有实例域都必须声明为transient的。

readResolve的可访问性很重要。如果把readResolve方法放在一个final类上,它就应该是私有的。如果吧readResolve方法放在一个非final类上,就必须考虑它的可访问性。

尽可能的使用枚举类型来实施实例控制的约束条件。如果做不到,同时又需要一个既可序列化又是实例受控(instance-controlled)的类,就必须提供一个readResolve方法,并确保该类的所有实例域都为基本类型,或者是transient的。

78.考虑用序列化代理代替序列化实例

每当你发现自己必须在一个不能被客户端扩展的类上编写readObject或者writeObject方法的时候,就应该考虑使用序列化代理模式。要想文件的带有重要约束条件的对象序列化,这种模式可能是最容易的。