0%

Effective Java 第三版 (1) 创建和销毁对象

创建和销毁对象

§ 考虑使用静态工厂方法代替构造函数

优点:

  • 有名字,可以更确切描述返回对象的名称。
  • 不用每次调用都创建一个新的对象,可以重复调用返回 相同实例
  • 可以返回原返回类型的任何子类型对象。
  • 返回对象的类可以根据输入参数的不同而不同
  • 便携包含该方法的类时,返回的对象的类不需要存在。
  • 可以在创建参数化类型实例的时候,使代码更简洁。
1
2
3
4
5
6
7
8
9
10
11
12
public static final Boolean TRUE = new Boolean(true);

// Boolean valueOf() 方法返回了可重复使用的 Boolean 实例
public static Boolean valueOf(boolean b) {
return (b ? TRUE : FALSE);
}


public static final List EMPTY_LIST = new EmptyList<>();

// Collections emptyList() 方法返回了可重复使用的 List 实例
Collections.emptyList();

缺点:

  • 类如果不含公有的或受保护的构造器,就不能被子类化
  • 它们与其他的静态方法实际没有区别,没有明确的标识,难以查找

以下是静态工厂方法常用名称:

  • from__,类型转换方法,接受单个参数并返回此类型的相应实例。如 Date d = Date.from(instant)
  • of__,聚合方法,接受多个参数返回该类型的实例,并合并在一起。如 Set<Rank> faceCards = EnumSet.of(JACK, QUEEN, KING)
  • valueOf__,from 与 to 更详细的替代方式
  • instancegetInstance__,返回一个由其参数描述的实例
  • createnewInstance__,保证每次调用返回一个新实例。
  • getType__,与 getInstance 类似,但是在工程方法处于不同的类时候使用。getTyp 中的 Type 是工厂方法返回的对象类型。如:FileStore fs = Files.getFileStore(path)
  • newType__,与 newInstance 类似
  • type__,getType 和 newType 简洁替代方法,如 List<Complaint> litany = Collections.list(legacyLitany)

§ 构造参数过多时要考虑使用 Builder 模式

类似于 lombok 中的 @Builder 注解

优点:

  • 易于编写和阅读
  • 可以对多个参数强加约束条件

缺点:

  • 需要先创建它的构造器,会有一点点开销(虽然不明显
1
2
3
4
User user = User.builder()
.id(1)
.name("李四")
.build();

§ 用私有构造器或者枚举类型强化单例 Singleton 模式

经典的饿汉式单例模式,通过公有的静态工厂方法返回同一个对象的引用且 线程安全

1
2
3
4
5
6
7
8
9
10
11
12
13

public class Elvis {
private static final Elvis INSTANCE = new Elvis();

// 设置为私有
private Elvis() {}

public static Elvis getInstance() {
return INSTANCE;
}

public void leaveTheBuilding() {}
}

推荐单一元素的 枚举类型 是实现 Singleton 的最佳方法,无偿提供序列化机制,防止多次实例化。

1
2
3
4
5
public enum Elvis {
INSTANCE;

public void leaveTheBuilding() {}
}

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

当类不包含显示的构造器时,Java 编译器会自动提供一个公有的、无参的的构造器。

所以,当一些类(如工具类)不希望被实例化时,应当提供私有的构造器

1
2
3
4
5
public class UtilityClass {
private UtilityClass() {
throw new AssertionError()
}
}

AssertionError 可以避免不小心在类的内部调用构造器

§ 依赖注入优于硬连接资源

1
2
3
4
5
6
7
8
9
10
11
public class SpellChecker {
private final Lexicon dictionary;

// 通过构造器传入实例
public SpellChecker(Lexicon dictionary) {
this.dictionary = Objects.requireNonNull(dictionary);
}

public boolean isValid(String word) { ... }
public List<String> suggestions(String typo) { ... }
}

不要用单例和静态工具类来实现依赖一个或多个底层资源的类,且该资源的行为会影响到该类的行为;

也不要直接用这个类来创建这些资源,而应该将这些资源或者工厂传递给 构造器。通过它们来创建类。

这个就是 依赖注入

依赖注入极大的提高了灵活性和可测试性,但可能使大型项目变得混乱,使用依赖注入框架(Dagger、Guice、Spring)可以消除这些混乱。

§ 避免创建不必要的对象

1
2
3
4
5
// 极端的例子,每次执行都会创建一个新的 String 实例
String s = new String("stingette")

// 改进后,字符串常量池
String s = "stringette"

通常可以使用提供静态工厂方法而不是构造器,避免创建不必要的对象。

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

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

如上,每次 sum 计算增加 long 会构造一个实例,应当避免。

创建正则表达式 Pattern 实例是昂贵的,需要将正则表达式编译成有限状态机。如果经常调用,可以将其作为类初始化的一部分。

1
2
3
4
5
6
7
public class RomanNumerals {
private static final Pattern ROMAN = Pattern.compile("");

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

§ 消除过期的对象引用

过期的对象引用,是指永远也不会再被解除的引用,如下所示:

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
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.copyOf(elements, 2 * size +1);
}
}
}

栈先增长,再收缩,那么从 pop 弹出来的对象永远不会再被访问了,但它的引用仍然存在于数组中,将不会被当做垃圾回收,即使栈的程序不再引用这些对象。从而产生内存泄漏。
该例同时可参见《算法 第四版》1.3.2.4 对象游离。

解决方法是:

1
2
3
4
5
6
7
8
public Object pop() {
if (size == 0) {
throw new EmptyStackException();
}
Object result = elements[--size];
elements[size] = null;
return result;
}

清空对象引用应该是一种例外,而不是一种规范行为,只要 Stack 实例结束生命周期(如在比较紧凑的作局部用域范围内),那么就会进行垃圾回收。

因为是 Stack 自己管理内存,存储池包含了 elements 数组,对于垃圾回收器而言,elements 数组中的所有对象引用都同等有效。

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

参考 JDK 中 ArrayList 中的示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
public E remove(int index) {
rangeCheck(index);

modCount++;
E oldValue = elementData(index);

int numMoved = size - index - 1;
if (numMoved > 0)
System.arraycopy(elementData, index+1, elementData, index,
numMoved);
elementData[--size] = null; // clear to let GC do its work

return oldValue;
}

§ 避免使用终结方法

终结方法,即finalize() 。Java 不仅不保证终结方法会被及时的执行,而且根本就不保证它们会被执行。

当一个程序终止的时候,某些已经无法访问的对象上的终结方法根本没有被执行,这是完全有可能的。如依赖终结方法来释放公共资源(数据库)上的永久锁,很容易让整个分布式系统垮掉。

System.gcSystem.runFinalization 这两个方法也并不保证终结方法一定会被执行。

推荐的做法是提供一个显式终止的方法。并且通常与try-finally 结构结合使用,以确保及时终止。如 InputStream、OutputStream 上的 close 方法

1
2
3
4
5
6
7
8
9
10
Foo foo = new Foo();
try {

......

} finally {
if (foo != null) {
foo.terminate()
}
}

§ 使用 try-with-resources 代替 try-finally 语句

许多资源必须通过调用 close 方法手动关闭资源,try-finally 是可以保证资源正确关闭的最佳方式,即使程序抛出异常或返回的情况下。

但是当资源很多时候,需要多个 try-finally 语句,冗长混乱。

Java7 引入的 try-with-resources 语句得到了很好的解决。要使用这个构造,资源必须实现 AutoCloseable 接口。

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

这样生成的代码更简洁,必要时还可以添加 catch 子句。