flyEn'blog

effectiveJava

Effective Java

本文为阅读《Effective Java》书籍的笔记摘录。

创建和销毁对象

1. 多用静态工厂方法

优点:

  • 有名称——可读性高

  • 不必每次调用都创建一个新对象——性能

  • 可返回任何子类型对象——灵活性高

  • 返回对象的类可随着每次调用而变化(取决于参数值)

    比如EnumSet没有公有的构造器,只有静态工厂方法。在OpenJDK中,它们返回两种子类之一的一个实例,具体取决于底层枚举类型的大小。

  • 返回的对象所属的类,可以在编写时不存在。

缺点:

  • 如果不包含public/protected的构造器,就不能被子类化。
  • 很难被发现

可读性高

惯用名称:

from、of、valueOf、instance、getInstance、create、newInstance、getType、newTypetype(type-子类名)

性能高

在某些情况下,可以事先进行实例化一些对象,调用时直接调用即可,不需要进行改变。比如Boolean。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
public final class Boolean implements Serializable, Comparable<Boolean> {
// 预先设置两个对象
public static final Boolean TRUE = new Boolean(true);
public static final Boolean FALSE = new Boolean(false);

public Boolean(boolean var1) {
this.value = var1;
}

public Boolean(String var1) {
this(parseBoolean(var1));
}

// 工厂方法
public static Boolean valueOf(boolean var0) {
return var0?TRUE:FALSE; // 返回预先设置的对象,而不是创建对象
}
// 工厂方法
public static Boolean valueOf(String var0) {
return parseBoolean(var0)?TRUE:FALSE;
}
// ... other code
}

灵活性高

可根据具体情况,返回子类。相当于更强大的工厂。直接从父类获取到子类。尤其适用于工具类(提供各种API)。例子:Collections。

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
public class Collections {
// 私有,典型工厂
private Collections() {
}

public static final List EMPTY_LIST = new EmptyList<>();
// 工厂方法
public static final <T> List<T> emptyList() {
return (List<T>) EMPTY_LIST;
}
private static class EmptyList<E> extends AbstractList<E> implements RandomAccess, Serializable {
// code
}

// 工厂方法
public static <E> List<E> checkedList(List<E> list, Class<E> type) {
// 根据具体情况,获取相应子类
return (list instanceof RandomAccess ?
new CheckedRandomAccessList<>(list, type) :
new CheckedList<>(list, type));
}

// 子类1
static class CheckedRandomAccessList<E> extends CheckedList<E> implements RandomAccess {
CheckedRandomAccessList(List<E> list, Class<E> type) {
super(list, type);
}

public List<E> subList(int fromIndex, int toIndex) {
return new CheckedRandomAccessList<>(
list.subList(fromIndex, toIndex), type);
}
}

// 子类2
static class CheckedList<E> extends CheckedCollection<E> implements List<E> {
// code
}
}

2. 多个构造器参数时应考虑用Builder模式

第一种方式重叠构造器 模式

在这种模式下,提供的第一个构造器只有必要的参数,第二个有一个可选参数,第三个有两个可选参数,以此类推,最后一个包含所有参数。

可行,但是有许多参数时,代码难编写并且可读性较差。

第二种方式JavaBeans 模式

在这种模式下,先调用一个无参构造器创建对象,再调用setter方法来设置每个参数。

创建实例容易,可读性也提升,但是对象不能做到不可变。在构造过程中JavaBeans可能处于不一致的状态,需要多花精力去确保它的线程安全

第三种方式Builder 模式 (多个构造参数时推荐)

它不直接生成最后的对象,而是利用必要的参数调用构造器,得到一个builder对象。然后使用者在builder对象上调用setter的方法,来设置相关可选参数。最后使用者调用无参的build方法来生成通常是不可变的对象

灵活,容易编写,可读性强,模拟了具名的可选参数,也适用于类层次结构(抽象-具类)。

一般只有在有很多参数的时候才使用(也会稍微有额外性能的开销)

👉举例:

比如一个类表示包装食品外面显示的营养成分标签。几个域是必需的:每份的含量、每份的卡路里;还有很多可选域:总脂肪量、饱和脂肪量、转化脂肪、胆固醇、钠等。

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
// builder Pattern
public class NutritionFacts {
private final int serving;
private final int calories;
private final int fat;
private final int sodium;
private final int carbohydrate;

// 私有构造器(通过内部builder对象构造)
private NutritionFacts(Builder builder) {
this.serving = builder.serving;
this.calories = builder.calories;
this.fat = builder.fat;
this.sodium = builder.sodium;
this.carbohydrate = builder.carbohydrate;
}

public static class Builder {
// required parameters
private final int serving;
private final int calories;

// Optional parameters
private int fat;
private int sodium;
private int carbohydrate;

public Builder(int serving, int calories){
this.serving = serving;
this.calories = calories;
}

public Builder fat(int val){
fat = val;
return this;
}

public Builder sodium(int val){
sodium = val;
return this;
}

public Builder carbohydrate(int val){
carbohydrate = val;
return this;
}

public NutritionFacts build() {
return new NutritionFacts(this);
}
}

// 流式API,可随意选择可选参数进行build
public static void main(String[] args) {
NutritionFacts nutritionFacts = new NutritionFacts.Builder(240, 120).fat(10).sodium(35).carbohydrate(12).build();
}
}

3. 用私有化构造器或枚举类型强化singleton

singleton指最多会被实例化一次的类。通常情况下,以前的做法是没有问题的。但是在某些高级情况下,通过使用反射的相关知识访问private的构造函数,破坏singleton。

1
2
3
4
5
public class Elvis{
// 注意,公有final对象
public static final Elvis INSTANCE = new Elvis();
private Elvis(){}
}

另一种情况,在序列化的过程中,反序列化得到的对象已经不再是以前的对象(破坏了singleton),这种情况下,可以通过单元素枚举型处理。

1
2
3
4
public enum Elvis{
INSTANCE;
// some methods
}

###

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

有一些工具类,仅仅是提供一些能力,自己本身不具备任何属性,所以,不适合提供构造函数。然而,缺失构造函数编译器会自动添加上一个无参的构造器。所以,需要提供一个私有化的构造函数。为了防止在类内部误用,再加上一个保护措施和注释。

1
2
3
4
5
6
public class Util{
private Util(){
// 抛出异常,防止内部误调用
throw new AssertionError();
}
}

弊端是无法对该类进行继承。

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

  • 对象的重用
  • 昂贵的对象,使用对象池
  • 廉价的对象,慎用对象池。

现代JVM对廉价对象的创建和销毁非常快,此时不适于使用对象池。

6. 消除过期的对象引用

以下三种情况可能会造成内存泄露:

  • 自己管理的内存(数组长度减小后,pop出的对象容易导致内存泄漏)
  • 缓存
  • 监听和回调

自己管理的内存

对于自己管理的内存要小心,比如:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
public class Stack{
private Object[] elements;
private int size = 0;
private static final int DEFAULT_INITIAL_CAPACITY = 16;

public Stack(){
elements = new Object[DEFAULT_INITIAL_CAPACITY];
}

public void push(Object e){
ensureCapacity();
elements[size++]=e; // allocate新的堆内存和栈内存
}

public Object pop(){
if(size==0) throw new EmptyStackException();
return element[--size]; // pop出element[size],该对象不再有效。内存泄漏原因。
}

private void ensureCapacity(){
if(elements.length==size)
elements = Arrays.copyOf(elements, 2*size+1);
}
}

弹出的对象不再有效,但JVM不知道,所以会一直保持该对象,造成内存泄露。

解决:

1
2
3
4
5
public Object pop(){
if(size==0) throw new EmptyStackException();
elements[size] = null; // 等待回收
return element[--size];
}

缓存

缓存的对象容易被程序员遗忘,需要设置机制来维护缓存,例如不定期回收不再使用的缓存(使用定时器)。某些情况下,使用WeakHashMap可以达到缓存回收的功效。注,只有缓存依赖于外部环境,而不是依赖于值时,WeakHashMap才有效。

监听或回调

使用监听和回调要记住取消注册。确保回收的最好的实现是使用弱引用(weak reference),例如,只将他们保存成WeakHashMap的键。

7. 避免显示调用GC

Java的GC有强大的回收机制,可以简单的记住:不要显示调用finalizer。可以这样理解:

jvm是针对具体的硬件设计的,然而程序却不是针对具体硬件设计的,所以,java代码无法很好的解决gc问题(因为他具有平台差异化)。另外,finalizer的性能开销也非常大,从这个角度上考虑也不应该使用它。

8. 避免equals方法请遵守通用约定

  • 自反性。 x.equals(x) == true
  • 对称性。 当前仅当y.equals(x)==true时,x.equals(y)==true
  • 传递性。 if(x.equals(y)&&y.equals(z)),y.equals(z)==true
  • 一致性。
  • 非空性。 x.equals(null)==false

9. 覆盖equals方法时总要覆盖hashCode

为了保证基于散列的集合使用该类(HashMap、HashSet、HashTable),同时,也是object.hashCode的通用约定,覆盖equals方法时,必须覆盖hashCode。

10.始终覆盖toString

Object的toString方法的通用约定是该对象的描述。注意覆盖时,如果有格式,请备注或者严格按照格式返回。

11. 谨慎覆盖clone

12. 考虑实现Comparable接口

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

目的是解耦。简单来讲,使用修饰符的优先级从大到小,private>protected>default(缺省)>public。如果在设计之初,设计为private修饰符后,在之后的编码过程如果不得不扩大其作用于,应该先检查是否设计的确如此。

子类覆盖超类,不允许访问级别低于超类的访问级别。(超类的protected,子类覆盖后不能改为default)。

成员变量决不允许是公有的。一旦设置为公有,则放弃了对他处理的能力。这种类并不是线程安全的。即使是final的,也不允许。除非希望通过public static final来暴露常量。成员变量总是需要使用setter和getter来维护。有一个例外:长度非零的数组。这是安全漏洞的一个根源。

1
2
3
4
5
public Object pop(){
if(size==0) throw new EmptyStackException();
elements[size] = null; // 等待回收
return element[--size];
}

改进

1
2
3
4
private static final Thing[] PRIVATE_VALUES = {...}
// 此时获取到的才是“常量”
public static final List<Thing> VALUS =
Collections.unmodifiableList(Arrays.asList(PRIVATE_VALUES))

另一种:

1
2
3
4
5
private static final Thing[] PRIVATE_VALUES = {...}
// 此时获取到的才是“常量”
public static final Thing[] values(){
return PRIVATE_VALUES.clone();
}

14.在公有类中使用访问方法而非公有成员变量(类似13)

15. 使可变性最小化

16. 复合优先于继承

继承有利于代码复用,但是尽可能不要进行跨包的继承。包内的继承是优秀的设计方式,一个包里的文件处在同一个程序员的控制之下。但是继承有其局限性:子类依赖于超类。超类一旦发生更改,将可能破坏子类。并且,如果超类是有缺陷的,子类也会得“遗传病”。

复合,即不扩展已有的类,而是在的类中新增一个现有类的。相当于现有类作为一个组建存在于新类中。如此,将只会用到需要用到的东西,而不表现现有类所有的方法和成员变量。新类也可以称为“包装类”,也就是设计模式中的Decorate模式。

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

18.接口优于抽象类

19.接口只用于定义类型

20.类层次优先于标签类

21.用函数对象表示策略

函数参数可以传入类似listener的对象,目的是使用listener中的方法。如果使用匿名的参数,每一次调用会创建新的对象。可以将listener声明为成员变量,每次都复用同一个对象,并且可以使用静态域(static变量)。比如String类的CASE_INSENSITIVE_ORDER域。

22.优先考虑静态类成员

嵌套类的目的应该只是为了他的外围类提供服务,如果以后还可能用于其他环境中,则应该设计为顶层类。静态类相当于一个普通的外部类,只是恰好声明在了一个类内部。通常的用户是:Calculator.Operation.PLUS等。和普通类的区别只是,在PLUS前,有了2个前缀,来表明其含义。而非静态类必须存在于外部类对象中。不要手动在外部创建一个内部非静态类对象,创建的过程是:instance.New MemberClass()。这非常奇怪。

如果成员类不需要访问外围类,则需要添加static,是他成为静态成员类,否则每个实例都将包含一个额外指向外围对象的引用。将会影响垃圾回收机制。

Fork me on GitHub