Effective Java:01 创建和销毁对象

Demon.Lee 2024年04月06日 508次浏览

何时以及如何创建对象,何时以及如何避免创建对象,如何确保它们能够适时地销毁,以及如何管理对象销毁之前必须进行的各种清理动作。


01 用静态工厂方法代替构造器

为方面理解这条,我们先上代码:

public class BigInteger extends Number implements Comparable<BigInteger> {
    // ...
    // ...
  
    /**
     * Constructs a randomly generated positive BigInteger that is probably
     * prime, with the specified bitLength.
     * ...
     */
    public BigInteger(int bitLength, int certainty, Random rnd) {
        BigInteger prime;

        if (bitLength < 2)
            throw new ArithmeticException("bitLength < 2");
        prime = (bitLength < SMALL_PRIME_THRESHOLD
                                ? smallPrime(bitLength, certainty, rnd)
                                : largePrime(bitLength, certainty, rnd));
        signum = 1;
        mag = prime.mag;
    }

    // ...
    // ...

    /**
     * Returns a positive BigInteger that is probably prime, with the
     * specified bitLength. The probability that a BigInteger returned
     * by this method is composite does not exceed 2<sup>-100</sup>.
     * ...
     */
    public static BigInteger probablePrime(int bitLength, Random rnd) {
        if (bitLength < 2)
            throw new ArithmeticException("bitLength < 2");

        return (bitLength < SMALL_PRIME_THRESHOLD ?
                smallPrime(bitLength, DEFAULT_PRIME_CERTAINTY, rnd) :
                largePrime(bitLength, DEFAULT_PRIME_CERTAINTY, rnd));
    }

    // ...
    // ...
}

如上所示,获得一个可能为素数的 BigInteger 实例,有两种方式:直接使用构造器 new BigInteger(int bitLength, int certainty, Random rnd),或使用静态工厂方法 BigInteger.probablePrime(int bitLength, Random rnd) 。哪种方式更好呢?很显然,静态工厂方法,因为它能表达意图:不用看方法注释,一看名字就知道是做什么。而构造器显然没有这种自描述的能力,尤其是有很多个构造器的时候。

除此之外,使用静态工厂方法创建对象还有一个很大的优势是——灵活性

  • 每次都创建新对象?还是复用已有的对象?
  • 返回的对象类型,可以是当前对象,也可以是子对象。没错,多态开始起作用了;
  • 根据参数的不同,可以控制返回的对象实例,比如不同的子类;
  • 返回对象所属的类,甚至在编写该静态工厂方法时可以不存在,这就是 Service Provider Framework 机制。

除了最后一条,前面几条都非常好理解,这里就不做赘述。关于 Service Provider Framework 机制,笔者特地梳理了一篇总结

说完优点,再看看不足之处:不易被编程人员发现。

因为创建对象时,大家首先想到的都是构造器。如果把构造器的权限设置为 private 呢,子类化会受到影响。那改成 protected 岂不是最佳方案?不一定,除非增加代码注释,否则会引起误会。因为 protected 的隐喻是,让子类调用,而不是外部。这并不能代表:请不要使用构造器创建实例,而要使用我提供的静态工厂方法。有什么法子可以缓解这种境况?一个好名字或许能发挥一些余力,下面是一些行业实践:

  • from :类型转换,单个参数,比如:LocalDateTime dateTime = LocalDateTime.from(temporal);
  • of :聚合方法,多个参数,比如:List<String> list = List.of("a", "b", "c");
  • valueOf :对 fromof 的一种替代方式,比如:Long value = Long.valueOf("100000");
  • instance or getInstance :根据参数获取对象实例,比如:NumberFormat numberFormat = NumberFormat.getInstance();
  • create or newInstance :与上面一条相似,但每次都会创建一个新实例,比如:Object stringArr = Array.newInstance(String.class, 2);
  • getType :与 getInstance 类似,Type 表示所返回的对象类型,比如:FileStore fs = Files.getFileStore(filePath);
  • newType :与 newInstance 类似,比如:BufferedReader br = newBufferedReader(path);
  • typegetTypenewType 的简化版,比如:List<Complaint> list = Collections.list(legacyLitany);

02 遇到多个构造器参数时要考虑使用构建器

这也是一个比较常见的使用技巧。当每次的构造器参数都有可能不同时,如果将这些构造器都创建出来,让人搞不清楚这些构造器的意图是什么。为了应对这种情况,有的人就选择使用 setter 方法,比如:

Xxx xxx = new Xxx();
xxx.setX1("x1");
xxx.setX2(2);
xxx.setX3(true);
//...

但这种方式也有其不足,那就是数据一致性。本来需要 5 个参数,使用 setter 方法后,忘了某一两个,毕竟没有地方提示你:此处需要 5 个参数。针对这种场景,构建器模式就被提了出来,简单来说就是构造一个中间层进行数据拼装,然后再转成实际需要的类,比如:

public class JoinQueryTableParams {

    private String tableName;
    private List<String> fields;
    private ConditionGroup conditions;
    private DBTableRelationType joinType;
    private String joinColumn;
    private String refJoinColumn;

    public JoinQueryTableParams(String tableName, List<String> fields, ConditionGroup conditions, DBTableRelationType joinType, String joinColumn, String refJoinColumn) {
        this.tableName = tableName;
        this.fields = Optional.ofNullable(fields).orElse(Collections.emptyList());
        this.conditions = Optional.ofNullable(conditions).orElse(ConditionGroup.empty());
        this.joinType = joinType;
        this.joinColumn = joinColumn;
        this.refJoinColumn = refJoinColumn;
    }

    public static JoinQueryTableParamsBuilder builder() {
        return new JoinQueryTableParamsBuilder();
    }

    public static class JoinQueryTableParamsBuilder {
        private String tableName;
        private List<String> fields;
        private ConditionGroup conditions;
        private DBTableRelationType joinType;
        private String joinColumn;
        private String refJoinColumn;

        JoinQueryTableParamsBuilder() {
        }

        public JoinQueryTableParamsBuilder tableName(String tableName) {
            this.tableName = tableName;
            return this;
        }

        public JoinQueryTableParamsBuilder fields(List<String> fields) {
            this.fields = fields;
            return this;
        }

        public JoinQueryTableParamsBuilder conditions(ConditionGroup conditions) {
            this.conditions = conditions;
            return this;
        }

        public JoinQueryTableParamsBuilder joinType(DBTableRelationType joinType) {
            this.joinType = joinType;
            return this;
        }

        public JoinQueryTableParamsBuilder joinColumn(String joinColumn) {
            this.joinColumn = joinColumn;
            return this;
        }

        public JoinQueryTableParamsBuilder refJoinColumn(String refJoinColumn) {
            this.refJoinColumn = refJoinColumn;
            return this;
        }

        public JoinQueryTableParams build() {
            return new JoinQueryTableParams(this.tableName, this.fields, this.conditions, this.joinType, this.joinColumn, this.refJoinColumn);
        }
    }
}

上面代码示例中的 JoinQueryTableParamsBuilder 就是笔者提到的中间层,当我们使用 JoinQueryTableParams.builder() 时,便会创建 JoinQueryTableParamsBuilder 实例,再调用 build() 方法,则会调用 JoinQueryTableParams 类的构造器,即创建我们需要的类实例。前面提到 setter 方法无法保证数据的一致性,比如进行参数校验等,那么现在呢?从上面的创建过程不难发现,我们完全可以在 JoinQueryTableParams 类的构造器中处理,保证数据合法、一致等。

另外,Builder 模式也适用于类层次结构,也就是说在子类继承父类时,也可以使用这种方式,将父类的属性通过 builder 模式进行构建。但父子类使用 builder 模式可能没上面那么容易,但我们可以借助 lombook 类库中的 @SuperBuilder 注解。通过将其放在父子类上,然后查阅生成的代码,其内部逻辑便一目了然了。下面 QueryTableParamsPageQueryTableParams 中的 builder 代码便是通过这种方式获得的:

// 父类
public class QueryTableParams {
    private String tableName;
    private List<String> fields;
    private ConditionGroup conditions;

    protected QueryTableParams(QueryTableParamsBuilder<?, ?> b) {
        this.tableName = b.tableName;
        this.fields = b.fields;
        this.conditions = b.conditions;
    }

    public static QueryTableParamsBuilder<?, ?> builder() {
        return new QueryTableParamsBuilderImpl();
    }

    private static final class QueryTableParamsBuilderImpl extends QueryTableParamsBuilder<QueryTableParams, QueryTableParamsBuilderImpl> {
        private QueryTableParamsBuilderImpl() {
        }

        protected QueryTableParamsBuilderImpl self() {
            return this;
        }

        public QueryTableParams build() {
            return new QueryTableParams(this);
        }
    }

    public abstract static class QueryTableParamsBuilder<C extends QueryTableParams, B extends QueryTableParamsBuilder<C, B>> {
        private String tableName;
        private List<String> fields;
        private ConditionGroup conditions;

        public QueryTableParamsBuilder() {
        }

        protected abstract B self();

        public abstract C build();

        public B tableName(String tableName) {
            this.tableName = tableName;
            return this.self();
        }

        public B fields(List<String> fields) {
            this.fields = fields;
            return this.self();
        }

        public B conditions(ConditionGroup conditions) {
            this.conditions = conditions;
            return this.self();
        }
    }
}


// 子类
public class PageQueryTableParams extends QueryTableParams {

    private int pageNum;
    private int pageSize;

    protected PageQueryTableParams(PageQueryTableParamsBuilder<?, ?> b) {
        super(b);
        this.pageNum = b.pageNum;
        this.pageSize = b.pageSize;
    }

    public static PageQueryTableParamsBuilder<?, ?> builder() {
        return new PageQueryTableParamsBuilderImpl();
    }

    private static final class PageQueryTableParamsBuilderImpl extends PageQueryTableParamsBuilder<PageQueryTableParams, PageQueryTableParamsBuilderImpl> {
        private PageQueryTableParamsBuilderImpl() {
        }

        protected PageQueryTableParamsBuilderImpl self() {
            return this;
        }

        public PageQueryTableParams build() {
            return new PageQueryTableParams(this);
        }
    }

    public abstract static class PageQueryTableParamsBuilder<C extends PageQueryTableParams, B extends PageQueryTableParamsBuilder<C, B>> extends QueryTableParams.QueryTableParamsBuilder<C, B> {
        private int pageNum;
        private int pageSize;

        public PageQueryTableParamsBuilder() {
        }

        protected abstract B self();

        public abstract C build();

        public B pageNum(int pageNum) {
            this.pageNum = pageNum;
            return this.self();
        }

        public B pageSize(int pageSize) {
            this.pageSize = pageSize;
            return this.self();
        }
    }
}

书中说:子类方法声明返回父类中声明的返回类型的子类型,这被称作协变返回类型(covariant return type)。它允许客户端无须转换类型就能使用这些构建器。笔者倒觉得无需去记这些概念,通过一个使用样例把上面的实现串起来,这其中多态的使用技巧也能理解个大概。

最后提醒一下,Builder 模式比重叠构造器模式更加冗长,因此它只在有很多参数的时候才使用,比如4个或者更多个参数


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

Singleton 模式在 Java 中的使用非常多,比如 Spring 框架之中。但如果从单元测试的角度来看,它并不是一个好模式,因为我们没法 Mock 它。没法 Mock 的意思是:无法改变该对象实例内部的状态和行为,即无法改变属性的值或是方法的逻辑。

关于单例模式,有懒汉,饿汉,双重检测,枚举等多种实现方式,笔者整理了几个,提交在这里:Github Singleton。如果消耗的性能不高,大多数情况下,我们可以选择最简单的饿汉模式,比如:

public class IdGeneratorStarve {

    private AtomicLong id = new AtomicLong();

    private static final IdGeneratorStarve instance = new IdGeneratorStarve();

    private IdGeneratorStarve() {
    }

    public static IdGeneratorStarve getInstance() {
        return instance;
    }

    public long generateId() {
        return id.getAndIncrement();
    }
}

但即使这样,也不是百分百安全,因为可以使用反射这个后门绕过私有构造器,比如:

public class IdGeneratorStarveTest {

    @Test
    void should_create_more_id_generator() throws NoSuchMethodException, InvocationTargetException, InstantiationException, IllegalAccessException {
        Constructor constructor = IdGeneratorStarve.class.getDeclaredConstructor();
        constructor.setAccessible(true);
        IdGeneratorStarve idGeneratorStarve = (IdGeneratorStarve) constructor.newInstance();
        Assertions.assertNotEquals(IdGeneratorStarve.getInstance(), idGeneratorStarve);
    }
}

如果想避免这个问题,可以增加校验,即不允许创建第二个实例,比如:

public class IdGeneratorStarve {

    private AtomicLong id = new AtomicLong();
    private static boolean instanceCreated = false;
    private static final IdGeneratorStarve instance = new IdGeneratorStarve();

    private IdGeneratorStarve() {
        if (instanceCreated) {
            throw new IllegalStateException("cannot create new IDGeneratorStarve instance for singleton");
        }
        instanceCreated = true;
    }

    public static IdGeneratorStarve getInstance() {
        return instance;
    }

    public long generateId() {
        return id.getAndIncrement();
    }
}

但在序列化的场景下,这种写法还是有问题,因为反序列化依然会创建新对象,比如:

public class IdGeneratorStarveTest {

    @Test
    void should_create_serializable_id_generator() {
        String dataName = "id-generator.data";
        try (ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream(dataName));
             ObjectInputStream ois = new ObjectInputStream(new FileInputStream(dataName))) {
            oos.writeObject(IdGeneratorStarve.getInstance());
            IdGeneratorStarve idGeneratorStarve = (IdGeneratorStarve) ois.readObject();
            Assertions.assertNotEquals(idGeneratorStarve, IdGeneratorStarve.getInstance());
            Assertions.assertNotEquals(idGeneratorStarve.getId(), IdGeneratorStarve.getInstance().getId());
        } catch (Exception ex) {
            throw new RuntimeException(ex);
        } finally {
            try {
                Files.deleteIfExists(Path.of(dataName));
            } catch (IOException e) {
                log.error("delete file failed: ", e);
            }
        }
    }
}

为此,我们需要将所有实例域都声明为 transient ,并提供一个 readResolve() 方法:

public class IdGeneratorStarve implements Serializable {

    // `transient` for id
    private transient AtomicLong id = new AtomicLong();
    private static boolean instanceCreated = false;
    private static final IdGeneratorStarve instance = new IdGeneratorStarve();

    private IdGeneratorStarve() {
        if (instanceCreated) {
            throw new IllegalStateException("cannot create new IDGeneratorStarve instance for singleton");
        }
        instanceCreated = true;
    }

    public static IdGeneratorStarve getInstance() {
        return instance;
    }

    public long generateId() {
        return id.getAndIncrement();
    }

    public AtomicLong getId() {
        return id;
    }

    // return `instance` by `readResolve()`
    private Object readResolve() {
        return instance;
    }
}

如果想再简化一下上面的使用方式,可以将 instance 调整为 public-field ,然后移除 getInstance() 方法,即:

public static final IdGeneratorStarve instance = new IdGeneratorStarve();

由于 getInstance() 方法是一个静态方法,它相比直接使用公有的属性域做了一定的封装:在不改变 API 的前提下,我们随时改变 Singleton 的假设,从而增加了代码的灵活性。

最后,如果想避开前面提到的各种安全问题,最简单的方式是使用枚举:

public enum IdGeneratorEnum implements Serializable {

    INSTANCE;

    private AtomicLong id = new AtomicLong();

    IdGeneratorEnum() {
    }

    public long generateId() {
        return id.getAndIncrement();
    }

    public IdGeneratorEnum getInstance() {
        return INSTANCE;
    }

    public AtomicLong getId() {
        return id;
    }
}

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

有时可能需要编写只包含静态方法和静态域的类。这些类的名声很不好,因为有些人在面向对象的语言中滥用这样的类来编写过程化的程序,但它们也确实有特别的用处。

我们经常会使用一些工具类,这些工具类里面都是静态方法和静态域,比如集合工具类:java.util.Collections。既然是各种静态方法,所以这样的类不需要实例化。但我们往往不会在自己的工具类中增加构造器,这就会导致这些工具类都会有一个默认公有的无参构造器,从而让这些类有了实例化和子类化的可能。解决方法也很简单,增加一个私有构造器即可,比如:

public class Collections {
    // Suppresses default constructor, ensuring non-instantiability.
    private Collections() {
    }

    // ...
    private static final int BINARYSEARCH_THRESHOLD   = 5000;
    private static final int REVERSE_THRESHOLD        =   18;
    private static final int SHUFFLE_THRESHOLD        =    5;
    private static final int FILL_THRESHOLD           =   25;
    private static final int ROTATE_THRESHOLD         =  100;
    private static final int COPY_THRESHOLD           =   10;
    private static final int REPLACEALL_THRESHOLD     =   11;
    private static final int INDEXOFSUBLIST_THRESHOLD =   35;

    // ...
    public static <T extends Comparable<? super T>> void sort(List<T> list) {
        list.sort(null);
    }

为了让代码具备更多的可读性,可以像上面的代码那样,增加一行注释:Suppresses default constructor, ensuring non-instantiability.


05 优先考虑依赖注入来引用资源

不要用 Singleton 和静态工具类来实现依赖一个或多个底层资源的类,且该资源的行为会影响到该类的行为 ;也不要直接用这个类来创建这些资源。而应该将这些资源或者工厂传给构造器(或者静态工厂,或者构建器),通过它们来创建类。这个实践就被称作依赖注入,它极大地提升了类的灵活性,可重用性和可测试性。

依赖注入,又被称为控制反转,可能很多小伙伴在使用 Spring 这类框架后才接触到:

但其实,像下面这样,把 dictionary 作为构造器参数传入给 SpellChecker,就是依赖注入。因为 SpellChecker 所依赖的资源是通过外部传入的,而不是自产自销,这大大增加了业务逻辑的灵活性。

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){
    // ...
  }
}

06 避免创建不必要的对象

  • String 类的实例不要调用构造器,直接赋值使用即可,即:String s = "abc";
  • 有静态工厂方法时,优先使用静态工厂方法,而不是构造器(原因前文已经说过了);
  • 优先使用基本类型而不是基本类型的装箱类型;
  • 如果需要创建的对象很昂贵,应该将其缓存下来重用。

虽然要避免创建不必要的对象,但:

小对象的构造器只做很少量的显式工作,所以小对象的创建和回收动作是非常廉价的,特别是在现代的 JVM 实现上更是如此。通过创建附加的对象,提升程序的清晰性、简洁性和功能性,这通常是件好事。


07 消除过期的对象引用

清空对象引用应该是一种例外,而不是一种规范行为。消除过期引用最好的方法是让包含该引用的变量结束其生命周期。

书中提到了三种内存泄漏的情况,更多内容,笔者将在后续额外补充一篇总结,特别是涉及到 WeakHashMap

  • 集合中(最常见的是栈)引用了很多对象,但集合伸缩时没有手动回收这些对象;
  • 缓存池,其实跟上面的集合有点像,对象放进去之后,很容易被我们忘记;
  • 监听器和回调,如果你实现了一个API,客户端在这个API中注册回调,却没有显式地取消注册,那么除非你采取某些动作,否则它们就会不断地堆积起来。

08 避免使用终结方法和清除方法

Java语言规范不仅不保证终结方法或者清除方法会被及时地执行,而且根本就不保证它们会被执行。

Cleaner 规范指出:“清除方法在 System.exit 期间的行为是与实现相关的。不确保清除动作是否会被调用。”

总而言之,除非是作为安全网,或者是为了终止非关键的本地资源,否则请不要使用清除方法,对于在 Java 9 之前的发行版本,则尽量不要使用终结方法。

简单来说就是忘记这两个特性,笔者也曾总结过一篇:Java 之 finalize 终结


09 try-with-resources 优先于 try-finally

Java 7 提供的语法糖,不仅解决了繁琐的资源关闭问题,同时能将这中间多个异常打印到堆栈轨迹之中,不会出现后面异常覆盖前面异常的情况。语法糖的意思是:编译器会帮我们生成原生的代码,即那些繁杂的逻辑:

public class TryWithResourcesTest {

    private void test() throws IOException, ClassNotFoundException {
        String dataName = "a.txt";
        try (ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream(dataName));
             ObjectInputStream ois = new ObjectInputStream(new FileInputStream(dataName))) {
            oos.writeObject(IdGeneratorEnum.INSTANCE);
            IdGeneratorEnum idGeneratorEnum = (IdGeneratorEnum) ois.readObject();
            System.out.println("idGeneratorEnum = " + idGeneratorEnum);
        }
    }
}

将上面的代码进行编译,再使用 Intellij IDEA 打开对应的 class 文件,便能看到资源的关闭以及 addSuppressed() 方法的使用。

public class TryWithResourcesTest {
    public TryWithResourcesTest() {
    }

    private void test() throws IOException, ClassNotFoundException {
        String dataName = "a.txt";
        ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream(dataName));

        try {
            ObjectInputStream ois = new ObjectInputStream(new FileInputStream(dataName));

            try {
                oos.writeObject(IdGeneratorEnum.INSTANCE);
                IdGeneratorEnum idGeneratorEnum = (IdGeneratorEnum)ois.readObject();
                System.out.println("idGeneratorEnum = " + String.valueOf(idGeneratorEnum));
            } catch (Throwable var8) {
                try {
                    ois.close();
                } catch (Throwable var7) {
                    var8.addSuppressed(var7);
                }

                throw var8;
            }

            ois.close();
        } catch (Throwable var9) {
            try {
                oos.close();
            } catch (Throwable var6) {
                var9.addSuppressed(var6);
            }

            throw var9;
        }

        oos.close();
    }
}