Effective Java读书随想1

阔别许久,想起之前定下的一月一博客的目标,可真够宏伟的。理由倒是有很多,但是终归是我懈怠了。但即使这样,我依然不想放弃那个目标,那就从现在开始补起来吧。算上9月、10月欠的,11月要写三篇。11月对我好一点😭。

Effective Java能被誉为Java四大名著之一,看来是有原因的。我本以为它会是一本晦涩难懂的大部头,没想到如此贴近实战,给了我很多的启发,故作读书随想。

类的实例化和非实例化

本文中讨论的对象(Object),实际上就是类的实例。

本书前9节

  1. 考虑使用静态工厂方法替代构造方法
  2. 当构造方法参数过多时使用 builder 模式
  3. 使用私有构造方法或枚举类实现 Singleton 属性
  4. 使用私有构造方法执行非实例化
  5. 使用依赖注入取代硬连接资源(hardwiring
    resources)
  6. 避免创建不必要的对象
  7. 消除过期的对象引用
  8. 避免使用 Finalizer 和 Cleaner 机制
  9. 使用 try-with-resources 语句替代 try-finally 语句

实际围绕一个主题,类的实例。一个类,总是应当被设计为可实例化和不可实例化的。其中可实例化类又分为对象的创建,对象的回收。

可实例化

对于一个被设计为可实例化的类,应当考虑何种实现是最高效的。

创建对象

创建对象时,除了传统的公共构造方法外,还可以考虑使用静态工厂方法(和设计模式中的工厂方法模式没有关系)。这点实际上是其它一切优化的基础,毕竟直接使用构造函数的限制很多。

Boolean(boolean基本类型的包装类)的简单例子

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

优点:

  1. 静态方法命名自由,可以描述构造行为。
  2. 静态方法不需要每次调用时都创建一个新对象。
  3. 静态方法可以返回其返回类型的任何子类型的对象。
  4. 参数组合清晰,返回对象的类可以根据输入参数的不同而不同。
  5. 容易版本化,方法可以标记过时然后删除,但构造器不方便。
  6. 构造器之间互相调用有顺序要求,而静态方法无限制。

缺点:

  1. 需要调用者知道哪些静态方法是提供构造功能的,建议辅以注释。
  2. 静态方法不能访问实例变量。
  3. 可能返回null。

当构造方法参数过多时就可以使用 builder 模式,在实践中往往使用Lombok的@Builder注解实现。

当试图创建一个单例对象时,书上给出了三种方法:
先给出前两种

1
2
3
4
5
public class Elvis { 
public static final Elvis INSTANCE = new Elvis();
private Elvis() { ... }
public void leaveTheBuilding() { ... }
}
1
2
3
4
5
6
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
7
8
9
10
11
12
13
14
15
16
17
18
public class Elvis { 
private static Elvis INSTANCE;
private Elvis() {
if(INSTANCE != null){
throw new Exception();
}
else{
INSTANCE = this;
}
}
public static Elvis getInstance() {
if(INSTANCE == null){
INSTANCE = new Elvis();
}
return INSTANCE;
}
}
public void leaveTheBuilding() { ... }

可以看到这种方式可以在构造方法中实现一些安全措施,防止通过反射创建出对象。

但在这种情况下,如果有多个线程同时获取实例,就会出现问题。再稍作改进

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
public class Elvis { 
private static Elvis INSTANCE;
private Elvis() {
if(INSTANCE != null){
throw new Exception();
}
else{
INSTANCE = this;
}
}
public static Elvis getInstance() {
if(INSTANCE == null){
synchronized(Elvis.class){
if(INSTANCE == null){
INSTANCE = new Elvis();
}
}
}
return INSTANCE;
}
}
public void leaveTheBuilding() { ... }

当这个类实现序列化时,会出现新的问题。当对象被序列化和反序列化时,Java不会调用构造函数,而是通过反射机制直接创建对象实例。

补充一下序列化和反序列化的流程

1
2
>ObjectInputStream inputStream = new ObjectInputStream(...);
>Elvis instance = (Elvis) inputStream.readObject(); // 关键调用

在 readObject() 方法内部,Java 会:

创建新对象(通过反射,不调用构造函数)

检查是否有 readResolve() 方法

如果有,就用 readResolve() 的返回值替换新创建的对象

此时有两种解决方法:

  1. 通过实现readResolve方法,返回单例。
1
2
3
private Object readResolve() {
return getInstance();
}
  1. 声明单一元素的枚举类。这也是Effective Java中提到的第三种方法。这样的写法或许有些不自然,但确实是最佳方案。
    1
    2
    3
    4
    public enum Elvis { 
    INSTANCE; // 这实际上是一个 public static final Elvis INSTANCE
    public void leaveTheBuilding() { ... }
    }

补充说明,事实上在spring框架中,@Autowired注释注入的实例是默认单例的。但是可以通过@Component(scope=”…”)声明别的类型。

既然在讨论创建和销毁对象,那么最性能的方案肯定是少创建、少销毁。

做个实验

1
2
3
String a = new String("a");
String b = new String("a");
System.out.println(a==b);

毫无疑问是false。

但是

1
2
3
String a = "a";
String b = "a";
System.out.println(a==b);

这个就是true,说明JVM会帮我们自动做优化。

当然这种错误一般都不会犯,更常见的是自动装箱拆箱机制导致多余实例被创建。

再次做个实验,这是一个计算所有正整数之和的函数

1
2
3
4
5
6
private static long sum() { 
Long sum = 0L;
for (long i = 0; i <= Integer.MAX_VALUE; i++)
sum += i;
return sum;
}

这个程序的结果是正确的,但是由于将变量sum声明为Long而非long,导致程序构造了大约213个不必要的Long实例。个人实验中,两者分别用了481ms和4405ms。

回收对象

作为一个有GC的语言,回收对象应该不用我们操心才对,但在某些情况下还是会有内存泄漏的问题。

要理解这点需要先理解JVM的垃圾回收机制。有空了单独开一篇博客讲讲JVM吧。

主要是在使用容器时(比如Map,List),当不再用到某个索引时,应当将对应引用声明为null,以触发垃圾自动回收。
还有就是当打开某个资源时,例如:

1
2
3
4
5
FileInputStream fio = null;
try {
fio = new FileInputStream(new File("file.txt"));
...
}catch (Exception e) {}

就容易出现忘记close的情况,哪怕记得也要再写一个try-catch,十分不便。这时就建议使用try-with-resource机制。

1
2
3
try(FileInputStream fio = new FileInputStream(new File("file.txt"))){
...
}catch (Exception e) {}

这种写法中创建的fio对象,会在try-with-resource结束后自动调用close()方法。

第八节避免使用Finalizer和Cleaner机制。从来没用过,甚至没听说过,没啥感觉。

不可实例化

对于一个设计为非实例化的类,即使在没有显示构造方法的情况下,编译器也会自动提供一个公共的(public),无参的默认构造方法。

试图通过创建抽象类来强制执行非实例化是行不通的。该类可以被继承,而子类可以被实例化。此外,还会误导调用者认为该类是为继承而设计。

只有当类不包含显式构造方法时,才会生成一个默认构造方法,因此可以通过包含一个私有构造方法来实现非实例化

1
2
3
4
5
6
7
8
// 不可实例化类
>public class NoninstantiableClass {
>// 私有构造方法
private UtilityClass() {
throw new Exception();
>}
>...
>}

其中抛出异常是非必要的,但可以防止在类中意外的调用构造方法,以保证在任何情况下都不会被实例化。但此种用法还是有点违反直觉,好像构造方法反而使它不能被实例化了。而且还会导致子类不可实例化,因为所有的构造方法都必须显式或隐式地调用父类构造方法,而此父类并没有构造方法可以调用。