EFFECTIVE JAVA 3RD EDITION — 第二章 创建和销毁对象

Item 1 : using static factory method instead of constructor

多使用静态工厂方法而不是构造器

优势:

  • 构造器容易造成参数顺序混乱,静态工厂方法名称有很好的表意性,对于类属性较多的情况有着非常明显的优势
  • 静态工厂方法都不必每次调用都创建新的对象,对于创建开销很大的对象或者同一个对象反复调用来说有性能提升(实现instance-controlled)
    instance-controlled classes(Flyweight Pattern):
    1. 保证是一个单例或者不可实例化
    2. 保证不存在两个完全相同的实例
  • 静态工厂方法返回对象灵活,可以是自己或者是子对象
  • 静态工厂方法可以根据参数返回不同的对象(自身或者是子对象,或者说是不同的实现)
  • 静态工厂方法返回的对象在方法所在的类写入之前都可以不用存在

不足:

  • 只提供静态工厂方法,由于没有公共或者保护的构造器无法被继承,扩展性有限
  • 开发人员很难注意到静态工厂方法,java文档中构造器的文档比静态工厂方法更显眼(可以忽略)

常用静态工厂方法命名:

  1. from 单参数的类型转换
  2. of 多参数聚合转换
  3. valueOf from和of更详细的转换
  4. instance or getInstance 有可能返回相同的实例
  5. create or newInstance 返回新实例
  6. getType 同getInstance,如果工厂方法定义在另一个类中
  7. newType 同上
  8. type 同上

Item 2 : considering a builder when faced with many constructor parameters

构造器参数过多时考虑使用builder模式

builder模式在Java 9中其实使用非常多,比如介绍 Java9 HTTP2新特性 里面HttpClient HttpRequest等等的实例化方式都是builder模式。

静态工厂方法和构造器在面对大量可选参数时候的扩展性非常不好:

  • 普通伸缩构造模式在参数多了的情况下调用代码难于编写和阅读
    想象一下当我new 一个对象需要传入超过5个参数的时候,根本不知道参数先后顺序,还要看文档,是多么痛苦的一件事情。
  • get,set方法过于啰嗦,虽然解决了上面的不足,但是在构造对象的过程中对象可能已经被改变,导致线程不安全,这是JavaBeans模式的一个短板
    当一个类有超过10个属性的时候使用get,set方法简直就是一场灾难。

比如你可以像下面一样实现builder模式:

public class Human {
 
    private String name;
 
    private Integer id;
 
    private char sex;
 
    private Date birthday;
 
    private Double height;
 
    private Double weight;
 
    private String motto;
 
    public static class Builder {
        private String name;
 
        private Integer id;
 
        private char sex;
 
        private Date birthday;
 
        private Double height;
 
        private Double weight;
 
        private String motto;
 
        public Builder name(String name) {
            this.name = name;
            return this;
        }
        public Builder id(Integer id) {
            this.id = id;
            return this;
        }
        public Builder sex(char sex) {
            this.sex = sex;
            return this;
        }
        public Builder birthday(Date birthday) {
            this.birthday = birthday;
            return this;
        }
        public Builder height(Double height) {
            this.height = height;
            return this;
        }
        public Builder weight(Double weight) {
            this.weight = weight;
            return this;
        }
        public Builder motto(String motto) {
            this.motto = motto;
            return this;
        }
        public Human build() {
            return new Human(this);
        }
    }
    private Human(Builder builder) {
        name = builder.name;
        id = builder.id;
        sex = builder.sex;
        birthday = builder.birthday;
        height = builder.height;
        weight = builder.weight;
        motto = builder.motto;
    }
}

构造一个Human:

Human.Builder builder = new Human.Builder();
builder.id(1).name("Tom").birthday(new Date()).height(1.8).weight(60.0).motto("Hello World");
builder.build();

builder模式的优缺点:

  1. 更加灵活,安全,对参数可以做更多的处理,调用代码可读性好
  2. 在性能要求极端的场景下builder创建所消耗的性能是不被容忍的
  3. 从传统的get,set或者构造方法迁移的时候代码量大(删除旧代码),所以最好一开始就使用builder模式

使用场景:

  • 参数过多难以处理,特别是可选参数过多时
  • 安全

Item 3 : making singleton property private constructor or enum type

用枚举代替单例属性或者给它提供一个私有构造方法

构造一个单例:

  • 声明一个公共静态final属性,无参构造方法私有
    优点是api清晰,属性是final的表明其是个单例
  • 声明一个私有静态final属性,提供一个公共静态获取实例方法返回该属性,同样无参构造方法私有
    优点是灵活性,返回的实例可以根据需要修改,不用修改api来决定是否是单例;
    可以写一个通用的单例工厂方法,如果需要的话;
    可以作为一个 Supplier 比如:
    Supplier supplier = SingletonEnforce::getInstance;
    supplier.get();
    

但是上面第二种方法有一定的危险,因为私有的构造方法也有可能被调用:

Constructor constructor = Class.forName("me.minei.effective.SingletonEnforce").getDeclaredConstructor();
constructor.setAccessible(true);
constructor.newInstance();

这种情况在私有构造方法里判断是否实例化过,已经实例化抛出异常即可。

声明一个单元素枚举,通常这是最好的方法来实现单例,如果要扩展一个非枚举子类的话就不用这种方式实现:

public enum SingletonEnum {
    INSTANCE;
}

使用前两种方式实现的单例如果需要序列化的话不光需要实现 Serializable 接口,同时所有实例属性都需要加 transient 关键字和提供一个私有的 readResolve 方法:

private Object readResolve() {
    return INSTANCE;
}

否则每次反序列化时都会有新的实例创建。

Item 4 : Enforce noninstantiability with a private constructor

给不能实例化对象强制实现一个私有构造方法

对于不需要实例化的类提供一个私有构造器(里面抛出异常等等处理)来避免被实例化,通常是对于一些接口,使接口抽象是不可取的因为可以被继承然后实例化。

这样做不好的一点就是无法被继承。

Item 5 : Prefer dependency injection to hardwiring resources

优先依赖注入底层资源

静态聚合工具类(接口)和单例不适合依赖参数实例化的场景,简单说就是不要用单例或者是静态聚合类(接口)去实现一个依赖除自身的类,
并且不要用类直接去创建这些资源,传递给构造器(builder工厂方法)去处理,提升类的灵活性、可重用性和可测试性。

这种情况下需要将参数传递给构造器来实例化(依赖注入模式)
这种方式的一个变种是传入一个 factory 给构造器,这个 factory 可以重复的返回对象实例(Factory Method pattern)

Java8中的 Supplier 就是 factory 一个很好的代表。
传入构造器的最好是一个通配的类型以便于扩展:

Mosaic create(Supplier<? extends Tile> tileFactory) { ... }

Item 6 : Avoid creating unnecessary objects

避免创建不必要的对象

  • 适当的重用对象
  • 避免创建无用的和重复的对象
  • 可通过静态工厂方法来避免创建不必要的对象
  • 有时候在调用工厂方法的时候不希望某个final属性被初始化而是当调用涉及到的方法的时候才初始化(后面会有提到),这样做性能提升不明显
  • 多使用基本数据类型,小心隐藏的自动拆装箱带来的性能损耗
    比如下面的代码:
    	private static long sum() {
            Long sum = 0L;
            for (long i = 0; i <= Integer.MAX_VALUE; i++)
                sum += i;
            return sum;
    }
    

    如果上面sum改成long性能会提升巨大

Item 7 : Eliminate obsolete object references

及时处理过期对象引用

  • 无意的对象引用保留可能会造成gc无法回收,内存泄露,无用对象直接置空即可(null)
  • 不管任何时候,只要是一个类自己管理内存,程序员都要小心内存泄露(数组等等)

缓存可能造成内存泄漏(无用对象没有及时被回收)

  1. 使用WeakHashMap
  2. 启用一个后台进程去清理过期缓存

监听器和其他回调可能导致内存泄露

  1. 可以使用 weak references 将其放入 WeakHashMap

内存泄露往往不是立刻发生,而是可能潜伏在系统多年,只有发生了才会体现出来,可以使用 http://goog-perftools.sourceforge.net/doc/heap_profiler.html 来帮助分析

Item 8 : Avoid finalizers and cleaners

避免使用 finalizer 和 cleaner

  • Java9中finalizers被cleaners替代,finalizer和cleaner不能保证被及时执行
  • 绝对不要在finalizer和cleaner中做一些关键操作,比如关闭文件
  • 不要依赖finalizer和cleaner修改一些持久状态,比如释放公共资源的锁
    System.gc和System.runFinalization两个方法虽然会增加finalizer和cleaner被执行的几率但是并不能保证,
    只保证System.runFinalizersOnExit和Runtime.runFinalizersOnExit肯定被执行,但是后面的两种方法已经被弃用。
  • finalization过程中抛出的未捕获异常被忽略,导致对象的finalization终止,cleaner没有这个问题因为cleaner可以自己控制进程
  • 使用finalizers和cleaners有严重的性能缺陷
  • finalizers有一个严重的安全问题:容易被 finalizer 攻击
    如果构造器或者与序列化等价的方法(比如 readObject, readResolve)抛出异常,一个恶意子类的 finalizer 可以运行在部分构建的实例上,
    这个恶意子类可以引用通过一个静态属性引用父类防止父类被gc回收,一旦这个引用成功,那就很容易执行父类中并不允许存在的方法。
    解决:只需提供一个final finalize 空方法即可

可以实现 AutoCloseable 来代替finalizer和cleaner,然后实现其 close 方法即可,每个实例不再使用调用该方法即可,一般使用 try-with-resources 来确保万一。
值得一提的是,这种类最好有一个字段来记录一个实例是否被 close ,如果不小在实例已经被 closed 之后再次调用,记得抛出 IllegalStateException 异常。

finalizers和cleaners两个正确的用法:

  1. 资源占用着忘记调用上面提到的 close 方法来释放资源,虽然也并不能保证 finalizer 和 cleaner及时执行,但总比什么都不做好,但也需要考虑性能的缺失是否值得
  2. 处理 native peers, 因为 native peer 不是普通的java对象,所以gc不知道它也无法回收它,而finalizers和cleaners适合来完成这个任务

总的来说不要使用 cleaners 或者不要使用 Java9之前的 cleaners;
不要使用 finalizers ,除非需要安全操作或者关闭重要资源,并且留意他们的不确定性和性能。

Item 9 : Prefer try-with-resources to try-finally

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

Java中有很多的资源需要手动释放,执行 close 方法即可,比如 InputStream,BufferedReader 等等,他们都直接或者间接的实现了 AutoCloseable 接口。

使用需要释放的资源时尽可能用 try-with-resources ,可以提升代码可读性,更加简洁清楚,异常堆栈信息更有利于排查bug,比如下面的代码:

private static final int BUFFER_SIZE = 16;
static void copy(String src, String dst) throws IOException {
    InputStream in = new FileInputStream(src);
    try {
        OutputStream out = new FileOutputStream(dst);
        try {
            byte[] buf = new byte[BUFFER_SIZE];
            int n;
            while ((n = in.read(buf)) &gt;= 0)
                out.write(buf, 0, n);
        } finally {
            out.close();
        }
    } finally {
        in.close();
    }
}
private static final int BUFFER_SIZE = 16;
static void copy(String src, String dst) throws IOException {
    try (InputStream in = new FileInputStream(src); OutputStream out = new FileOutputStream(dst)) {
        byte[] buf = new byte[BUFFER_SIZE];
        int n;
        while ((n = in.read(buf)) &gt;= 0) {
            out.write(buf, 0, n);
        }
    }
}

可以明显看到使用try-with-resources 代码简洁很多,更加清楚。

版权所有丨转载请注明出处:https://minei.me/archives/303.html