Effective Java 读书笔记

本文是Effective Java的读书笔记。

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

静态工厂方法优点:

1.不像构造方法,它们是有名字的

2.与构造方法不同,它们不需要每次调用时都创建一个新对象

3.与构造方法不同,它们可以返回其返回类型的任何子类型的对象

4.返回对象的类可以根据输入参数的不同而不同

5.在编写包含该方法的类时,返回的对象的类不需要存在

静态工厂方法缺点:

1.没有公共或受保护构造方法的类不能被子类化

2.不像构造方法那样在 API 文档中突出

2. 当构造方法参数过多时使用 builder 模式

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
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 {
private final int servingSize;
private final int servings;
private int calories;
private int fat;
private int sodium;
private int carbohydrate;

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 sodium(int val) {
sodium = val;
return this;
}

public Builder carbohydrate(int val) {
carbohydrate = 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;
}
}

客户端代码使用示例:

1
NutritionFacts cocaCola = new NutritionFacts.Builder(240, 8) .calories(100).sodium(35).carbohydrate(27).build();

3. 使用私有构造方法或枚举类实现 Singleton 属性

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

5. 依赖注入优于硬连接资源

通俗解释:就在创建新实例时将资源传递到构造方法中。

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

案例一:

1
2
String s = new String("bikini"); // DON'T DO THIS!
String s = "bikini";

案例二:

错误做法:

1
2
3
4
5
public class RomanNumerals {
static boolean isRomanNumeral(String s) {
return s.matches("^(?=.)M*(C[MD]|D?C{0,3})" + "(X[CL]|L?X{0,3})(I[XV]|V?I{0,3})$");
}
}

正确做法:

1
2
3
4
5
6
7
public class RomanNumerals {
private static final Pattern ROMAN = Pattern.compile("^(?=.)M*(C[MD]|D?C{0,3})" + "(X[CL]|L?X{0,3})(I[XV]|V?I{0,3})$");

static boolean isRomanNumeral(String s) {
return ROMAN.matcher(s).matches();
}
}

​ 案例二实现的问题在于它依赖于 String.matches 方法。 虽然 String.matches 是检查字符串是否与正则表
达式匹配的最简单方法,但它不适合在性能临界的情况下重复使用。 问题是它在内部为正则表达式创建一个
Pattern 实例,并且只使用它一次,之后它就有资格进行垃圾收集。 创建 Pattern 实例是昂贵的,因为它需要
将正则表达式编译成有限状态机(finite state machine)。
​ 为了提高性能,作为类初始化的一部分,将正则表达式显式编译为一个 Pattern 实例(不可变),缓存它,
并在 isRomanNumeral 方法的每个调用中重复使用相同的实例。

如果对象是不可变的,它就始终可以被重用

优先使用基本类型而不是装箱基本类型,要当心无意识的自动装箱

小对象的创建和回收动作是十分廉价的

真正正确使用对象池的典型对象示例就是数据库连接池

7. 消除过期的对象引用

所谓的过期引用,是指永远也不会再被解除的引用。

如果一个对象引用被无意识地保留起来了, 那么,垃圾回收机制不仅不会处理这个对象,而且也不会处理被这个对象所引用的所有其他对象。

一旦对象引用已经过期,只需清空引用即可。即赋值为null。

只要类是自己管理内存,就应该警惕内存泄露问题。

内存泄露的另一个常见来源是缓存。

内存泄露的第三个来源是监听器和回调。确保回调是垃圾收集的一种方法是只存储弱引用(weak references),例如,仅将它们保存在 WeakHashMap 的键(key)中。

8. 应该避免使用终结方法 – finalizer 和 cleaner 机制

终结方法的缺点是不能保证会被及时执行。

9. 使用 try-with-resources 语句替代 try-finally 语句

从以往来看,try-finally 语句是保证资源正确关闭的最佳方式,即使是在程序抛出异常或返回的情况下:

1
2
3
4
5
6
7
8
static String firstLineOfFile(String path) throws IOException {
BufferedReader br = new BufferedReader(new FileReader(path));
try {
return br.readLine();
} finally {
br.close();
}
}

当 Java 7 引入了 try-with-resources 语句后:

1
2
3
4
5
static String firstLineOfFile(String path) throws IOException {
try (BufferedReader br = new BufferedReader(new FileReader(path))) {
return br.readLine();
}
}

10. 重写 equals 方法时遵守通用约定

覆盖equals时总要覆盖hashcode

自反性 对称性 传递性 一致性 如果俩个对象相等,它们必须始终保证相等。

  1. 用 == 操作符 检查 参数是否是这个对象的引用;
  2. 使用 instanceof 操作符 检查 参数是否为正确的类型;
  3. 把参数转换为正确的类型;
  4. 对于参数中的关键域,检查参数中的域是否与该对象中对应的域相匹配;
  5. 当编写完 equal方法时。应该检查它是否是对称的,传递的,一致的。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
public boolean equals(Object obj) {
// 判断obj是否和this相等,保证自反性
if (obj == this) return true;
// 判断obj是否为null,保证最后一条准则
if (obj == null) return false;
// 判断两个对象的实际类型是否相等,
// 如果不相等,则说明比较的是两个不同种类的对象,应当返回false
if (obj.getClass() != this.getClass()) return false;
// 强制类型转换
// 由于之前已经使用getClass判断过实际类型,因此这里强转是安全的
Student stu = (Student) obj;
// 判断每个属性是否相等
// 对于基本类型的属性用“ == ”比较,对象类型的属性用 equals 比较
if (this.age == stu.age && this.name.equals(stu.name)){
return true;
}else {
return false;
}
}

11. 始终重写 toString 方法

12. 谨慎覆盖 clone

永远不要让客户去做任何类库可以替客户完成的事情。

13. 考虑实现 Comparable 接口

​ 无论何时实现具有合理排序的值类,都应该让该类实现 Comparable 接口,以便在基于比较的集 合中轻松对其实例进行排序,搜索和使用。 比较 compareTo 方法的实现中的字段值时,请避免使用 “<” 和 “>” 运算符。 相反,使用包装类中的静态 compare 方法或 Comparator 接口中的构建方法。

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

使用尽可能低的访问级别,与你正在编写的软件的对应功能保持一致。

如果一个包级私有顶级类或接口只被一个类使用,那么可以考虑这个类作为使用它的唯一类的私有静态嵌套类。这将它的可访问性从包级的所有类减少到使用它的一个类。

设计良好的模块会隐藏所有的实现细节,把它的API和实现清晰地隔离开来。然后,模块之间只通过API进行通信。

包含共有可变域的类并不是线程安全的。一个典型的示例如下,非零长度的数组总是可变的,所以类具有公共静态 final 数组属性,或返回这样一个属性的访问器是错误的。 这是安全漏洞的常见来源:

1
public static final Thing[] VALUES = { ... };

要小心这样的事实,一些 IDE 生成的访问方法返回对私有数组属性的引用,导致了这个问题。 有两种方法可以解 决这个问题。 你可以使公共数组私有并添加一个公共的不可变列表:

1
2
3
private static final Thing[] PRIVATE_VALUES = { ... }; 

public static final List<Thing> VALUES = Collections.unmodifiableList(Arrays.asList(PRIVATE_VALUES));

或者,可以将数组设置为 private,并添加一个返回私有数组拷贝的公共方法:

1
2
3
4
5
private static final Thing[] PRIVATE_VALUES = { ... }; 

public static final Thing[] values() {
return PRIVATE_VALUES.clone();
}

总之:要确保 public static final 属性引用的对象是不可变的。

15. 在共有类中使用访问方法而非共有域

16. 使可变性最小化

不可变类只是实例不能被修改的类。

1 不要提供任何修改对象的方法。

2 保证类不会被扩展。

3 使得所有域都是 final 和私有的。

4 确保对于任何可变组件的互斥访问。

不可变对象本质上是线程安全的,不要求同步,可以自由共享。

不可变类的真正唯一缺点,对于每个不同的值都需要一个单独的对象。

17. 组合优于继承

不要继承一个现有的类,而应该给你的新类增加一个私有属性,该属性是现有类的实例引用,这种设计被称为组合(composition),因为现有的类成为新类的组成部分。新类中的每个实例方法调用现有类的包含实例上的相应方法并返回结果,这被称为转发。

下面用一个示例来说明组合优于继承,假设有一个使用 HashSet 的程序。 为了调整程序的性能,需要查询 HashSet ,从创建它之后已经添加了多少个元素。

继承做法:

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
public class InstrumentedHashSet<E> extends HashSet<E> {
private int addCount = 0;

public InstrumentedHashSet() {
}

public InstrumentedHashSet(int initCap, float loadFactor) {
super(initCap, loadFactor);
}

@Override
public boolean add(E e) {
addCount++;
return super.add(e);
}

@Override
public boolean addAll(Collection<? extends E> c) {
addCount += c.size();
return super.addAll(c);
}

public int getAddCount() {
return addCount;
}
}

组合做法:

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
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
public class ForwardingSet<E> implements Set<E> {
private final Set<E> s; // 给你的新类增加一个私有属性,该属性是现有类的实例引用

public ForwardingSet(Set<E> s) {
this.s = s;
}

// 类中的每个实例方法调用现有类的包含实例上的相应方法并返回结果
public void clear() {
s.clear();
}

public boolean contains(Object o) {
return s.contains(o);
}

public boolean isEmpty() {
return s.isEmpty();
}

public int size() {
return s.size();
}

public Iterator<E> iterator() {
return s.iterator();
}

public boolean add(E e) {
return s.add(e);
}

public boolean remove(Object o) {
return s.remove(o);
}

public boolean containsAll(Collection<?> c) {
return s.containsAll(c);
}

public boolean addAll(Collection<? extends E> c) {
return s.addAll(c);
}

public boolean removeAll(Collection<?> c) {
return s.removeAll(c);
}

public boolean retainAll(Collection<?> c) {
return s.retainAll(c);
}

public Object[] toArray() {
return s.toArray();
}

public <T> T[] toArray(T[] a) {
return s.toArray(a);
}

@Override
public boolean equals(Object o) {
return s.equals(o);
}

@Override
public int hashCode() {
return s.hashCode();
}

@Override
public String toString() {
return s.toString();
}
}


public class InstrumentedSet<E> extends ForwardingSet<E> {
private int addCount = 0;

public InstrumentedSet(Set<E> s) {
super(s);
}

@Override
public boolean add(E e) {
addCount++;
return super.add(e);
}

@Override
public boolean addAll(Collection<? extends E> c) {
addCount += c.size();
return super.addAll(c);
}

public int getAddCount() {
return addCount;
}
}

第一种继承做法看起来很合理,但是不能正常工作。我们期望 getAddCount 方法返回的结果是 3,但实际上返回了 6。哪里出现了问题呢? 这是因为在 HashSet 内部, addAll 方法是基于它的 add 方法来实现的。InstrumentedHashSet 中的 addAll 方法首先给 addCount 属性设置为 3,然后使用 super.addAll 方法调用了 HashSet 的 addAll 实现。然后反过来又调用在 InstrumentedHashSet 类中重写的 add 方法,每个元素调用一次。这三次调用又分别给 addCount 加 1,所以一共增加了 6:通过 addAll 方法每个增加的元素都被计算了两次。

第二种组合做法InstrumentedSet 类被称为包装类,因为每个 InstrumentedSet 实例都包含(“包装”)另一个 Set 实例。 这也被称为装饰器模式[Gamma95],因为 InstrumentedSet 类通过添加计数功能来“装饰”一个集合。 有时组合和转发的结合被不精确地地称为委托。

继承是强大的,但它是有问题的,因为它违反封装。 只有在子类和父类之间存在真正的子类型关系时才适用。 即使如此,如果子类与父类不在同一个包中,并且父类不是为继承而设计的,继承可能会导致脆弱性。 为了避免这种脆弱性,使用合成和转发代替继承,特别是如果存在一个合适的接口来实现包装类。 包装类不仅比子类更健壮,而且更强大。

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

好的API文档应该描述了一个给定的方法做了什么工作,而不是如何做到的。

19. 接口优于抽象类

抽象类允许包含某些方法的实现,但是接口不可以。

为了实现由抽象类定义的类型,类必须成为抽象类的一个子类。

接口的优点:

  1. 现有的类可以很容易被更新,以实现新的接口。
  2. 接口是定义mixin(混合类型的)理想选择。
  3. 接口允许我们构造非层次结构的类型框架。

抽象类的演变比接口的演变要容易得多。

如果你导出一个重要的接口,应该强烈考虑提供一个骨架的实现类。

20. 接口仅用来定义类型

一般不定义常量。

常量的定义可以放在本身类,枚举类,工具类中。

21. 类层次优于标签类

标签类很少有适用的情况。 当遇到一个带有标签属性的现有类时,可以考虑将其重构为一个类层次中。

22. 优点考虑静态成员类

嵌套类存在的目的只是为了它的外围类服务。

静态成员类:它看作是一个普通的类,恰好在另一个类中声明,并且可以访问所有宿主类的成员,甚至是那些被声明为私有类的成员。一个常见用途是作为公共帮助类,仅在与其外部类一起使用时才有用。

非静态成员类:每个实例都与一个外部类的外部实例相关联 。

匿名类:是创建小函数对象和处理对象的首选方法(lambda表达式现在是首选),实现静态工厂方法。

局部类:在任何“可以声明局部变量的地方” 都可以声明局部类。

如果你声明了一个不需要访问宿主实例的成员类,总是把static 修饰符放在它的声明中,使它成为一个静态成员类,而不是非静态的成员类。因为非静态成员类的每个实例都与外部类的实例进行关联,浪费时间和空间。

后三个成为内部类。

总结

如果一个嵌套的类需要在一个方法之外可见,或者太长而不能很好地适应一个方法,使用一个成员类。 如果一个成员类的每个实例都需要一个对其宿主实例的引用,使其成为非静态的; 否则,使其静态。 假设这个类属于一个方法内部,如果你只需要从一个地方创建实例,并且存在一个预置类型来说明这个类的特征,那么把它作为一个匿名类;否则,把它变成局部类。

23. 永远不要将多个顶级类或接口放在一个源文件中

24. 新代码中要使用泛型而不是原生态类型

无限制的通配符类型

25. 消除非受检警告

如果无法消除未经检查的警告,并且可以证明引发该警告的代码是安全类型的,则可以在尽可能小的范围内使用 @SuppressWarnings(“unchecked”) 注解来禁止警告。 并且在注释中说明抑制此警告的理由。

26. 列表优先于数组

数组是协变和具体化的; 泛型是不变的,类型擦除的。 因此,数组提供运行时类型的安全性,但不提供编译时类型的安全性。

27. 优先考虑泛型方法

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

PECS 代表: producer-extends,consumer-super

producer:比如向集合中put值,入栈等操作。consumer:比如从集合中取值,出栈等操作。

1
2
List<? extend E>
Collection<? super E>

记住基本规则: producer-extends, consumer-super(PECS)。 还要记住,所有 Comparable 和 Comparator 都是消费者。

29. 合理地结合泛型和可变参数

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

cast方法的使用,下面的Favorites 称为类型安全异构容器:

1
2
3
4
5
6
7
8
9
10
11
public class Favorites {
private Map<Class<?>, Object> favorites = new HashMap<>();

public <T> void putFavorite(Class<T> type, T instance) {
favorites.put(Objects.requireNonNull(type), instance);
}

public <T> T getFavorite(Class<T> type) {
return type.cast(favorites.get(type));
}
}

31. 用枚举代替int常量

枚举类特点:单例、实现了 Comparable 和 Serializable 接口。

如下是一个进行四则运算的枚举类:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public enum Operation {
PLUS, MINUS, TIMES, DIVIDE;

public double apply(double x, double y) {
switch (this) {
case PLUS:
return x + y;
case MINUS:
return x - y;
case TIMES:
return x * y;
case DIVIDE:
return x / y;
}
throw new AssertionError("Unknown op: " + this);
}
}

此代码有效,但不是很漂亮。 如果没有 throw 语句,就不能编译,因为该方法的结束在技术上是可达到的, 尽管它永远不会被达到。

改进:有一种更好的方法可以将不同的行为与每个枚举常量关联起来:在枚举类型中声明一个抽象的 apply 方法,并用常量特定的类主体中的每个常量的具体方法重写它。 这种方法被称为特定于常量(constant-specific)的方法实现:

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

public abstract double apply(double x, double y);
}

以下是与操作符关联的代码:

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
public enum 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;

Operation(String symbol) {
this.symbol = symbol;
}

@Override
public String toString() {
return symbol;
}

public abstract double apply(double x, double y);
}

32. 禁止使用枚举类默认的 ordinal 方法

oridinal方法它返回每个枚举常量类型的数值位置。

反例:

1
2
3
4
5
6
7
public enum Ensemble {
SOLO, DUET, TRIO, QUARTET, QUINTET, SEXTET, SEPTET, OCTET, NONET, DECTET;

public int numberOfMusicians() {
return ordinal() + 1;
}
}

正例:

1
2
3
4
5
6
7
8
9
10
11
12
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 方法。

33. 使用EnumSet代替位属性

EnumSet 是一个专为枚举设计的集合类,EnumSet中的所有元素都必须是指定枚举类型的枚举值,该枚举类型在创建EnumSet时显式或隐式地指定。

EnumSet特点:

  1. EnumSet的集合元素也是有序的,EnumSet以枚举值在Enum类内的定义顺序来决定集合元素的顺序。
  2. EnumSet在内部以位向量的形式存储,这种存储形式非常紧凑、高效,因此EnumSet对象占用内存很小,而且运行效率很好。尤其是进行批量操作(如调用containsAll()和retainAll()方法)时,如果其参数也是EnumSet集合,则该批量操作的执行速度也非常快。
  3. EnumSet集合不允许加入null元素,如果试图插入null元素,EnumSet将抛出NullPointerException异常。
  4. EnumSet类没有暴露任何构造器来创建该类的实例,程序应该通过它提供的类方法来创建EnumSet对象。
  5. 如果只是想判断EnumSet是否包含null元素或试图删除null元素都不会抛出异常,只是删除操作将返回false,因为没有任何null元素被删除。

方法介绍:

  • EnumSet allOf(Class elementType): 创建一个包含指定枚举类里所有枚举值的EnumSet集合。
  • EnumSet complementOf(EnumSet e): 创建一个其元素类型与指定EnumSet里元素类型相同的EnumSet集合,新EnumSet集合包含原EnumSet集合所不包含的、此类枚举类剩下的枚举值(即新EnumSet集合和原EnumSet集合的集合元素加起来是该枚举类的所有枚举值)。
  • EnumSet copyOf(Collection c): 使用一个普通集合来创建EnumSet集合。
  • EnumSet copyOf(EnumSet e): 创建一个指定EnumSet具有相同元素类型、相同集合元素的EnumSet集合。
  • EnumSet noneOf(Class elementType): 创建一个元素类型为指定枚举类型的空EnumSet。
  • EnumSet of(E first,E…rest): 创建一个包含一个或多个枚举值的EnumSet集合,传入的多个枚举值必须属于同一个枚举类。
  • EnumSet range(E from,E to): 创建一个包含从from枚举值到to枚举值范围内所有枚举值的EnumSet集合。

EnumSet 的一个真正缺点是,它不像 Java 9 那样创建一个不可变的 EnumSet ,但是在即将发布的版本中可能会得到补救。 同时,你可以用 Collections.unmodifiableSet 封装一个 EnumSet ,但是简洁性和性能会受到影响。

34. 使用EnumMap代替序数索引

EnumMap是维护枚举类的值和自定义值的映射关系的,所有的键必须是同一个枚举类的实例。

EnumMap是一个与枚举类一起使用的Map实现类
EnumMap在内部以数组的形式保存,所以这种实现形式非常紧凑、高效
EnumMap不允许使用null作为key,但允许使用null作为value

35. 使用接口模拟可扩展的枚举

36. 注解优于命名模式

37. 坚持使用Override注解

38. 使用标记接口定义类型

本条目与条目 20 的的意思正好相反,条目 20 的意思是:“如果你不想定义一个类型,不要使用接口”。本条目的意思是:如果想定义一个类型,一定要使用接口。

Java中常用的三个标记接口分别是:RandomAccess、Cloneable、Serializable。

39. lambda表达式优于匿名类

lambda表达式优于匿名类的主要优点是它更简洁。

40. 方法引用优于lambda表达式

如果方法引用看起来更简短更清晰,请使用它们;否则,还是坚持lambda。

41. 优先使用Collection而不是Stream作为方法的返回类型

42. 检查参数的有效性

保护性拷贝是在检查参数的有效性之前进行的,并且有效性检查是针对拷贝后的对象,而不是针对原始的对象。

43. 仔细设计方法签名

1.谨慎地选择方法的名称。

2.避免过长的参数列表:

  • 创建辅助类
  • build模式

3.对于参数类型, 优先使用接口而不是类。例如,没有理由在编写方法时使用 HashMap 作为输入参数,相反,而是使用 Map 作为参数。这允许传入 HashMap、TreeMap、ConcurrentHashMap、TreeMap 的子 Map(submap)或任何尚未编写的 Map 实现。

4.与布尔类型参数相比,优先使用两个元素枚举类型。

44. 慎用重载

覆盖方法 代码例子。

45. 慎用可变参数

可变参数的每次调用都会导致进行一次数组分配和初始化。

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

有时有人认为,null 返回值比空集合或数组更可取,因为它避免了分配空容器的开销。这个论点有两点是不成立
的。首先,除非测量结果表明所讨论的分配是性能问题的真正原因,否则不宜担心此级别的性能。第二,可以在不分配空集合和数组的情况下返回它们。

如果有证据表明分配空集合会损害性能,可以通过重复返回相同的不可变空集合来避免分配,因为不可变对象可 以自由共享 。

1
2
3
Collections.EMPTY_SET;
Collections.EMPTY_LIST;
Collections.EMPTY_MAP;

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

Javadoc

方法的文档注释应该简洁地描述出它和客户端之间的约定

说明方法做了什么

列举出方法的前置条件 @throws @param

后置条件

类的线程安全性和可序列化性

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

第一次使用的地方声明。

最小化局部变量作用域的最终技术是保持方法小而集中。

49. for each 优于 for 循环

有三种情况是不能使用for each的:

有损过滤(Destructive filtering)——如果需要遍历集合,并删除指定选元素,则需要使用显式迭代器,以便可 以调用其 remove 方法。 通常可以使用在 Java 8 中添加的 Collection 类中的 removeIf 方法,来避免显式遍历。

转换——如果需要遍历一个列表或数组并替换其元素的部分或全部值,那么需要列表迭代器或数组索引来替换元 素的值。

并行迭代——如果需要并行地遍历多个集合,那么需要显式地控制迭代器或索引变量,以便所有迭代器或索引变 量都可以同步进行 (正如上面错误的 card 和 dice 示例中无意中演示的那样)。

50. 了解和使用类库

可靠的算法库

性能不断提升

代码融入主流

Random.nextInt(int)

java.long java.util java.io

集合相关的API

java.util.concurrent

51. float 和 double 是非精确的

使用BigDecimal int long 进行货币计算

52. 基本类型优先于包装类

基本类型只有值

装箱基本类型有null值

基本类型更节省空间和时间

何时使用装箱基本类型:

  1. 集合中的元素,键 和值
  2. 类型参数
  3. 反射调用

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

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

使用StringBuilder

55. 通过接口引用对象

56. 接口优先于反射机制

57. 谨慎地使用本地方法

58. 谨慎地进行优化

要努力编写好的程序而不是快的程序

要考虑API设计决策的性能后果

在每次试图优化之前和之后,要对性能进行测量

设计API

线路层协议

永久数据格式

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

多个缩略语时,首字母大写。HTTPURL 和 HttpUrl。

枚举常量是常量字段,常量字段所有字母大写并且单词之间用 _ 隔开。

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

异常永远不应该用于正常的控制流

设计良好的API不应该强迫它的客户端为了正常的控制流而使用异常

如果类有状态相关的方法,则也应该有状态测试的方法或者可识别的返回值: next()方法 和 hasNext() 方法

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

三种Throwable:受检异常(checked exceptions)、运行时异常(runtime exceptions)、错误(errors)。后两个称为非受检异常。

  1. 如果期望调用者能够合理的恢复程序进行,对于这种情况就应该使用受检异常。通过抛出受检异常,强迫调用者在一个catch子句中处理改异常,或者把它传播出去。

  2. 用运行时异常来表明编程错误,如果程序抛出非受检异常或者错误,往往属于不可恢复的情形,程序继续执行下去有害无益。

  3. 非受检异常是不需要也不应该被捕获的可抛出结构。

  4. 你实现的所有非受检的 throwable 都应该是 RuntimeExceptiond 子类。

  5. 对于可恢复的情况,要抛出受检异常;对于程序错误,就要抛出运行时异常。不确定是否可恢复,就

    跑出为受检异常。

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

63. 优先使用标准的异常

重用异常的好处:

  1. 它使 API 更易于学习和使用,因为它与程序员已经熟悉的习惯用法一致。
  2. 对于用到这些 API 程序而言,它们的可读性会更好,因为它们不会出现很多程序员不熟悉的异常。
  3. 异常类越少,意味着内存占用(footprint)就越小,装载这些类的时间开销也越少。

下表概括了最常见的可重用异常:

异常 使用场合
IllegalArgumentException 非null的参数值不正确
IllegalStateException 不适合方法调用的对象状态
NullPointerException 在禁止使用null的情况下参数值为null
IndexOutOfBoundsExecption 下标参数值越界
ConcurrentModificationException 在禁止并发修改的情况下,检测到对象的并发修改
UnsupportedOperationException 对象不支持用户请求的方法

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

更高层的实现应该捕获低层的异常,同时抛出可以按照高层抽象进行解释的异常。

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

Javadoc 的 @throws 标签

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

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

原则:失败的方法调用应该使对象保持在被调用之前的状态。

有几种途径可以实现这种效果:

  1. 不可变对象。如果对象是不可变的,失败原子性就是必然的。对于可变对象,获得失败原子性最常见的做法就是在执行操作之前检查参数的有效性。例如如下代码:
1
2
3
4
5
6
7
public Object pop() {
if (size == 0)
throw new EmptyStackException();
Object result = elements[--size];
elements[size] = null; /* Eliminate obsolete reference */
return (result);
}

如果取消对初始大小(size)的检查,当这个方法企图从一个空栈中弹出元素时,它仍然会抛出异常。然而,这 将会导致 size 字段保持在不一致的状态(负数)之中,从而导致将来对该对象的任何方法调用都会失败。

  1. 调整计算处理过程的顺序,使得任何可能会失败的计算部分都在对象状态被修改之前发生。
  2. 在对象的一份临时拷贝上执行操作,当操作完成之后再用临时拷贝中的结果代替对象的内容。
  3. 编写一段恢复代码 (recovery code),由它来拦截操作过程中发生的失败,以及便对象回滚到操作开始之前的状态上。这种办法主要用于永久性的 (基于磁盘的) 数据结构。

68. 不要忽略异常

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

long 或者 double 类型变量的读写都不是原子的。

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

增量操作符(++)不是原子的。它执行两项操作:首先它读取值,然后写回一个新值,相当于原来的值再加上 1。如果第二个线程在第一个线程读取旧值和写回新值期间读取这个字段第二个线程就会与第一个线程一起看到同一个值,并返回相同的序列号。这就是安全性失败。

使用 AtomicLong 类,它是 java.util.concurrent.atomic 的组成部分。这个包为在 单个变量上进行免锁定、线程安全的编程提供了基本类型。 volatile 只提供了同步的通信效果,但这个包还提供了原子性。

如下方法仍然无法正确地工作,就是因为++操作符不是原子性的。

1
2
3
4
5
private static volatile int nextSerialNumber = 0;

public static int generateSerialNumber() {
return nextSerialNumber++;
}

修正这个方法的方式如下:

方式1:

在generateSerialNumber方法声明添加 synchronized 修饰符,然后去掉volatile修饰符。

方式2:

使用 java.util.concurrent.atomic 包的类。

1
2
3
4
5
private static final Atomiclong nextSerialNum = new Atomiclong();

public static long generateSerialNumber() {
return nextSerialNum.getAndIncrement();
}

70. 避免过度同步

不要在同步区域调用外来方法

同步区做尽可能少的工作

71. executor 和 task 优先于线程

72. 并发工具优于 wait 和 notify

73. 线程安全性的文档化

线程安全有多种级别:

1.不可变的String、Long、BigInteger等。

2.无条件线程安全的AtomicLong和ConcurrentHashMap。

3.有条件的线程安全,比如Collections.synchronized包装器返回的集合。

4.非线程安全的普通集合,如ArrayList,HashMap。客户端必须使用外部同步来包围每个方法调用。

74. 慎用延迟初始化

在大多数情况下,常规初始化优于延迟初始化。延迟初始化的最佳建议是「除非需要,否则不要这样做」。

静态域的延迟初始化示例:

如果需要在静态字段上使用延迟初始化来提高性能,使用 lazy initialization holder class 模式。

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

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

实例域的延迟初始化示例:

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

private FieldType getField() {
FieldType result = field;
if (result == null) { // First check (no locking)
synchronized (this) {
if (field == null) // Second check (with locking)
field = result = computeFieldValue();
}
}
return result;
}

这段代码可能看起来有点复杂。特别是不清楚是否需要局部变量(result)。该变量的作用是确保 field 在已经初 始化的情况下只读取一次。

虽然不是严格必需的,但这可能会提高性能,而且与低级并发编程相比,这更优雅。在我的机器上,上述方法的 速度大约是没有局部变量版本的 1.4 倍。虽然您也可以将双重检查模式应用于静态字段,但是没有理由这样做:lazy initialization holder class idiom 是更好的选择。

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

编写健壮、响应快、可移植程序的最佳方法是确保可运行线程的平均数量不显著大于处理器的数量。

Thread.yield 没有可预测的语义

76. 序列化的实现

附录

  1. 何时使用静态代码块?

一个类可以使用不包含在任何方法体中的静态代码块,当类被载入时,静态代码块被执行,且只被执行一次,静态块常用来执行类属性的初始化。

  1. 对象初始化顺序
  • 首先,初始化父类中的静态成员变量和静态代码块,按照在程序中出现的顺序初始化;
  • 然后,初始化子类中的静态成员变量和静态代码块,按照在程序中出现的顺序初始化;
  • 其次,初始化父类的普通成员变量和代码块,再执行父类的构造方法;
  • 最后,初始化子类的普通成员变量和代码块,再执行子类的构造方法;
坚持原创技术分享,您的支持将鼓励我继续创作!

------本文结束 感谢您的阅读------