Effective Java 读书笔记-第二章

Thinking in java 太厚了,我不想看,所以先拿EJ开坑。
Effective Java 和 Thinking in java都是java基础评分超高的书,所以有必要看一看。
Effective Java这本书被java之父推荐,所以特意买了本正版。

我很希望10年前就拥有这本书。可能有人认为我不需要任何Java方面的书籍,但是我需要这本书。

——Java 之父 James Gosling

当然,这本书是2009年出版的,已经过去N多个年头,所以带着“批判”的眼光来瞻仰这本巨作。

ps:中文版的翻译真是烂的可以,英文厉害的强烈推荐看英文原版,另外,新手还是先多搬砖,或者去看看java核心技术上下两卷,这本书不适合0基础。

0x01 静态工厂方法代替构造器

用静态工厂方法代替构造器有什么好处呢?

  1. 静态工厂方法有方法名称,可以更确切的针对对象进行不同的构造如构造素数和正整数,可以在同一个类里放两个不同的工厂方法,起两个有意义的名字
  2. 工厂方法不一定每次都会创建一个新对象,如果配合单例,会有更好的性能提升(这个视场景而定)
  3. 可以返回子类型的对象,具有更高的灵活性。
  4. 在创建参数化实例时,代码更加简洁(这个书中使用了泛型作为例子,现在java已经有了类型推断的能力,所以看情况咯)
    1
    2
    3
    4
    5
    6
    7
    8
    //以前
    Map<String,List<String>> m = new HashMap<String,List<String>>();
    //现在
    Map<String,List<String>> m = new HashMap<>();
    //工厂
    public static <K,V> HashMap<K,V> newIncetance(){
    return new HashMap<K,V>();
    }

当然工厂方法也有缺点

  1. 如果类中没有public 或者protected的构造器,就不能子类化
  2. 工厂方法和其他静态方法没有区别,所以不能作为特殊的方法对待

0x02 构造器

上面提到的工厂方法和类的构造函数对多个可选参数时,劣势就明显出来了,一个方法中包含多个参数对调用方是非常不友好的,这时候可以考虑使用构造器(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
// 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 sodium = 0;
private int carbohydrate = 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 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
2
NutritionFacts cocaCola = new NutritionFacts.Builder(240, 8)
.calories(100).sodium(35).carbohydrate(27).build();

但是Builder模式也不是完美的,创建对象前需要先创建Builder对象,而且Builder代码相对比较多,并不适合参数少的时候使用。但是一个类如果要考虑以后扩展属性,最好一开始就使用Builder模式,因为属性越多,静态工厂和构造函数就越难控制。

0x03 单例模式

单例模式一般面试被问到的可能性比较高,什么饱汉饿汉的区别,几种单例模式的写法,单例模式的好处自然是很多的,对只需要被实例化一次的类,最好使用单例模式
一般单例模式的类的构造器被私有化,并且加了一重或者两重判断来保障线程安全,并且有的写法还在构造器中添加防止反射强制实例化的代码。
关于单例模式的几种写法我就不写了,网上有。
Effective Java上介绍的单例模式的代码

1
2
3
4
5
6
7
// 单例模式静态成员变量
public class Elvis {
public static final Elvis INSTANCE = new Elvis();
private Elvis() { ... }
public void leaveTheBuilding() { ... }
}

1
2
3
4
5
6
7
8
// 静态工厂方法
public class Elvis {
private static final Elvis INSTANCE = new Elvis();
private Elvis() { ... }
public static Elvis getInstance() { return INSTANCE }
public void leaveTheBuilding() { ... }
}
1
2
3
4
5
6
// 枚举单例模式
public enum Elvis {
INSTANCE;
public void leaveTheBuilding() { ... }
}

另外为了防止反序列化出假冒的单利的对象,书上说要加上这么一句。具体的牵扯到后面内容,坑以后填。

1
2
3
4
5
6
// readResolve method to preserve sigleton property
private Object readResolve() {
// Return the one true Elvis and let the garbage collector
// take care of the Elvis impersonator.
return INSTANCE;
}

##0x04 私有构造器强化 禁止工具类被实例化
Math类,Arrays类这些工具类是不应该被实例化的,因为里面的方法都是静态的。并且没有实例化的意义,实例化反而会浪费内存。所以编写这类Java类的时候,最好将构造器私有化。
当实例化这些类的时候,应该有异常抛出。

1
2
3
4
5
6
7
public class UtilityClass {
// Suppress default constructor for noninstantiability
private UtilityClass() {
throw new AssertionError();
}
... // Remainder omitted
}

##0x05 不重复创建对象

书中建议相同功能的对象只需要创建一次就行了,不需要多次创建,另外要尽可能的重用对象,高效运用内存。

1
String s = new String("stringette"); // 别这么干!

上面的代码会在常量池创建一个String,再在用构造器在堆里创建一个String,相当于两次创建,最好是这样的写法

1
String s = "stringette";

另外不变的常亮最好事先加载,不要每次使用对象的时候重新创建。书中用了一个例子,判断一个人是不是1946年至1964年生的

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public class Person {
private final Date birthDate;
// 2B程序员的写法
public boolean isBabyBoomer() {
// 没有必要每次都创建Calendar对象
Calendar gmtCal =
Calendar.getInstance(TimeZone.getTimeZone("GMT"));
gmtCal.set(1946, Calendar.JANUARY, 1, 0, 0, 0);
Date boomStart = gmtCal.getTime();
gmtCal.set(1964, Calendar.JANUARY, 1, 0, 0, 0);
Date boomEnd = gmtCal.getTime();
return birthDate.compareTo(boomStart) >= 0 &&
birthdate.compareTo(boomEnd) < 0;
}
}

很明显,下面代码会好很多,如果你看不出来,请回炉重学java

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
public class Person {
private final Date birthDate;
/**
* 正经程序员的写法
*/
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(1964, Calendar.JANUARY, 1, 0, 0, 0);
BOOM_END = gmtCal.getTime();
}
public boolean isBabyBoomer() {
return birthDate.compareTo(boomStart) >= 0 &&
birthdate.compareTo(boomEnd) < 0;
}
}

在这一节,还讲到了拆装箱对程序性能的影响

1
2
3
4
5
6
7
public static void main(String[] args) {
Long sum = 0;
for (long i = 0; i < Integer.MAX_VALUE; i++) {
sum += i;
}
System.out.println(sum);
}

因为将long的第一个字母大写了,导致程序慢了近40秒。
并不是所有创建对象的开销都很大,但是重复创建是很浪费的,内存就那么大,CPU速度也有上限,无意义的拆装箱浪费了性能。所以养成好习惯,节约内存。

另外书中说自己维护对象池是个费力不讨好的事情,如果能交给GC,请务必交给GC。否则代码后期维护将是一个很重量级的工作。

0x06 及时丢弃无用对象

首先看一段代码,是一种栈的实方式

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
// 这里藏着一个内存泄漏的隐患
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;
}
public Object pop() {
if (size == 0)
throw new EmptyStackException();
return elements[--size];
}
/**
* 确保至少有一个元素的可用空间,每次到达临界时让容量增加一倍。
*/
private void ensureCapacity() {
if (elements.length == size)
elements = Arrays.copayOf(elements, 2 * size + 1);
}
}
```
写C/C++的程序员在对象失去作用的时候,会把对象置空。这个是好办法,但是java里没有必要这样,所以程序员对释放资源松懈了.
栈弹出元素后,需要解除对这个元素的引用,否则有可能会导致内存泄漏。
```java
public Object pop() {
if (size == 0)
throw new EmptyStackException();
Object result = elements[--size];
elements[size] = null; // 置空引用
return result;
}

java的缓存是一个内存泄漏的高发地段,因为缓存的对象会被遗忘,最后即使不用也放在缓存中,所以现在的带缓存的功能都有一个时间机智,一段时间不用后,自动回收。

另外回调函数也有可能造成内存泄漏。如果一个对象注册了回调,但是还没等到回调这个对象就被干掉了,这时候,回调的时候就出现了内存泄漏问题。所以注册回调的时候最好是弱引用。减少内存泄漏的概率

0x07 尽量别使用finalizer方法

书中说了很多,概括一下就是

  • 使用这个方法不知道什么时候会被调用,甚至不会执行,容易造成内存泄漏
  • 即使被调用了,不一定会让你得到想要的结果,比如打印异常日志。
  • 如果是多线程,分布式系统,容易导致系统崩溃。(线程被锁住,宕机)
  • 严重的性能损耗
    所以能在对象回收前做完的,不要等到对象失去引用后再做!能不用finalize()方法就不用。

至于好处嘛,finalizer方法的确有两个优点
当对象的所有者忘记调用前面段落中建议的显式终止方法时,可以作为保险方法
另一种是对GC不知道的对象进行保险回收操作,比如Native Peer对象。

FileInputStreamFileOutputStreamTimerConnection),都具有终结方法,当它们的close方法未能被调用的情况下,终结方法提供了一层保障。
书上说:

总之,除非是作为安全网,或者是为了终止非关键的本地资源,否则请不要使用终结方法。在这些很少见的情况下,既然使用了终结方法,就要记住调用super.finalize。如果终结方法作为安全网,要记得记录终结方法的非法用法。最后,如果需要把终结方法与公有的非final类关联起来,请考虑使用终结方法守卫者,以确保即使子类的终结方法未能调用super.finalize,该终结方法也会被执行。

love & peace

转载请注明出处:https://micorochio.github.io/2017/07/28/reading-effective-java-01/

如若有误请帮忙指正,谢谢