【Effective Java】建立和销毁对象

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

构造器是建立一个对象实例的最基本最经常使用的方法。开发者在使用某个类的时候,一般会使用new一个构造器来实现,其实也有其余方式能够实现的,如利用发射机制。这里主要说的是经过静态类工厂的方式来建立class的实例,如:html

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

静态工厂方法和构造器不一样有如下主要优点:java

1. 有意义的名称

可能有多个构造器,不一样构造器有不一样的参数,而参数自己并不能确切地描述被返回的对象,因此显得有点模糊,而具备适当名称的静态工厂可读性更强,表达也更清晰。数组

如,构造器BigInteger(int, int, Random)返回一个BigInteger多是一个素数,更名为BigInteger.probablePrime的静态工厂方法表示也就更加清晰。缓存

2. 没必要在每次调用的时候建立一个新的对象

这样能够避免建立没必要要的重复对象,提升程序效率。安全

3. 能够返回原返回类型的任何子类型的对象

Java的不少服务提供者框架(ServiceProvider Framework,三个主要组件:服务接口(Service Interface)这是提供者实现的;提供者注册API(Provider Registration API),这是系统用来注册实现,让客户端访问它的;服务访问API(Service Access API),是客户端用来获取服务实例的。可选组件:服务提供者接口(Service Provider Interface),提供者负责建立其服务实现的实例)都运用到这个特性,如JDBC的API。多线程

下面是一个包含一个服务提供者接口和一个默认提供者:框架

//Service interface
public interface Service{
    //...service methods
}
//Service provider interface
public interface Provider{
    Service newService();
}
//noninstantiable class for service registration and access
public class Service{
    private Service(){}

    //Maps service names to services
    private static final Map<String, Provider> providers=new ConcurrentHashMap<String, Provider>();
    public static final String DEFAULT_PROVIDER_NAME="<def>";
    //Provider registration API
    public static void registerDefaultProvider(Provider p){
        registerProvider(DEFAULT_PROVIDER_NAME);
    }
    public static void registerProvider(String name,Provider p){
        providers.put(name, p);
    }
    //Service access API
    public static Service newInstance(){
        return newInstace(DEFAULT_PROVIDER_NAME);
    }
    public static Service newInstance(String name){
        Provider p=providers.get(name);
        if(p==null)
            throw new IllegalArgumentException("No provider registered with name:"+name);
        return p.newService();
    }
}

4. 在建立参数化类型实例的时候,它们使代码变得更加简洁

原来:dom

Map<String, List<String>> map=new HashMap<String, List<String>>();

改成静态工厂方法,能够利用参数类型推演的优点,避免了类型参数在一次声明中被屡次重写所带来的烦忧:ide

public static <K,V> HashMap<K,V> newInstance() {
    return new HashMap<K,V>();
}
Map<String,String> m = MyHashMap.newInstance();

固然,静态方法也存在缺点:函数

  1. 类若是不含公有的或者受保护的构造器,就不能被子类化;
  2. 与其余的静态方法实际上没有任何区别(API没有像构造器那样标识出来)

不过,对于静态工厂方法和构造器,一般优先考虑静态工厂方法。

2、遇到多个构造参数时要考虑用构建器(Builder模式)

静态工厂和构造器有一个共同的局限性:它们都不能很好地扩展到大量的可选参数。固然能够经过如下方法解决:

方法一:利用重叠构造器模式(就是须要多少个参数就在参数列表添加多少个),可是当有不少个参数时,客户端代码会很难编写,而且难以阅读;

方法二:JavaBeans模式,调用一个无参构造函数,而后调用setter方法来设置每一个必要的参数,但调用的过程当中可能会出现不一致的状态,调试比较麻烦;

方法三:Builder模式。不直接生成想要的对象,而是让客户端利用全部必要的参数调用构造器(或静态方法),获得一个builder对象,而后再在builder对象对每一个参数对应的方法进行调用来设置,以下:

class NutritionFacts {
    private final int servingSize;
    private final int servings;
    private final int calories;
    private final int fat;
    private final int sodium;
    private final int carbohydrate;
    public static class Builder {
        //对象的必选参数
        private final int servingSize;
        private final int servings;
        //对象的可选参数的缺省值初始化
        private int calories = 0;
        private int fat = 0;
        private int carbohydrate = 0;
        private int sodium = 0;
        //只用少数的必选参数做为构造器的函数参数
        public Builder(int servingSize,int servings) {
            this.servingSize = servingSize;
            this.servings = servings;
        }
        public Builder calories(int val) {
            calories = val;
            return this;
        }
        public Builder fat(int val) {
            fat = val;
            return this;
        }
        public Builder carbohydrate(int val) {
            carbohydrate = val;
            return this;
        }
        public Builder sodium(int val) {
            sodium = val;
            return this;
        }
        public NutritionFacts build() {
            return new NutritionFacts(this);
        }
    }
    private NutritionFacts(Builder builder) {
        servingSize = builder.servingSize;
        servings = builder.servings;
        calories = builder.calories;
        fat = builder.fat;
        sodium = builder.sodium;
        carbohydrate = builder.carbohydrate;
    }
}
//使用方式
public static void main(String[] args) {
    NutritionFacts cocaCola = new NutritionFacts.Builder(240, 8).calories(100)
        .sodium(35).carbohydrate(27).build();
    System.out.println(cocaCola);
}

因此,若是类的构造器或者静态工厂中具备多个参数,设计这种类时,Builder模式就是种不错的选择!

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

Singleton模式常常会被用到,它被用来表明那些本质上惟一的系统组件,如窗口管理器或者文件系统。在Java中实现单例模式主要有三种:

  1. 将构造函数私有化,直接经过静态公有的final域字段获取单实例对象:

    public class Elvis {
        public static final Elvis INSTANCE = new Elvis();
        private Elivs() { ... }
        public void leaveTheBuilding() { ... }
    }

    这样的方式主要优点在于简洁高效,使用者很快就能断定当前类为单实例类,在调用时直接操做Elivs.INSTANCE便可,因为没有函数的调用,所以效率也很是高效。然而事物是具备必定的双面性的,这种设计方式在一个方向上走的过于极端了,所以他的缺点也会是很是明显的。若是从此Elvis的使用代码被迁移到多线程的应用环境下了,系统但愿可以作到每一个线程使用同一个Elvis实例,不一样线程之间则使用不一样的对象实例。那么这种建立方式将没法实现该需求,所以须要修改接口以及接口的调用者代码,这样就带来了更高的修改为本

  2. 经过公有域成员的方式返回单实例对象:

    public class Elvis {
        public static final Elvis INSTANCE = new Elvis();
        private Elivs() { ... }
        public static Elvis getInstance() { return INSTANCE; }
        public void leaveTheBuilding() { ... }
    }

    这种方法很好的弥补了第一种方式的缺陷,若是从此须要适应多线程环境的对象建立逻辑,仅须要修改Elvis的getInstance()方法内部便可,对用调用者而言则是不变的,这样便极大的缩小了影响的范围。至于效率问题,现今的JVM针对该种函数都作了很好的内联优化,所以不会产生因函数频繁调用而带来的开销。

  3. 使用枚举的方式(Java SE5):

    public enum Elvis {
        INSTANCE;
        public void leaveTheBuilding() { ... }
    }

    就目前而言,这种方法在功能上和公有域方式相近,可是他更加简洁更加清晰,扩展性更强也更加安全。虽然这种方法还没被普遍采用,但单元素的枚举类型已经成为实现Singleton的最佳方法。

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

对于有些工具类如java.lang.Math、java.util.Arrays等,其中只是包含了静态方法和静态域字段,所以对这样的class实例化就显得没有任何意义了。然而在实际的使用中,若是不加任何特殊的处理,这样的classes是能够像其余classes同样被实例化的。这里介绍了一种方式,既将缺省构造函数设置为private,这样类的外部将没法实例化该类,与此同时,在这个私有的构造函数的实现中直接抛出异常,从而也避免了类的内部方法调用该构造函数

public class UtilityClass {
    //Suppress default constructor for noninstantiability.
    private UtilityClass() {
        throw new AssertionError();
    }
}

这样定义以后,该类将不会再被外部实例化了,不然会产生编译错误。然而这样的定义带来的最直接的负面影响是该类将不能再被子类化。

5、避免建立没必要要的对象

通常来讲,最好能重用对象而不是在每次须要的时候建立一个相同功能的新对象。

试比较如下两行代码在被屡次反复执行时的效率差别:

String s = new String("stringette"); //don't do this
    String s = "stringette";

因为String被实现为不可变对象,JVM底层将其实现为常量池,既全部值等于"stringette" 的String对象实例共享同一对象地址,并且还能够保证,对于全部在同一JVM中运行的代码,只要他们包含相同的字符串字面常量,该对象就会被重用。

咱们继续比较下面的例子,并测试他们在运行时的效率差别:

Boolean b = Boolean.valueOf("true");
    Boolean b = new Boolean("true");

前者经过静态工厂方法保证了每次返回的对象,若是他们都是true或false,那么他们将返回相同的对象。换句话说,valueOf将只会返回Boolean.TRUE或Boolean.FALSE两个静态域字段之一。然后面的Boolean构造方式,每次都会构造出一个新的Boolean实例对象。这样在屡次调用后,第一种静态工厂方法将会避免大量没必要要的Boolean对象被建立,从而提升了程序的运行效率,也下降了垃圾回收的负担。

继续比较下面的代码:

public class Person {
    private final Date birthDate;
    //判断该婴儿是不是在生育高峰期出生的。
    public boolean isBabyBoomer {
        Calender c = Calendar.getInstance(TimeZone.getTimeZone("GMT"));
        c.set(1946,Calendar.JANUARY,1,0,0,0);
        Date dstart = c.getTime();
        c.set(1965,Calendar.JANUARY,1,0,0,0);
        Date dend = c.getTime();
        return birthDate.compareTo(dstart) >= 0 && birthDate.compareTo(dend) < 0;
    }
}
//修改后
public class Person {
    private static final Date BOOM_START;
    private static final Date BOOM_END;

    static {
        Calender c = Calendar.getInstance(TimeZone.getTimeZone("GMT"));
        c.set(1946,Calendar.JANUARY,1,0,0,0);
        BOOM_START = c.getTime();
        c.set(1965,Calendar.JANUARY,1,0,0,0);
        BOOM_END = c.getTime();
    }
    public boolean isBabyBoomer() {
        return birthDate.compareTo(BOOM_START) >= 0 && birthDate.compareTo(BOOM_END) < 0;
    }
}

改进后的Person类只是在初始化的时候建立Calender、TimeZone和Date实例一次,而不是在每次调用isBabyBoomer方法时都建立一次他们。若是该方法会被频繁调用,效率的提高将会极为显著。

集合框架中的Map接口提供keySet方法,该方法每次都将返回底层原始Map对象键数据的视图,而并不会为该操做建立一个Set对象并填充底层Map全部键的对象拷贝。所以当屡次调用该方法并返回不一样的Set对象实例时,事实上他们底层指向的将是同一段数据的引用。

在该条目中还提到了自动装箱行为给程序运行带来的性能冲击,若是能够经过原始类型完成的操做应该尽可能避免使用装箱类型以及他们之间的交互使用。见下例:

public static void main(String[] args) {
    Long sum = 0L;  //注意Long与long
    for (long i = 0; i < Integer.MAX_VALUE; ++i) {
        sum += i;
    }
    System.out.println(sum);
}

本例中因为错把long sum定义成Long sum,其效率下降了近10倍,这其中的主要缘由即是该错误致使了2的31次方个临时Long对象被建立了。要优先使用基本类型而不是装箱基本类型,要小心无心识的自动装箱。

6、消除过时的对象引用

尽管Java的JVM垃圾回收机制对内存进行智能管理了,不像C++那样须要手动管理,但只是由于如此,Java中内存泄露变得更加隐匿,更加难以发现,见以下代码:

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.copys(elements,2*size+1);
    }
}

以上示例代码,在正常的使用中不会产生任何逻辑问题,然而随着程序运行时间不断加长,内存泄露形成的反作用将会慢慢的显现出来,如磁盘页交换、OutOfMemoryError等。那么内存泄露隐藏在程序中的什么地方呢?当咱们调用pop方法是,该方法将返回当前栈顶的elements,同时将该栈的活动区间(size)减一,然而此时被弹出的Object仍然保持至少两处引用,一个是返回的对象,另外一个则是该返回对象在elements数组中原有栈顶位置的引用。这样即使外部对象在使用以后再也不引用该Object,那么它仍然不会被垃圾收集器释放,长此以往致使了更多相似对象的内存泄露。修改方式以下:

public Object pop() {
    if (size == 0) 
        throw new EmptyStackException();
    Object result = elements[--size];
    elements[size] = null; //手工将数组中的该对象置空
    return result;
}

因为现有的Java垃圾收集器已经足够只能和强大,所以没有必要对全部不在须要的对象执行obj = null的显示置空操做,这样反而会给程序代码的阅读带来没必要要的麻烦,该条目只是推荐在如下3中情形下须要考虑资源手工处理问题:

  1. 类是本身管理内存,如例子中的Stack类。
  2. 使用对象缓存机制时,须要考虑被从缓存中换出的对象,或是长期不会被访问到的对象。
  3. 事件监听器和相关回调。用户常常会在须要时显示的注册,然而却常常会忘记在不用的时候注销这些回调接口实现类。

7、避免使用终结方法

终结方法(finalizer)一般是不可预测的,也是很危险的,通常状况下是没必要要的。使用终结方法会致使行为不稳定、下降性能,以及可移植性问题。

在Java中完成这样的工做主要是依靠try-finally机制来协助完成的。然而Java中还提供了另一种被称为finalizer的机制,使用者仅仅须要重载Object对象提供的finalize方法,这样当JVM的在进行垃圾回收时,就能够自动调用该方法。可是因为对象什么时候被垃圾收集的不肯定性,以及finalizer给GC带来的性能上的影响,所以并不推荐使用者依靠该方法来达到关键资源释放的目的。好比,有数千个图形句柄都在等待被终结和回收,惋惜的是执行终结方法的线程优先级要低于普通的工做者线程,这样就会有大量的图形句柄资源停留在finalizer的队列中而不能被及时的释放,最终致使了系统运行效率的降低,甚至还会引起JVM报出OutOfMemoryError的错误。

Java的语言规范中并无保证该方法会被及时的执行,甚至都没有保证必定会被执行。即使开发者在code中手工调用了 System.gcSystem.runFinalization 这两个方法,这仅仅是提升了finalizer被执行的概率而已。还有一点须要注意的是,被重载的finalize()方法中若是抛出异常,其栈帧轨迹是不会被打印出来的。在Java中被推荐的资源释放方法为,提供显式的具备良好命名的接口方法,如 FileInputStream.close()Graphic2D.dispose() 等。而后使用者在finally区块中调用该方法,见以下代码:

public void test() {
    FileInputStream fin = null;
    try {
        fin = new FileInputStream(filename);
        //do something.
    } finally {
        fin.close();
    }
}

在实际的开发中,利用finalizer又能给咱们带来什么样的帮助呢?见下例:

public class FinalizeTest {
    //@Override
    protected void finalize() throws Throwable {
        try {
            //在调试过程当中经过该方法,打印对象在被收集前的各类状态,
            //如判断是否仍有资源未被释放,或者是否有状态不一致的现象存在。
            //推荐将该finalize方法设计成仅在debug状态下可用,而在release
            //下该方法并不存在,以免其对运行时效率的影响。
            System.out.println("The current status: " + _myStatus);
        } finally {
            //在finally中对超类finalize方法的调用是必须的,这样能够保证整个class继承
            //体系中的finalize链都被执行。
            super.finalize(); 
        }
    }
}

整理参考自《Effective Java》和 Effective Java (建立和销毁对象)

相关文章
相关标签/搜索