Chapter 2 创建和销毁对象

第1条 考虑用静态工厂方法代替构造器

一个类可以提供一个公有的构造器来让客户端获取其实例,但是我们更应该考虑使用静态工厂方法来代替公有构造器。

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

对于类而言, 最常用的获取实例的方法就是提供一个公有的构造器, 还有一种方法, 就是提供一个公有的静态工厂方法(static factory method), 返回类的实例.
(注意此处的静态工厂方法与设计模式中的工厂方法模式不同.)
提供静态工厂方法而不是公有构造, 这样做有几大优势:

  • 静态工厂方法有名称. 可以更确切地描述正被返回的对象.
    当一个类需要多个带有相同签名的构造器时, 可以用静态工厂方法, 并且慎重地选择名称以便突出它们之间的区别.
  • 不必在每次调用它们的时候都创建一个新对象. 可以重复利用实例, 进行实例控制. 如果程序经常请求创建相同的对象, 并且创建对象的代价很高, 这项改动可以提升性能. (不可变类, 单例, 枚举).
  • 可以返回原类型的子类型对象. 适用于基于接口的框架, 可以隐藏实现类API, 也可以根据参数返回不同的子类型.
    由于在Java 8之前, 接口不能有静态方法, 因此按照惯例, 接口Type的静态工厂方法被放在一个名为Types的不可实例化的类中.
    (Java的java.util.Collections).
  • 返回对象的类型可以根据输入的参数而变化. 比如EnumSet类的静态工厂, 根据元素的多少返回不同的子类型.
  • 返回对象的类型不需要在写这个方法的时候就存在. 服务提供者框架(Service Provider Framework, 如JDBC)的基础, 让客户端与具体实现解耦.
    Java 6开始提供了java.util.ServiceLoader.
    静态工厂方法的缺点:
  • 类如果不含public或者protected的构造器, 就不能被子类化. (鼓励程序员: 组合优于继承).
  • 不容易被程序员发现, 因为静态工厂方法与其他的静态方法没有区别. 在API文档中没有像构造器一样明确标识出来. 可以使用一些惯用的名称来弥补这一劣势:
    • from: 类型转换方法.
    • of: 聚集方法, 参数为多个, 返回的当前类型的实例包含了它们.
    • valueOf: 类型转换方法, 返回的实例与参数具有相同的值.
    • instancegetInstance: 返回的实例通过参数来描述(并不是和参数有一样的值). 对于单例来说, 该方法没有参数, 返回唯一的实例.
    • createnewInstance: 像getInstance一样, 但newInstance能确保返回的每个实例都与其他实例不同.
    • getType: 和getInstance一样, Type表示返回的对象类型, 在工厂方法处于不同的类中的时候使用.
    • newType: 和newInstance一样, Type表示返回的对象类型, 在工厂方法处于不同的类中的时候使用.
    • type: getTypenewType的简洁替代.

第2条 遇到多个构造器参数时要考虑用Builder

相信在我们的日常开发中,肯定遇到过一个场景,那就是一个类的构造器中有多个参数。有些参数是必选的,其余则是可选的。这种情况下,我们一般使用重叠构造器模式:即创建了若干个构造器,构造器之间进行相互调用。
重叠构造器模式是我们最常见的一种写法,但是我们应该考虑builder模式,即:不直接生成想要的对象,而是让客户端利用所有必要的参数调用构造器,得到一个builder对象,然后客户都在builder对象上调用类似于setter的方法,来设置每个可选的参数。

第3条 用私有构造器或者枚举类型强化Singleton属性

这一条主要讲述了如何更加优雅高效的实现单例模式,那就是使用枚举,你需要编写一个包含单个元素的枚举类型。

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

比如一些工具类,我们提供了静态方法,不希望这个类被私有化。很简单,我们可以将构造器私有化。产生的副作用:该类不可以被子类化。因为所有的构造器都必须显示或者隐示的调用超类构造器。

第5条 优先使用依赖注入而不是直接绑定资源

对于其行为由底层资源参数化的类(比如SpellChecker, 底层资源是dictionary), 静态辅助类和单例都是不合适的实现方式.
一个简单的模式是在创建新实例的时候, 通过构造函数传入资源.
依赖注入(dependency injection): 依赖(dictionary)在spell checker被创建的时候注入(injected).
依赖注入适用于: 构造函数, 静态工厂, builder模式.
优点: 灵活, 复用, 易于测试.
一个有用的变种: 将资源工厂传入构造函数.
依赖注入的framework: Dagger, Guice, Spring.

第6条 避免创建不必要的对象

一般来说, 最好能重用对象而不是每次需要的时候创建一个相同功能的新对象.
如果对象是不可变的(immutable), 它就始终可以被重用.
比如应该用:

String s = "bikini";

而不是:

String s = new String("bikini"); // Don't do this

包含相同字符串的字面常量对象是会被重用的(同一个虚拟机).
再来看一个关于创建无用对象所带来的问题。我们都知道自动装箱/拆箱,这个功能使得基本类型和装箱基本类型之间的差别变得模糊起来。但是,还是有差别的,案例如下:

public class AutoBoxTest {
    public static void main(String[] args) {
        long startTime = System.currentTimeMillis();
        Long sum = 0L;
        for (int i = 0; i < Integer.MAX_VALUE; i++) {
            sum +=i;
        }
        System.out.println("时间差1:"+(System.currentTimeMillis()-startTime));

        long startTime2 = System.currentTimeMillis();
        long sum2 = 0;
        for (int i = 0; i < Integer.MAX_VALUE; i++) {
            sum2 +=i;
        }
        System.out.println("时间差2:"+(System.currentTimeMillis()-startTime2));
    }

}

运行结果如下

图片说明

在上边的计算中,我们先定义了Long sum = 0L 导致,程序构造了大约2的31次方个多余的Long实例。当我们定义了long sum = 0L之后,减少了无用对象的创建,计算时间从7150ms减少到了593ms。

结论:优先使用基本类型而不是装箱基本类型,要当心无意识的自动装箱。
这里并不是告诉大家创建对象的代价很昂贵,相反,由于小对象的构造器只做很少量的显示工作。所以小对象的创建和回收动作是非常廉价的。通过创建附加的对象,提升程序的清晰性,简洁性和功能性,这通常是件好事。

第7条 消除过期的对象引用

我们先来看一个简单的自定义栈的实现:

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);
        }
    }

}

实际上,这段程序中并没有很明显的错误。无论如何测试,它都会成功地运行通过每一项测试,但这个程序中隐藏着一个问题。不严格地讲,这段程序有一个”内存泄漏“, 随着垃圾回收器活动的增加,或者由于内存占用的不断增加,程序性能的降低会逐渐表现出来。在极端的情况下,这种内存泄露会导致磁盘交换,甚至程序失败,但这种情况比较少见。
在我们的stack例子中,凡是在elements数组的”活动范围“之外的任何引用都是过期的,这里的活动部分指的是elements中下标小于size的那些元素。内存泄漏发生在pop方法中。如何解决stack中的过期引用问题?一旦对象引用已经过期,我们只需清空这些引用即可。

public Object pop(){
        if (size == 0){
            throw new EmptyStackException();
        }
        Object result = elements[--size];
        //清空引用
        elements[size] = null;
    }

第8条 避免使用终结方法和清理器

终结方法(finalizer)通常是不可预测的,也是很危险的,一般情况下是不必要的。使用终结方导致行为不稳定,降低性能等问题。Java语言规范不仅不保证终结方被及时执行,而且根本就不保证它们会被执行

第9条 优先使用try-with-resources而不是try-finally

曾经, try-finally是确保资源被关闭的最好方式, 即便是有Exception或者return也不怕.
但是要关闭多个资源, 嵌套使用的时候看起来很丑.

并且如果try和finally块中都有异常抛出, 通常第二个会掩盖了第一个.

所有的这些问题都被Java 7新添加的try-with-resources语句解决了.
要使用的话, 资源类必须实现AutoCloseable接口.

当多个异常抛出的时候, 后续异常会被suppressed, 可以通过getSuppressed()方法获取(Java 7).
try-with-resources也可以加catch语句.

总之, 推荐使用try-with-resources -> 代码更短, 更简洁, close()被隐式调用, 异常信息更有意义.

只是书籍资料的整理者,方便以后反复消化,来源:
https://blog.csdn.net/qq_25827845/article/details/85016496
https://www.cnblogs.com/mengdd/p/effective-java-reading-notes.html