阔别许久,想起之前定下的一月一博客的目标,可真够宏伟的。理由倒是有很多,但是终归是我懈怠了。但即使这样,我依然不想放弃那个目标,那就从现在开始补起来吧。算上9月、10月欠的,11月要写三篇。11月对我好一点😭。
Effective Java能被誉为Java四大名著之一,看来是有原因的。我本以为它会是一本晦涩难懂的大部头,没想到如此贴近实战,给了我很多的启发,故作读书随想。
类的实例化和非实例化
本文中讨论的对象(Object),实际上就是类的实例。
本书前9节
- 考虑使用静态工厂方法替代构造方法
- 当构造方法参数过多时使用 builder 模式
- 使用私有构造方法或枚举类实现 Singleton 属性
- 使用私有构造方法执行非实例化
- 使用依赖注入取代硬连接资源(hardwiring
resources)- 避免创建不必要的对象
- 消除过期的对象引用
- 避免使用 Finalizer 和 Cleaner 机制
- 使用 try-with-resources 语句替代 try-finally 语句
实际围绕一个主题,类的实例。一个类,总是应当被设计为可实例化和不可实例化的。其中可实例化类又分为对象的创建,对象的回收。
可实例化
对于一个被设计为可实例化的类,应当考虑何种实现是最高效的。
创建对象
创建对象时,除了传统的公共构造方法外,还可以考虑使用静态工厂方法(和设计模式中的工厂方法模式没有关系)。这点实际上是其它一切优化的基础,毕竟直接使用构造函数的限制很多。
Boolean(boolean基本类型的包装类)的简单例子
1 | public static Boolean valueOf(boolean b) { |
优点:
- 静态方法命名自由,可以描述构造行为。
- 静态方法不需要每次调用时都创建一个新对象。
- 静态方法可以返回其返回类型的任何子类型的对象。
- 参数组合清晰,返回对象的类可以根据输入参数的不同而不同。
- 容易版本化,方法可以标记过时然后删除,但构造器不方便。
- 构造器之间互相调用有顺序要求,而静态方法无限制。
缺点:
- 需要调用者知道哪些静态方法是提供构造功能的,建议辅以注释。
- 静态方法不能访问实例变量。
- 可能返回null。
当构造方法参数过多时就可以使用 builder 模式,在实践中往往使用Lombok的@Builder注解实现。
当试图创建一个单例对象时,书上给出了三种方法:
先给出前两种
1 | public class Elvis { |
1 | public class Elvis { |
前两种在系统启动后直接创建实例的方式称为饿汉式,实际上还有一种称为懒汉式。
1 | public class Elvis { |
可以看到这种方式可以在构造方法中实现一些安全措施,防止通过反射创建出对象。
但在这种情况下,如果有多个线程同时获取实例,就会出现问题。再稍作改进
1 | public class Elvis { |
当这个类实现序列化时,会出现新的问题。当对象被序列化和反序列化时,Java不会调用构造函数,而是通过反射机制直接创建对象实例。
补充一下序列化和反序列化的流程
1
2 >ObjectInputStream inputStream = new ObjectInputStream(...);
>Elvis instance = (Elvis) inputStream.readObject(); // 关键调用在 readObject() 方法内部,Java 会:
创建新对象(通过反射,不调用构造函数)
检查是否有 readResolve() 方法
如果有,就用 readResolve() 的返回值替换新创建的对象
此时有两种解决方法:
- 通过实现readResolve方法,返回单例。
1 | private Object readResolve() { |
- 声明单一元素的枚举类。这也是Effective Java中提到的第三种方法。这样的写法或许有些不自然,但确实是最佳方案。
1
2
3
4public enum Elvis {
INSTANCE; // 这实际上是一个 public static final Elvis INSTANCE
public void leaveTheBuilding() { ... }
}
补充说明,事实上在spring框架中,@Autowired注释注入的实例是默认单例的。但是可以通过@Component(scope=”…”)声明别的类型。
既然在讨论创建和销毁对象,那么最性能的方案肯定是少创建、少销毁。
做个实验
1 | String a = new String("a"); |
毫无疑问是false。
但是
1 | String a = "a"; |
这个就是true,说明JVM会帮我们自动做优化。
当然这种错误一般都不会犯,更常见的是自动装箱拆箱机制导致多余实例被创建。
再次做个实验,这是一个计算所有正整数之和的函数
1 | private static long sum() { |
这个程序的结果是正确的,但是由于将变量sum声明为Long而非long,导致程序构造了大约213个不必要的Long实例。个人实验中,两者分别用了481ms和4405ms。
回收对象
作为一个有GC的语言,回收对象应该不用我们操心才对,但在某些情况下还是会有内存泄漏的问题。
要理解这点需要先理解JVM的垃圾回收机制。有空了单独开一篇博客讲讲JVM吧。
主要是在使用容器时(比如Map,List),当不再用到某个索引时,应当将对应引用声明为null,以触发垃圾自动回收。
还有就是当打开某个资源时,例如:
1 | FileInputStream fio = null; |
就容易出现忘记close的情况,哪怕记得也要再写一个try-catch,十分不便。这时就建议使用try-with-resource机制。
1 | try(FileInputStream fio = new FileInputStream(new File("file.txt"))){ |
这种写法中创建的fio对象,会在try-with-resource结束后自动调用close()方法。
第八节避免使用Finalizer和Cleaner机制。从来没用过,甚至没听说过,没啥感觉。
不可实例化
对于一个设计为非实例化的类,即使在没有显示构造方法的情况下,编译器也会自动提供一个公共的(public),无参的默认构造方法。
试图通过创建抽象类来强制执行非实例化是行不通的。该类可以被继承,而子类可以被实例化。此外,还会误导调用者认为该类是为继承而设计。
只有当类不包含显式构造方法时,才会生成一个默认构造方法,因此可以通过包含一个私有构造方法来实现非实例化
1
2
3
4
5
6
7
8 // 不可实例化类
>public class NoninstantiableClass {
>// 私有构造方法
private UtilityClass() {
throw new Exception();
>}
>...
>}其中抛出异常是非必要的,但可以防止在类中意外的调用构造方法,以保证在任何情况下都不会被实例化。但此种用法还是有点违反直觉,好像构造方法反而使它不能被实例化了。而且还会导致子类不可实例化,因为所有的构造方法都必须显式或隐式地调用父类构造方法,而此父类并没有构造方法可以调用。