Effective Java

2020-06-28

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

例如:

1
2
3
public static Boolean valueOf(boolean b) {
return b ? Boolean.TRUE : Boolean.FALSE;
}

1.1 静态工厂方法代替构造方法的优缺点

优点:

  • 静态工厂方法不需要每次都创建一个对象,而构造方法每次都会创建一个对象。
  • 静态工厂方法返回值可以是任何对象,而构造方法返回指定的对象。
  • 静态工厂方法返回值可以根据入参的不同而不同。
  • 静态工厂方法有自定义的方法名,构造方法方法名与类名相同。

缺点:

  • 限制:没有公共或受保护的构造方法不能被子类化。(假如定义一个Person类,且该类没有公共或受保护的构造函数,如果我们想定义一个类来继承Person类,将发生编译错误)
  • 静态方法不如构造方法易发现,不仔细阅读源码我们很难找到他们。

1.2 总结

  • 定义静态工厂方法代替构造方法时,建议将构造方法设置成私有的(private)或受保护的(protected)。
  • 如果定义一个静态工厂方法用来代替构造方法,且静态构造方法每次被调用都返回一个新对象,那么使用静态工厂方法代替构造方法将意义不大。
  • 根据需要,重载静态工厂方法。

2 当构造方法参数过多时,使用Builder模式

说明:

使用Builder模式可以使用简化代码,提高代码可读性。(这里对Builder模式不做过多的解释,若您想了解Builder模式,请百度。)

2.1 方式一:使用Lombok插件

安装Lombok插件,引入Lombok依赖之后使用Lombok的@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
/**
* 定义响应值
*
* @author : sungm
* @date : 2020-06-23 17:28
*/
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
@EqualsAndHashCode
public class ResponseDTO implements Serializable {

//序列化
private static final long serialVersionUID = 1L;

private String resultCode;
private String resultMessage;

}

/**
* 主函数
*
* @param args 入参
*/
public static void main(String[] args) {
ResponseDTO response = ResponseDTO.builder()
.resultCode(ResultEnum.ONE.getResultCode())
.resultMessage(ResultEnum.ONE.getResultMessage())
.build();
}

备注:

使用Lombok插件还是蛮香的,我们可以通过使用Lombok定义的注解,来减少了我们编写的代码量、提高了代码可读性,但是Lombok插件会增加我们程序编译的时间,且通常情况下我们也可以通过使用IDEA的快捷键来自动生成代码。

2.2 方式二:使用通用Builder工具类

Builder工具类博客地址:

https://miracle-sungm.github.io/2020/06/15/%E9%80%9A%E7%94%A8Builder/

2.3 总结

  • 对于方式一和方式二的选择,仁者见仁智者见智,你更喜欢哪种方式呢?或者你还有其他更好的方式?

3. 使用私有构造方法或枚举实现单例模式

说明:

单例是一个仅实例化一次的类,通常情况下表示无状态对象。

3.1 使用私有构造方法实现单例模式

两种常见的单例模式实现方式

方式一:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
/**
* 单例模式:声明公共属性的方式
*
* @author : sungm
* @date : 2020-06-29 11:49
*/
public class SingletonOne {

public static final SingletonOne INSTANCE = new SingletonOne();

private SingletonOne() {
}

}

方式二:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
/**
* 单例模式:声明静态工厂方法的方式
*
* @author : sungm
* @date : 2020-06-29 11:51
*/
public class SingletonTwo {

private static final SingletonTwo INSTANCE = new SingletonTwo();

private SingletonTwo() {
}

public static SingletonTwo getInstance() {
return INSTANCE;
}
}

说明:

  • 方式一和方式二的构造函数都是私有构造函数,且定义了一个 static final 类型的INSTANCE变量,并使其实例化,确保了该类只能被实例化一次,保证了全局的唯一性。
  • 不论是方式一还是方式二,特殊情况下可以通过使用反射的方式调用构造方法创建对象,如果需要防止此操作的产生,需修改构造方法,使其请求创建第二个对象时抛出异常。

建议:

建议通过静态工厂方法(方式二)来创建单例。原因是通过静态工厂的方式更加灵活,并且可以根据需要设计泛型单例工厂,并且还能使用函数式接口Supplier,例如Singleton::getInstance。

3.2 通过使用枚举设计单例模式

例如:

1
2
3
public enum Singleton {
INSTANCE;
}

说明:

这种方式类似于公共属性方法,但更简洁,提供了免费的序列化机制,并提供了针对多个实例化的坚固保证,即使是在复杂的序列化或反射攻击的情况下。这种方法可能感觉有点不自然,但是单一元素枚举类通常是实现单例的最佳方式。

注意:

如果单例必须继承 Enum 以外的父类 (尽管可以声明一个 Enum 来实现接口),那么就不能使用这种方法。


4. 使用私有构造方法执行非实例化

场景说明:

当我们希望设计一个类只包含静态方法和静态属性时,为了避免这样的类被实例化,可以通过声明私有的构造方法达到类不被实例化的目的。

副作用:

当我们想设计一个子类来继承私有化构造函数的父类时,将报编译异常。因为所有构造方法都必须显示或者隐式的调用父类的构造方法,若父类的构造函数被私有化,则子类没有访问父类构造函数的权限,因此报错。


5. 比起硬资源连接,优先使用依赖注入

硬资源连接:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
public class Dictionary {
//定义字典
}


public class SpellChecker {

//硬资源连接:想当然的认为一本字典就够了,无法承载多字典的场景
private static final Dictionary DICTIONARY = new ChineseDictionary();

private SpellChecker() {
}

public boolean check(String word) {
...
}

}

依赖注入:

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
/**
* 定义字典接口
*
* @author : sungm
* @date : 2020-06-29 20:53
*/
public interface Dictionary {
...
}

/**
* 定义中文字典
*
* @author : sungm
* @date : 2020-06-29 20:53
*/
public class ChineseDictionary implements Dictionary{
...
}

/**
* 定义英文字典
*
* @author : sungm
* @date : 2020-06-29 20:56
*/
public class EnglishDictionary implements Dictionary {
...
}

/**
* 拼写检查器
*
* @author : sungm
* @date : 2020-06-29 20:56
*/
public class SpellChecker {

private final Dictionary DICTIONARY;

//使用依赖注入的方式,创建对象时将字典注入到对象属性中
public SpellChecker(Dictionary dictionary) {
this.DICTIONARY = dictionary;
}

public boolean check(String word) {
...
}

}

说明:

当一个类依赖于一个或多个底层资源时,该类的实现方式不要使用单例或静态的实用类,这些资源的行为会影响类的行为,并且不让类直接创建这些资源。相反,将资源或工厂传递给构造方法(或静态工厂或 builder 模式)。这种称为依赖注入的实践将极大地增强类的灵活性、可重用性和可测试性。


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

说明:

通常情况下,重用对象比创建一个相同功能的对象的做法更恰当,重用可以使程序更快的执行且耗用更少的内存。如果一个对象是不可变的,他总是可以被重用。

举例:

1
2
//请不要这样写代码
String name = new String("sungm");

这样写代码产生的结果:

该语句每次执行时都会创建一个新的String实例,并且这些对象都不是必须的。String的构造方法String(String original)的入参original本身就是一个String实例,它与构造方法创建的对象的功能相同。

优化后:

1
String name = "sungm";

总结:

  • 通过使用静态工厂方法,可以避免创建不必要的对象。

    例如:使用静态工厂方法 Boolean.parseBoolean(String) 比构造方法 Boolean(String) 好的多,构造方法每次调用都会创建一个新的对象,而工厂方法永远不需要这样做,在实践中也不需要。

  • 一些对象的创建比其他对象的创建的代价要昂贵的多,如果要使用这样一个昂贵的对象,建议将其缓存起来以便重复使用。

    例如创建创建正则表达式的 Pattern 对象,因为它需要将正则表达式编译成有限状态机(finite state machine)。

  • 当一些对象是不可变(final定义的对象)的时侯,很明显它可以被重用,但是在其他情况下,没有很明显的可以被重用,这种情况下需谨慎考虑是否重用对象。
  • 自动装箱的情况下是不需要创建不必要的对象的,自动装箱允许程序员混用基本数据类型和装箱类型,根据需要自动装箱和拆箱。

    建议:优先使用基本数据类型而不是装箱的基本类型,也要注意无意识的自动装箱。


7. 消除过期对象的引用

举例:

《Effective Java》原书中该章节举了一个栈(Stock)弹出元素(pop)没有及时清空弹出的元素引用,当Stock扩容后收缩,容易发生内存溢出异常。

好处:

  • 及时消除过期对象的引用,减少内存消耗,增加程序执行速度,同时可以避免因内存溢出导致程序异常。
  • 消除过期对象的引用有一个好处是程序错误的引用过期的对象之后能及时抛出NPE(空指针异常),而不是让程序在引用过期的对象之后继续悄悄地做错误的事请。

不建议:

  • 不建议程序结束后立即清空所有对象的引用,因为这是Java垃圾回收器的工作。清空对象引用应该是例外而不是规范,程序结束后立即清空所有对象的引用是不必要的,也是不可取的。

常见的内存溢出:

  • 当一个类自己管理内存时,应警惕内存泄漏的问题
  • 缓存:防止业务数据缓存之后没有及时清空
  • 监听器和回调,防止程序执行时间过长导致内存溢出。

总结:

  • 内存溢出问题通常情况下不会变现出明显的故障,但一些没必要的内存消耗可能一直存在于系统中,建议多留意代码细节和实现方式,减少不必要的内存消耗。

8. 避免使用 Finalizer 和 Cleaner 机制

//TODO 待完成


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

说明:

  • Java 类库中有很多必须通过调用 close 方法手动关闭的资源。比如 InputStream、OutputStream 等等。年轻的程序员可能经常忽略关闭这些资源,未及时关闭资源会影响系统性能,甚至终止程序。尽管这些资源中有很多使用 finalizer 机制作为安全网,但 finalizer 机制却不能很好地工作。
  • 在我们 JDK 7 发布之前,我们使用 try-finally 语句保证资源的正确关闭是最佳的方式,JDK 7 发布之后,我们可以通过使用 try-with-resource 更好的关闭资源,但是使用 try-with-resource 关闭资源必须满足一些条件,详见下文。

使用 try-with-resource 语句需满足的条件:

  • 资源需实现 AutoCloseable 接口, Java 类库中和第三方类库中许多类都实现或继承了该接口,如果我们程序设计的类需要关闭资源,那么这个类也应该实现 AutoCloseable 接口。

使用 try-with-resource 语句的优点:

比 try-finally 语句更加精简,具有更好的可读性,并且生成的异常更有用。
比 try-finally 语句关闭资源更容易,也不会出错。

使用 try-with-resource 语句关闭资源时的特征:

先声明的资源先关闭,后声明的资源后关闭。

例如:

1
2
3
4
5
6
7
8
//先关闭 fin 资源, 接着关闭 fout 资源, 最后关闭 out 资源
try (
FileInputStream fin = new FileInputStream(input);
FileOutputStream fout = new FileOutputStream(output);
GZIPOutputStream out = new GZIPOutputStream(fout)
) {
//do something...
}

备注:

这里不详细说明 try-with-resource 语句的用法,想要了解的同学可以网上搜索相关资料。


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

//TODO 待完成