天天看点

Effective Java:创建和销毁对象

1. 静态工厂方法代替构造器

简介

获取类的实例,除了提供公有的构造器外,还可以使用静态工厂方法

静态工厂方法 提供实例,不同于设计模式中的 工厂方法模式,简单示例:

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

优势:

相对于构造器,静态工厂方法有如下优势
  • 他们有名称.如:
//不需要用参数列表来表达语义,针对需要多个相同签名的构造器(调整构造器参数顺序也可达到目的)
    public boolean isProbablePrime(int certainty) {
        if (certainty <= ) {
            return true;
        }
        return getBigInt().isPrime(certainty);
    }
           
  • 不是每次调用时都创建一个新对象.
针对创建对象代价很高的场景,类似于

Flyweight模式

.不可变类可以

预先构建好实例

,或者

将构建好的实例缓存

起来.
  • 可以返回原返回类型的任何子类型对象.
Collections类,返回隐藏的实现类,后续可更改实现,示例代码:Services.java
  • 创建更加简洁的参数化类型实例,如:
Map<String, List<String>> m = new HashMap<String, List<String>>();
           

可以精简为如下方式,虽然在java8中已经好太多.

public static <K, V> HashMap<K, V> newInstance() {
    return new HashMap<K, V>();
  }
           

缺点:

  • 如果不包含

    public/protect

    的构造器,就不能被子类化
  • 与其他静态方法实际上没有任何区别.因此需要用

    命名规范

    来弥补,如:
valueOf()
of()
getInstance
newInstance
getType
newType
           

2.多个参数考虑用builder

问题

无论是

静态工厂

还是

构造器

,对大量可选参数的扩展都不好.一向的办法:
  1. 使用

    重叠构造器

    模式,

    缺点是

    会给调用者带来疑惑.必须仔细小心,防止参数传递错误.
  2. 使用

    JavaBeans

    模式,

    缺点是

    设置参数被分到几个调用中,会导致

    JavaBean

    状态不一致,

    也就是说

    JavaBeans

    阻止了把类做成不可变的可能

Builder

:

示例:NutritionFacts.java

优点:

既保持了重叠构造器那样的安全性,也保证了

JavaBeans

模式那样好的阅读性.
  • Builder

    模拟了具名的可选参数.
  • 可以对其参数强加约束条件,在对象域中对参数进行检查.
  • 可以有多个可变参数
  • 可以用单个

    Builder

    创建多个对象
通常情况下,

Builder

常与有限制的通配符(如

Builder<? extends Node>

)一起使用
public interface Builder<T>{
    public T build();
}
           

拓展

Java

中 传统的抽象工厂实现

Class

存在一些问题,

newInstance

充当了

build

的一部分,总是试图调用无参的构造函数(无论存不存在),

无参构造不存在也不会

编译时错误

,因此破坏了

编译时的异常检查

.而通过

Builder

就不会有这个问题.

缺点:

  • 必须先创建

    Builder

    的构建器,实践中的开销并不十分明显
  • 比重叠构造器模式更加的

    冗长

    ,适合

    参数

    比较多的情况(且

    后期可能增加参数

    ).

小结

  1. 针对构造器或静态工厂中有多个参数,且参数不固定的时候,

    Builder

    模式是不错的选择.
  2. 否则,用静态工厂,或者构造器会是更好的选择.

3.使用私有构造器或者枚举强化Singleton

1.5之前的两种方法

  • 私有构造器,公有静态成员
  • 私有构造器,公有静态方法
  1. 缺点: 这两种方式,可以通过反射机制(

    setAccessible

    )来进行破坏,可以在构造器创建第二个实例的时候抛出异常来避免.
  2. 优点: 工厂方法很灵活,可以不改变

    API

    ,而改变类是否是

    Singleton

  3. 提醒: 为了防止反序列化创建新实例,需要声明

    实例域是transit的

    ,并且提供

    readResolve

    方法,实例如下:
private static transient final Elvis INSTANCE = new Elvis();
private Object readResolve(){
    return INSTANCE;
}
           

枚举实现方式

示例: Elvis.java

Java 1.5 后可以通过单个元素的枚举实现单例,已经成为实现

Singleton

的最佳方式,有如下好处:

  • 绝对防止多次实例化
  • 防止反序列化共计
单元素的枚举类型 已经称为实现

Singleton

最佳方法.

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

Utility

不需要实例化,如果没有显示构造器,但是编译器会默认提供一个 公有/无参 的构造器.

通过抽象防止类被实例化是行不通的

  1. 这样该类就可以子类化,并且子类可实例化.
  2. 会误导用户,以为是

    专为继承

    而设计的

明智的做法是:

  1. 私有默认构造器,保证外部不可以访问
  2. 默认构造器抛出异常,避免类内部不小心调用.
public class UtilityClass {
    // Suppress default constructor for noninstantiability
    private UtilityClass() {
        throw new AssertionError();
    }
}
           

5.避免创建不必要的对象

概述

尽量重用对象,如果一个对象是

不可变

的,就可以始终被重用.

示例: Person.java

反面例子:

//每次都会创建一个新的"string"
String s = new String("string");
           

应该使用

String s = "string";
           
对于

同时

提供了

静态工厂方法

构造器

的类,通常都是直接使用

静态工厂方法

.

案例:

  • Map#keyset

    方法,对于一个给定的

    Map

    对象,每次调用都返回同一个

    Set

    实例.
  • 优先使用基本类型,而不是装箱类型
public static void main(String[] args) {
        Long sum = L;//这里每次都会构造一个实例,多创建了2^31个实例.
        for (long i = ; i < Integer.MAX_VALUE; i++) {
            sum += i;
        }
        System.out.println(sum);
    }
           

小结

  1. 这里并不是说创建对象是十分昂贵的,如果能提高程序的清晰性,简洁性,多创建通常是好事.
  2. 除非

    Object pool

    中的对象是重量级的,否则无需维护自己的对象池.

6.消除过期对象引用

示例:

//class Stack
public Object pop(){
    if(size ==){
        throw new EmptyStackException();
    }
    Object result= elements[size--];
    elements[size] = null;//清空过期引用
    return result;
}
           
清空过期引用的好处,如果再次引用,将抛出

NullPointerException

说明

  1. 消除过期引用最好的方法是: 让 包含该引用的变量结束期生命周期.(自然发生).
  2. 但上面的例子中显然

    不是result

    来管理

    elements[size]

    生命周期,而是外层类

    Stack

提醒

内存泄漏来源
  1. 一般如果类自己管理内存,就需要警惕内存泄漏问题.
  2. 另一个常见来源是

    缓存

    .解决办法:使用

    WeakHashMap

    .[常见的迷惑 就是 不知道缓存是否还有意义(生命周期).]
  3. 最后,就是监听器和其他回调.解决办法: 使用弱引用(

    Weak reference

    )

7.避免使用终结方法

终结方法的缺点

终结方法(finalizer)是不可预测的,危险的,一般也是不必要的.根据经验,应尽量避免使用终结方法
  1. JVM

    会延迟执行 终结方法.导致从对象不可达到终结方法执行时间不可控.
  2. Java

    语言规范不仅不保证终结方法及时执行,而且不保证他们会被执行.
不能依赖

终结方法

来更新重要的

持久状态

.

System.gc

System.runFinalization

只是增加了终结方法被执行的机会.

System.runFinalizersOnExit

Runtime.runFinalizersOnExit

是声称可保证终结方法一定被执行,因有严重缺陷而废弃/
  1. 使用终结方法,有一个严重的

    Severe

    性能损失.

终结方案:

  • 提供一个

    显示的终止方法

    .在对象不可用的时候,调用即可.典型例证:

    InputStream

    ,

    OutputStream

  • 显示的终止方法,通常与

    try-finally

    结合使用,以确保及时执行.
Foo foo = new Foo();
  try{
    //...
  }finally{
    foo.terminate();//
  }
           

终结方法的好处

  • 当 所有者 忘记调动显示 终结方法时,终结方法可以充当

    安全网

    (释放关键资源总比一点都不释放要好)
  • 第二种用途 与

    java本地对等体

    (

    native peer

    ) 相关.[本地对等体是一个本地对象,普通对象通过本地方法委托给一个本地对象].本地对等体不是一个普通对象,垃圾回收器并不知道它.当它的Java对等体被回收的时候,它不会被回收.
  • 终止方法链并不会被自动执行,如果

    子类有并覆盖了

    终结方法.就必须手动调用

    父类的终结方法

@Override protected void finalize() throws Throwable {
    try {
    //即使子类抛出异常,父类的终结方法也会执行
      //finalize sub class
    } finally {
      super.finalize();
    }
  }
           

提醒

要防止忘记调用超类的终结方法,可以使用一个

终结方法守卫者

,如下:

public class Foo{
    private final Object finalizer = new Object(){
         @Override protected void finalize() throws Throwable {
            Foo.this.finalize();
          }
    }
}
           

小结

  1. 除非作为安全网,或者终止非关键的本地资源,否则不要使用本地方法/
  2. 既然使用了

    终结方法

    ,就必须调用父类的终结方法.
  3. 如果要把

    终结方法

    公有的非 final类

    关联起来,需要使用

    终结方法守卫者