oynix

于无声处听惊雷,于无色处见繁花

「Effective Java」读书整理


书地址 :链接: https://pan.baidu.com/s/1kUAwYgv 密码: ij4j

- Chapter 3 适用于所有对象

8. 重写equals方法

三个原则:对称性、传递性、一致性

9. 重写equals方法必定要重写hashCode方法

例如在HashMap中存储时会调用该方法

10. 始终要重写toString方法

便于阅读,使类用起来更加舒适

11. 谨慎的覆盖clone方法

相当于另一个构造器

12. 考虑实现Comparable接口

用于对象比较、排序(在集合里sort)


- Chapter 4 类和接口

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

1
2
3
4
5
6
7
// 错误方式,安全漏洞; 
// 当域为基本类型或不可变对象时安全;
// 当为可变对象的引用时存在安全漏洞, VALUE虽不可修改但数组里的对象可被修改
public static final Thing[] VALUE = {....};
// 正确方式
private static final Thing[] PRIVATE_VALUES = {...};
public static final List<Thing> VALUES = Collections.unodifiableList(Arrays.asList(PRIVATE_VALUES));

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

总有类永远不应该暴露可变域

15. 使可变性最小化

成为不可变类的5条规则 :

  1. 不要提供任何会修改对象状态的方法;
  2. 保证类不会被扩展(fina);
  3. 使所有域都是final的;
  4. 使所有域都成为私有的;
  5. 确保对于任何可变组件的互斥访问.

16. 复合优先于继承

当B和A的关系为”is-a”时,让B继承自A;否则B中应包含一个A的实例(复合),而不是扩展A(继承)。

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

1>. 关于程序文档有句格言:好的API文档应该描述一个给定的方法做了什么工作,而不是描述它如何做到的。

为了允许继承,类还必须遵守其他一些约束:

  • 构造器决不能调用可被覆盖的方法;
  • 无论是clone(Cloneable接口)还是readObject(Serializable接口),都不可调用可覆盖的方法,不管是直接还是间接的方式。

18. 接口优先于抽象类

抽象类的演变比接口容易;
骨架实现,即接口的简单实现

19. 接口只用于定义类型

避免常量接口;
接口应该只被用来定义类型

20. 类层次优先于标签类

标签类过于冗长、容易出错,并且效率底下

21. 用函数对象表示策略

比较器:String.CASE_INSENSITIVE_ORDER

22. 优先考虑静态成员类

嵌套类种类

  1. 静态成员类;
  2. 非静态成员类;
  3. 匿名类;
  4. 局部类。

后三种都被称为内部类。

如果声明成员类不要求访问外围实例,就要始终把static修饰符放在它的声明中,使它成为静态成员类,而不是非静态成员类。如果省略了static修饰符,则每个实例都将包含一个额外的指向外围对象的引用。例如ViewHolder。


- Chapter 5 泛型

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

  • Set : 原生态类型, 脱离了泛型系统;
  • Set<?> : 无限通配符类型,只能包含某种未知对象类型;
  • Set<Object>: 参数化类型,可以包含任何对象类型。
1
2
3
if (o instanceof Set) {
Set<?> m = (Set<?>) o;
}

原生态类型只是为了与引入泛型之前的遗留代码进行兼容和互用而提供的。

术语 示例
参数化类型 List<String>
实际类型参数 String
泛型 List<E>
形式类型参数 E
无限制通配符类型 List<?>
有限制类型参数 <E extends Number>
递归类型限制 <T extends Comparable<T>>
有限制通配符类型 List<? extends Number>
泛型方法 static <E>List<E> asList(E[] a)
泛型令牌 String.class

24. 消除非受检警告

@SuppressWarnings(“unchecked”)要放在一个声明上,要将禁止非受检警告范围缩到最小;每次使用时都要添加一个注释,说明为什么这么做是安全的。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// 例如ArrayList中的toArray方法, 注解不加在方法上而是单独声明一个局部变量
// 为的就是缩小非受检警告范围, 这么做是值得的.
public <T> T[] toArray(T[] a) {
if (a.length < size) {
// This cast is correct because the array we're creating
// is of the same type as the one passed in, which is T[].
@SuppressWarnings("unchecked")
T[] result = (T[]) Arrays.copyOf(elements, size, a.getClass());
return result;
}
System.arrayCopy(elements, 0, a, 0, size);
if (a.length > size)
a[size] = null;
return a;
}

25. 列表优先于数组

  • 禁止创建泛型数组,优先使用集合;
  • 数组是协变且可以具体化的,泛型是不可变的且可以被擦除的。
  • 混合使用时如何得到编译时错误或者警告时,用列表代替数组。

26. 优先考虑泛型

使用泛型比使用需要在客户端代码中进行转换的类型来的更加安全,也更加容易。只要时间允许,就把现有的类型都泛型化。

27. 优先考虑泛型方法

1
2
3
4
5
6
7
8
9
10
11
// 递归泛型 类型参数
public static <T extends Comparable<T>> T max(List<T> list) {
Iterator<T> i = list.iterator();
T result = i.next();
while (i.hasNext()) {
T t = i.next();
if (t.compare(result) > 0)
result = t;
}
return result;
}

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

为了获得最大限度的灵活性,要在表示生产者或者消费者的输入参数上使用通配符类型。
PECS表示producer-extends,consumer-super
换句话说, 如果参数化类型表示一个T生产者,就使用<? extends T>;如果表示一个T消费者,就使用<? super T>

1
2
3
4
5
6
7
8
9
// 用Stack示例
public void pushAll(Iterable<? extends E> src) {
for (E e : src)
push(e);
}
public void popAll(Collection<? super E> dst) {
while (!isEmpty())
dst.add(pop());
}

修改过的使用通配符类型的声明:PECS规则,list生产T实例,T的comparable消费T实例并产生表示顺序关系的整值。comparable始终是消费者,因此使用时始终应该是Comparable<? super T>优先于Comparable<T>。对于comparator也一样,因此使用时始终应该是Comparator<? super T> 优先于Comparator<T>

1
2
3
4
5
6
7
8
9
10
11
public static <T extends Comparable<? super T>> T max(List<? extends T> list)  {
// 这里做了修改
Iterator<? extends T> i = list.iterator();
T result = i.next();
while (i.hasNext()) {
T t = i.next();
if (t.compare(result) > 0)
result = t;
}
return result;
}

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

1
2
3
4
5
6
7
8
9
10
11
12
13
public class Favorites {
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));
}
}

确保永远不违背它的类型约束条件:

1
Collections.checkedXXX();

利用Class.asSubclass方法进行转换:

1
2
3
public <T extends Annotation> T getAnnotation(Class<T> annotationType);
Class<?> typeOne = Class.forName(typeOneInstance);
getAnnotation(typeOne.asSubclass(Annotation.class));

-Chapter 6 枚举和注解

30. 用enum代替int常量

枚举类型有一个自动产生valueOf(String)方法,它将常量的名字转变成常量本身;
枚举中的switch语句适合于给外部的枚举类型增加特定于常量的行为。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public enum Operation {
PLUS("+") {double apply(double x, double y) {return x + y;} },
MIMUS("-") {double apply(double x, double y) {return x - y} },
TIMES("*") {double apply(double x, double y) {return x * y} },
DIVEDES("/") {double apply(double x, double y) {return x / y} };

private String symbol;
public Operation(String sym) {
this.symbol = sym;
}
@Override
public void toString() {
return symbol;
}
abstract double apply(double x, double y);
}

31. 用实例域代替序数

所有的枚举都有一个ordinal方法, 它返回每个枚举常量在类型中的数字位置。永远不要根据枚举的序数导出与它关联的值,而是要将它保存在一个实例中

1
2
3
4
5
6
7
8
9
10
11
public enum Ensemble {
SOLO(1), DUET(2), TRIO(3);

private int numberOfMusicians;
public Ensemble(int size) {
this.numberOfMusicians = size;
}
public int numberOfMusician() {
return numberOfMusicians;
}
}

32. 用EnumSet代替位域

正是因为枚举类型要用在集合Set中, 所有没有理由用位域来表示它.EnumSet具有简洁和性能的优势.

1
2
3
4
5
6
7
8
9
10
11
public class Text {
public enum Style {BOLD, ITALIC, UNDERLINE, STRIKETHROUGH}

// 所有的Set都可传入, 但是EnumSet最好
// 考虑到可能还有其他实现,所以使用Set<Style>而不是EnumSet<Style>
public void applyStyles(Set<Style> styles){...}
}

// 下面是将EnumSet实例传递给applyStyles方法的客户端代码。EnumSet提供了丰富的
// 静态工厂来轻松创建集合, 其中一个如下
text.applyStyles(EnumSet.of(Style.BOLD, Style.ITALIC));

33. 用EnumMap代替序数索引

1
2
3
4
5
6
7
Map<Herb.Type, Set<Herb>> herbsByType = new EnumMap<Herb.Type, Set<Herb>>(Herb.Type.class);
for (Herb.Type t : Herb.Type.values()) {
herbsByType.put(t, new HashSet<Herb>);
}
for (Herb b : garden) {
herbsByType.get(b.type).add(b);
}

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

避免扩展枚举类型(继承), 采用用枚举类型实现接口(实现)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
// 定义一个接口
public interface Operation{...}
// ExtendOperation实现了这个接口
public ExtendOperation implements Operation{...}
public static void main(String[] args) {
double x = 3.3;
double y = 3.4;
// 方法一
test(ExtendOperation.class, x, y);
// 方法二
test(Arrays.asList(ExtendOperation.values()), x, y);
}
// 方法一 : 确保Class对象既表示枚举又表示Operation的子类型
private static <T extends Enum<T> & Operation> void test(
Class<T> opSet, double x, double y) {
for (Operation op : opSet.getEnumConstants()) {
// do sth
}
}
// 方法二
private static void test(Collection<? extends Operation> opSet, double x, double y) {
for (Operation op : opSet) {
// do sth
}
}

35. 注解优先于命名模式

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
// 注解类, 只用在无参数的静态方法上
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface Test {}

// 测试
Class testClass = Class.forName(agrs[0]);
for (Method m : testClass.getDeclaredMethods()) {
// 判断某个方法是否被Test注解标注
if (m.isAnnotationPresent(Test.class)) {
try {
// 可直接执行说明是静态方法; 传入null说明无参数
m.invoke(null);
} catch(InvocationTargetException ite) {
// 1. 实例方法
// 2. 一个或多个参数
// 3. 不可访问的方法
}
}
}

// 只有抛出异常才算成功的注解类
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface ExceptionTest {
Class<? extends Exception> value();
}
// 待测试的方法
@ExceptionTest(ArithmeticException.class)
public static void method1() {
int i = 0;
i = i / i;
}
@ExceptionTest(ArithmeticException.class)
public static void method2() {
int[] arr = new int[1];
int i = arr[3];
}
@ExceptionTest(ArithmeticException.class)
public static void method3() {
// do nothing
}

// 测试工具类
if (m.isAnnotationPresent(ExceptionTest.class)) {
try {
m.invoke(null);
} catch (InvocationTargetExcetpion ite) {
// 出现的异常类型
Throwable exception = ite.getCause();
// 期待的异常类型
Class<? extends Exception> ex = m.getAnnotation(ExceptionTest.class).value();
// 出现的异常与期待的异常时同一种
if (ex.instanceOf(exception)) {...}
}
}

// 多种类型异常注解类
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementTarget.METHOD)
public @interface ExceptionsTest {
Class<? extends Exception>[] value();
}
// 待测试方法注解
@ExceptionsTest({IndexOutOfBoundException.class, ArithmeticException.class})
public static void method4() {
}

36. 坚持使用Override注解

IDE可检查

37. 用标记接口定义类型

标记接口,类似于Serializable接口,没有方法,只是一个空接口作为标记,被标记过的实例可以通过ObjectOutputStream处理。
两者比较:标记接口和标记注解

  • 标记接口定义的类型是由被标记类的实例实现的;标记注解则是没有定义这样的类型。这个类型允许你在编译时捕捉在使用标记注解的情况下要到运行时才能捕捉到的错误;
  • 标记接口的另一个优点,可以被跟家精确的锁定;
  • 标记注解胜过标记接口的最大优点在于,它可以通过默认的方式添加一个或者多个注解类型元素,给一倍使用过的注解类型添加更多的信息。随着时间的推移,简单类型的标记注解可以演变成丰富的标记注解, 标记接口则不能。
  • 标记注解的另一个优点在于,它们是更大的注解机制的一部分。因此,标记注解在那些支持注解作为编程元素之一的框架中同样具有一致性。

区分使用

  • 应用到任何程序元素(方法,字段等)而不是类或者接口,必须用标记注解;
  • 标记类和接口, 优先使用标记接口;
  • 标记只用于特殊接口的元素,将标记定义为该接口的一个子接口;
  • 如果以后需要扩展,用标记注解;
  • 当目标是ElementType.TYPE时,多考虑标记接口。

-Chapter 7 方法

38. 检查参数的有效性

  • 在方法体的开头检查参数;
  • 使用断言assert,失败时抛出AssertionError;
  • 检查构造器的参数尤为重要

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

  • 保护性拷贝是在检查参数有效性之前进行的,并且有效性检查是针对拷贝之后的对象;
  • 对于参数类型可以被不可信任方子类化的参数,请不要使用clone进行保护性拷贝

40. 谨慎设计方法签名

  1. 谨慎选择方法名称。
  2. 不要过于追求提供便利的方法。
  3. 避免过长的参数列表(小于等于4)。
  4. 对于参数类型,优先使用接口而不是类。
  5. 对于boolean参数, 优先使用两个元素的枚举类型。

41. 慎用重载

  • 对于重载方法(overloaded method)的选择是静态的,而对于被覆盖的方法(overridden method)的选择是动态的。
  • 避免胡乱使用重载机制的安全而保守的策略是,永远不要导出两个具有相同参数数目的重载方法。如果方法是可变参数,保守策略是根本不要重载它。

42. 慎用可变参数

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
private final List<Cheese> cheeseInStock = ....;
private final static Cheese[] EMPTY_CHEESE_ARRAY = new Cheese[0];

public Cheese[] getCheese() {
return cheeseInStock.toArray(EMPTY_CHEESE_ARRAY);
}

// 集合值的方法
public List<Cheese> getCheeseList() {
if (cheeseOfStock .isEmpty()) {
return Collections.emptyList();
} else {
return new ArrayList<Cheese>(cheeseOfStock);
}
}

44. 为所有到处的API元素编写文档注释

-Chapter 8 通用程序设计

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

  • 要是局部变量的作用域最小化,最有力的方法就是在第一次使用它的地方声明。
  • 几乎每个局部变量的声明都应该包含一个初始化表达式,如果没有则应推迟声明。try-catch例外。
  • 如果循环终止之后不再需要循环变量的内容,for循环优于while循环。
1
2
3
4
// n的作用是:避免每次循环产生额外计算的开销
for (int i = 0 , n = getSize(); i < n; i++) {
doSomething(i);
}

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

三种情况无法使用for-each

  1. 过滤:如果需要遍历集合,并删除选定的元素,就需要使用显示的迭代器,以便可以调用它的remove方法。
  2. 转换:如果需要遍历列表或者数组,并取代它的部分或者全部元素值,就需要列表迭代器或者数组索引,以便设定元素的值。
  3. 平行迭代:如果需要并行的遍历多个集合,就需要显示的控制迭代器或者索引变量,以便所有迭代器或者索引变量都可以得到同步前移。

47. 了解和使用类库

  • 使用标准类库而不是专门的实现。
  • Collections Framework
  • java.util.concurrent包含高级并发工具来简化多线程的编程任务,还包含低级别的并发基本类型

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

使用int或者long或者BigDecimal替代。

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

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

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

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

使用StringBuilder

52. 通过接口引用对象

如果有合适的接口类型存在,那么对于参数、返回值、变量和域来说,就都应该使用接口类型进行声明,如List。
如果没哟合适的接口存在,完全可以用类而不是接口来引用对象,如值类String、BigInteger

53. 接口优先于反射机制

54. 谨慎地使用本地方法

使用本地方法提高性能的做法不值得提倡

55. 谨慎地进行优化

  • 努力避免那些限制性能的设计决策。
  • 为获得更好的性能而对API进行包装,这是一种非常不好的想法。

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

-Chapter 9 异常

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

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

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

60. 优先使用标准的异常

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

底层的异常被传到高层的异常,高层的异常提供访问方法(Throwable.getCause)来获得底层的异常

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

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

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

65. 不要忽略异常

-Chapter 10 并发

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

当多个线程共享可变数据的时候,每个读或者写数据的线程必须执行同步。

67. 避免过度同步

  • 为了避免活性失败和安全性失败,在一个被同步的方法或者代码块中,永远不要放弃对客户端的控制。
  • 在同步区域内做尽可能少的工作。
  • 为了避免死锁和数据损坏,千万不要从同步区域内部调用外来方法。

68. executor和task优先于线程

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

  • 除非迫不得已,否则应该优先使用ConcurrentHashMap,而不是使用Collections.synchronizedMap或Hashtable。只要用并发Map替代老式的同步Map,就可以极大地提升应用程序的性能。更一般地,应该优先使用并发集合,而不是使用外部的同步集合。
  • 对于间歇式的定时,始终应该优先使用System.nanoTime,而不是System.currentTimeMills,前者更加准确也更加精确,它不受系统的实时始终的调整影响。
  • 使用应该使用wait循环模式来调用wait方法;永远不要在循环之外调用wait方法。循环会在等待之前和之后测试条件。
1
2
3
4
5
6
7
8
9
10
11
12
13
private static final ConcurrentMap<String, String> map = ConcurrentHashMap<>();

public static String intern(String s) {
String result = map.get(s);
if (result == null) {
// 应对并发情况
result = map.putIfAbsent(s, s);
if (result == null) {
result = s;
}
}
return result;
}

70. 线程安全性的文档化

  • “出现synchronized关键字就足以用文档说明线程安全性”的这种说法隐含了一个错误的观念,即认为线程安全性是一种“要么全有要么全无”的属性。

线程安全性的几种级别:

  1. 不可变的(immutable):这个类的实例是不变的。所以,不需要外部的同步。这样的例子包括String、Long和BigInteger。
  2. 无条件的线程安全(unconditionally thread-safe):这个类的实例是可变的,但是这个类有着足够的内部同步,所以,它的实例可以被并发使用,无需任何外部同步。其例子包括Random和ConcurrentHashMap。
  3. 有条件的线程安全(conditionally thread-safe):除了有些方法为进行安全的并发使用而需要外部同步之外,这种线程安全级别与无条件的线程安全相同。这样的例子包括Collections.synchronized包装返回的集合,它们的迭代器(iterator)要求外部同步。
  4. 非线程安全(not thread-safe):这个类的实例是可变的。为了并发地使用它们,客户必须利用自己选择的外部同步包围每个方法调用(或者调用序列)。这样的例子包括通用的集合实现,例如ArrayList和HashMap。
  5. 线程对立的(thread-hostile):这个类不能安全地被多个线程并发使用,即使所有的方法调用都被外部同步包围。线程对立的根源通常在于,没有同步地修改静态数据。没有人会有意编写一个线程对立的类;这种类是因为没有考虑到并发性儿产生的后果。幸运的是,在Java平台类库中,线程对立的类或者方法非常少。System.runFinalizersOnExit方法是线程对立的,但已经被废除了。

71. 慎用延迟初始化

  • 在大多数情况下,正常初始化要优先于延迟初始化。如果域只在类的实例部分被访问,并且初始化这个域的开销很高,可能就值得进行延迟初始化。
1
2
3
4
5
6
7
8
9
10
// 正常初始化
private final FieldType field = computeFieldValue();

// 延迟初始化,要使用同步访问方法
private FieldType field;
synchronized FieldType getField() {
if (field == null)
field = computeFieldValue();
return field;
}
  • 如果出于性能的考虑而需要对静态域使用延迟初始化,就是用lazy initialization holder class模式。这种模式保证了类要到用到的时候才会被初始化。
1
2
3
4
5
6
private static class FieldHolder {
static final FieldType field = computeFieldValue();
}
static Field getField() {
return FieldHolder.field;
}
  • 如果处于性能考虑而需要对实例域使用延迟初始化,就使用双重检查模式。这种模式避免了在域被初始化之后访问这个域时的锁定开销。
1
2
3
4
5
6
7
8
9
10
11
12
13
private volatile FieldType field;
FieldType getField() {
// 局部变量result的作用是确保field只在已经被初始化的情况下读取一次,提升性能
FieldType result = field;
if (result == null) {
synchronized(this) {
result = field;
if (result == null)
field = result = computeFieldValue();
}
}
return result;
}
  • 延迟初始化一个可以接受重复初始化的实例域,可使用单重检查模式。
1
2
3
4
5
6
7
private volatile FieldType field;
private FieldType getField() {
FieldType result = field;
if (result == null)
field = result = computeFieldValue();
return result;
}

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

不要让程序的正确性依赖于线程调度器,否则结果得到的应用将既不健壮也不具有可移植性。作为推论,不要依赖Thread.yield或者线程优先级。

73. 避免使用线程组

-Chapter 11 序列化

74. 谨慎地实现Serializable接口

为了继承而设计的类应该尽可能少地去实现Serializable接口,用户的接口也应该尽可能少地继承Serializable接口。
如果一个类或者一个接口存在的目的主要是为了参与到某个框架中,该框架要求所有的参与者必须实现Serializable接口,这个时候实现或者扩展Serializable接口就很有意义。
内部类不应该实现Serializable,内部类的默认序列化形式是定义不清楚的,然而静态成员类却可以实现Serializable。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
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());
}

public Foo(int x, int y) {
super(x, y);
}

private static final long serialVersionUID = 185683560954L;
}

75. 考虑使用自动以序列化形式

1
2
3
4
5
6
7
8
9
10
public final class StringList implements Serializable {
private int size = 0;
private Entry head = null;
private static class Entry implements Serializable {
String data;
Entry next;
Entry previous;
}
// ...Remainder omitted
}

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

  1. 它使这个类的导出API永远地束缚在该类的内部表示法上。在上面的例子中,私有的StringList.Entry类变成了公有API的一部分。如果在将来额版本中,内部表示法发生了变化,StringList类仍将需要接受链表形式的输入,并产生链表形式的输出。这个类永远也摆脱不了维护链表项所需要的所有代码,即使它不再使用链表作为内部结构了,也仍然需要这些代码。
  2. 它会消耗过多的空间。在上面的例子中,序列化形式既表示了链表中的每个项,也表示了所有的链接关系,这是不必要的。这些链表项以及链表只不过是实现细节,不值得记录在序列化形式中。因为这样的序列化形式过于庞大,所以把它写到硬盘中,或者在网络上发送都将非常慢。
  3. 它会消耗过多的时间。序列化逻辑并不了解对象图的拓补关系,所以它必须要经过一个昂贵的图遍历(traversal)过程。在上面的例子中,沿着next引用进行遍历是非常简单的。
  4. 它会引起栈溢出。默认的序列化过程要对对象图执行一次递归遍历,即使对于中等规模的对象图,这样的操作也可能引起栈溢出。到底多少个元素会引发栈溢出,这要取决于JVM的具体实现以及Java启动时的命令行参数,(比如Heap Size的-Xms与-Xmx的值)有些实现可能根本不存在这样的问题。

修订版本,transient修饰符表明这个实例域将从一个类的默认序列化形式中省略掉。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
public final class StringList implements Serializable {
private transient int size = 0;
private transient Entry head = null;
private static class Entry {
String data;
Entry next;
Entry previous;
}
// 添加指定的string到这个集合
public final void add(String s) {...}

// 重写writeObject方法, 与物理表示法的细节脱离
private void writeObject(ObjectOutputStream s) throws IOException {
s.defaultWriteObject();
s.writeInt(size);
for (Entry e = head; e != null; e = e.next) {
s.writeObject(e.data);
}
}

// 重写readObject方法,与write对应
private void readObject(ObjectInputStream s) throws IOException, ClassNotFoundException {
s.defaultReadObject();
int numElements = s.readInt();
for (int i = 0; i < numElements; i++) {
add((String) s.readObject());
}
}
...// Remainder omitted
}

尽管StringList的所有域都是瞬时的(transient),但wirteObject方法的首要任务仍是调用defaultWriteObject,readObject方法的首要任务则是调用defaultReadObject。如果所有的实例域都是瞬时的,从技术角度而言,不调用defaultWriteObject和defaultReadObject也是允许的,但是不推荐这样做。即使所有的实例域都是transient的,调用defaultWriteObject也会影响该类的序列化形式,从而极大地增强灵活性。这样得到的序列化形式允许在以后的发行版中增加非transient实例域,并且还能保持向前或者向后兼容性。如果某一个实例将在未来的版本中被序列化,然后在前一个版本中被反序列化,那么,后增加的域将被忽略掉。如果旧版本的readObject方法没有调用defaultReadObject,反序列化过程将失败,引发StreamCorrupted Exception异常。
无论是否使用默认的序列化形式,当defaultWriteObject方法被调用的时候,每一个未被标记为transient的实例域都会被序列化。因此每一个可以被标记为transient的实例域都应该做上这样的标记。这包括那些冗余的域,即它们的值可以根据其他“基本数据类型”计算而得到的域,比如缓存起来的散列值。在将一个域做成非transient的之前,请一定要确信它的值是该对象逻辑状态的一部分。如果你正在使用一种自定义的序列化形式,大多数实例域,或者所有的实例域则都应该被标记为transient,就像上面例子中的StringList那样。
如果正在使用默认的序列化形式, 并且把一个或者多个域标记为transient,则要记住,当一个实例被反序列化的时候,这些域将被初始化为它们的默认值。
无论是否使用默认的序列化形式,如果在读取整个对象状态的任何其他方法上强制任何同步,则必须在对象序列化上强制这种同步。
不管选择了哪种序列化形式,都要为自己编写的每个可序列化的类声明一个显示的序列化版本UID(serial version UID)。第一避免不兼容,第二减小额外计算的开销。

1
pirvate static final long serialVersionUID = randomLongValue;

在编写新类时,为randomLongValue选择什么值并不重要。通过在该类上运行serialver工具,就可以得到这样一个值,但是,凭空编造一个数值也是可以的。如果想修改一个没有序列版本UID的现有的类,并希望新的版本能够接受现有的序列化实例,就必须使用serialver工具为旧版本生成值。

76. 保护性地编写readObject方法

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

指导方针:

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

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

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

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

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

序列化代理模式相当简单:

  1. 为可序列化的类设计一个私有的静态嵌套类,精确地表示外围类的实例的逻辑状态。这个嵌套类被称作序列化代理,它应该有一个单独的构造器,其参数类型就是那个外围类。这个构造器只从它的参数中复制数据:它不需要进行任何一致性检查或者保护性拷贝。从设计的角度来看,序列化代理的默认序列化形式是外围类最好的序列化形式。外围类及其序列代理都必须声明实现Serializable接口。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    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 serialVerionUID = 302480420480234L;
    }
  2. 接下来,将下面的writeReplace方法添加到外围类中。通过序列化代理,这个方法可以被逐字复制到任何类中:

1
2
3
private Object writeReplace() {
return new SerializationProxy(this);
}

这个方法的存在导致序列化系统产生一个SerializationProxy实例,代替外围类的实例。换句话说,writeReplace方法在序列化之前,将外围类的实例转变成了它的序列化代理。所以序列化系统永远不会产生外围类的序列化实例,为了避免攻击者伪造,只要在外围类中添加这个readObject方法即可:

1
2
3
private void readObject(ObjectInputStream s) throws InvalidationException {
throw new InvalidationException("Proxy required");
}
  1. 最后,在SerializationProxy类中提供一个readResolve方法,它返回一个逻辑上相当于外围类的实例。这个方法使序列化系统在反序列化时将序列化代理转变回外围类的实例。
    这个readResolve方法仅仅利用它的公有API创建外围类的一个实例,这正是该模式的魅力之所在。它极大地消除了序列化机制中语言本身之外的特征,因为反序列化实例是利用与任何其他实例相同的构造器、静态工厂和方法而创建的。这样就不必单独确保被反序列化的实例一定要遵守类的约束条件。如果该类的静态工厂或者构造器建立了这些约束条件,并且它的实例方法在维持着这些约束条件,你就可以确信序列化也会维持这些约束条件。
    上述Period.SerializationProxy的readResolve方法:

    1
    2
    3
    private Object readResolve() {
    return new Period(start, end);
    }

  • 两个局限性:它不能与可以被客户端扩展的类兼容,它也不能与对象图中包含循环的某些类兼容:如果企图从一个对象的序列化代理的readResolve方法内部调用这个对象的方法,就会得到一个ClassCastException异常,因为还没有这个对象,只有它的序列化代理。
  • 代价:比保护性拷贝进行的开销大。
  • 当必须在一个不能被客户端扩展的类(final)上编写readObject或者writeObject方法的时候,就应该考虑使用序列化代理模式。
------------- (完) -------------
  • 本文作者: oynix
  • 本文链接: https://oynix.com/2017/11/2ef3b58e394c/
  • 版权声明: 本博客所有文章除特别声明外,均采用 BY-NC-SA 许可协议。转载请注明出处!

欢迎关注我的其它发布渠道