Effective Java:创建和销毁对象

本章节共涉及到7条原则,包括如下主题:

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

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

通常情况下,大家都习惯使用new去直接创建一个对象实例,前提是这个类提供了一个公有构造器。除此之外,类可以提供一个公有的静态工厂方法(static factory method),用于返回自身的对象实例。我们在学习单例模式时使用过这种返回对象的方式。

除了单例模式种禁止客户端通过new创建额外的对象实例之外,通过静态工厂方法提供对象实例还有以下好处

1. 静态工厂方法有名称

众所周知,构造器没有返回类型、方法名必须与类名完全相同,这意味着如果这个类有多个构造函数,它们只能通过参数列表进行区分,使用者难以仅仅根据参数列表确定该使用哪个构造器。例如构造器BigInteger(int, int, Random)返回的BigInteger可能是素数,如果使用名为BigInteger.probablePrime的静态方法来表示,显然更加清楚。

2.不必在每次调用它们的时候都创建一个新的对象

一个类可以将创建好的实例缓存起来,进行重复利用,从而避免创建不必要的重复对象。Boolean.valueOf(boolean)方法便是利用了这个技术:

//事先创建好两个Boolean对象
public static final Boolean TRUE = new Boolean(true);
public static final Boolean FALSE = new Boolean(false);
//根据传来的参数决定返回哪个现有的对象,而不是重新创建一个
public static Boolean valueOf(boolean b) {
    return (b ? TRUE : FALSE);
}

如果程序经常请求创建相同的对象,并且创建对象的代价很高,这样可以极大地提升性能。

静态功能方法能够为重复的调用返回相同的对象,这样有助于类总能严格控制在某个时刻哪些实例应该存在。这种类被称作实例受控的类(instance-controlled)。编写实例受控的类有几个原因:

  • 确保它是一个单例或者不可实例化的
  • 使不可变的类可以确保不会存在两个相等的实例

3.它们可以返回原返回类型的任何子类对象

使用静态工厂方法返回的对象可以根据实际情况判断返回自己还是自己的子类,而且返回子类的时候又不需要使这个子类变成公有的,从而使实现类得以隐藏,使得API变得非常简洁。
例如EnumSet没有公有构造器,只有静态工厂方法,返回它的两个实现类之一,具体哪一个则取决于底层枚举类型的大小:如果它的元素不超过64个(大多数情况下一般不超过64个),静态工厂方法就会返回一个RegularEnumSet实例,用单个long类型支持;如果枚举类型超过了64个,工厂就会返回JumboEnumSet实例,用long数组进行支持。
这两个实现类对客户端而言是不可见的。如果未来RegularEnumSet不能再给小的枚举类型提供性能优势,就可能被删除,而不会对现有的代码产生任何影响。同样的,未来也可能添加第三、第四个EnumSet实现来满足新的需求,客户端永远不必关心究竟有多少实现类实现EnumSet,只要使用EnumSet提供的接口即可。

4.在创建参数化类型实例的时候,它们使代码变得更加简洁
在调用参数化类的构造器时,即使类型参数很明显,也必须指明。这通常需要连续两次提供类型参数,例如:

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

这样个声明是我在项目中实现一个功能时使用过的,看起来非常的繁琐,前面声明过的后面还需要再写一遍。
假设HashMap提供了这个静态工厂方法:

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

我就可以使用下面这么简洁的代码代替上面繁琐的声明:

Map<String, List<Map<String, Object>> map = HashMap.newInstance();

静态工厂方法的主要缺点有:

1. 类如果不含公有的或者受保护的构造器,就不能被子类化
一个只含有私有构造器的类是无法被继承的,这也因祸得福,因为它鼓励程序员使用组合,而不是继承。(见第16条)

2. 它们与其他的静态方法实际上没有任何区别
由于静态工厂方法在API文档上没有任何特殊标识,因此对于提供静态工厂方法而不是构造器的类而言,要想查明如何实例化一个类是非常困难的。
但是我们可以通过遵循标准的命名习惯来弥补这一劣势。下面是静态工厂方法的一些惯用名称:

  • valueOf——不太严格地讲,该方法返回的实例与它的参数具有相同的值。这样的静态工厂方法实际上是类型转换方法。
  • of——valueOf的一种更为简洁的替代,在EnumSet中使用并流行起来。
  • getInstance——返回的实例是通过方法的参数来描述的,但是不能够说与参数具有同样的值。对于Singleton来说,该方法没有参数,并返回唯一的实例
  • newInstance——想getInstance一样,但是newInstance能够确保返回的每个实例都与所有的其他实例不同。
  • getType——像getInstance一样,但是在工厂方法处于不同的类中的时候使用。Type表示工厂方法所返回的对象类型。
  • newType——像newInstance一样,但是在工厂方法处于不同的类中的时候使用。Type表示工厂方法所返回的对象类型。

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

静态工厂和构造器有个共同的局限性:它们都不能很好地扩展到大量的可选参数。我在项目中为了调用一个生成PDF文件的服务需要给这个文件提供一些在PDF中填空用的参数,共有18个之多,所以我定义了一个bean去装配这些参数,以一个整体的方式传递这些参数(请不要吐槽为啥不用Map,这不是重点)。那么问题来了,这个bean的18个参数有的可选、有的必选,我如何构造这个对象呢?

有些前辈们喜欢用重叠构造器(telescoping constructor)模式,我之前阅读Apache ODE源码的时候见过很多地方都是这么写的,这种模式是先提供一个只有必选参数的构造器,第二个构造器有一个可选参数,第三个构造器有两个可选参数…以此类推,我所接触到的那些最多大约也就4个构造器,用这种实现方式无可厚非。但是若用来构造我那有着18个属性的bean,假设有n个可选属性,则需要n+1个构造函数!

我所采用的方式是只提供一个无参构造函数,然后调用setter来设置每个必选的参数以及需要的可选参数。我自认为这种模式弥补了重叠构造器的不足,还使得代码读起来更容易。

遗憾的是这样导致一个对象的构造过程被分到了几个调用中,在构造过程中JavaBean可能处于不一致的状态。类无法仅仅通过检验构造器参数的有效性来保证一致性。与此同时,JavaBean模式阻止了把类做成不可变的可能,这就需要我付出额外的代价来确保它的线程安全。

今天看Effective Java的时候学到了第三种方法,既能保证像构造器模式那样的安全性,也能保证像JavaBeans模式那么好的可读性,这就是建造者模式的一种形式——不直接生成想要的对象,而是让客户端利用所有必要的参数调用构造器(或者静态工厂),得到一个builder对象,然后客户端在builder对象上调用类似setter的方法,来设置每个相关的可选参数。最后,客户端调用无参的builde方法来生成不可变的对象。这个builder是它构建类的静态成员类。下面是我的实例的简化版:

public class GuaranteeLetterFileSimple {

    //必选
    private final String BHCode;
    private final String tradeCenter;
    //可选
    private final String companyName;
    private final String projectName;
    private final String moneyText;
    private final int days;

    public static class Builder{
        //必选
        private final String BHCode;
        private final String tradeCenter;
        //可选
        private String companyName = "";
        private String projectName = "";
        private String moneyText = "";
        private int days = 0;

        public Builder(String BHCode, String tradeCenter) {
            this.BHCode = BHCode;
            this.tradeCenter = tradeCenter;
        }

        public Builder companyName(String companyName){
            this.companyName = companyName;
            return this;
        }

        public Builder projectName(String projectName){
            this.projectName = projectName;
            return this;
        }

        public Builder moneyText(String moneyText){
            this.moneyText = moneyText;
            return this;
        }

        public Builder days(int days){
            this.days = days;
            return this;
        }

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

    public GuaranteeLetterFileSimple(Builder builder) {
        BHCode = builder.BHCode;
        tradeCenter = builder.tradeCenter;
        companyName = builder.companyName;
        projectName = builder.projectName;
        moneyText = builder.moneyText;
        days = builder.days;
    }
}

其中GuaranteeLetterFileSimple本身是不可变的,所有的默认参数都单独放在一个地方。builder的setter方法返回builder本身,以便使用调用链设置参数。下面是客户端代码:

 GuaranteeLetterFileSimple fileSimple = new Builder("BH20001","交易中心").
                companyName("广联达").projectName("项目A").moneyText("伍佰万元整").days(30).build();

这样使得客户端代码很容易编写,而且易于阅读。

与构造器相比,builder的优势在于它有多个可变参数。构造器就行方法一样,只能有一个可变参数。由于构造器利用单独的方法来设置每个参数,因此想要多少可变参数都可以,甚至每个setter方法都有一个可变参数。

我们还可以在builder的每个setter方法中添加一些约束条件,当该约束条件没有得到满足的时候,setter方法就会跑出IllegalArgumentException,这样一旦传递了无效的参数,就会立即抛出异常,而不是等着调用build方法,因此也就不会有一个“不健全”的对象诞生。设想采用我之前的实现方案,在设置可选属性的时候出现了异常,这个对象已经被创建了出来,但是却不是一个健康可用的,显然是不应该的。

我们在使用传统的抽象工厂的时候,用调用Class对象的newInstance方法创建产品实例,这样隐含着很多问题。newInstance方法总是倾向于调用类的无参构造器,这个构造器甚至可能不存在,即便不存在,我们也看不到任何编译时错误,只有在客户端运行代码的时候才会处理InstantiationException和IllegalAccessException,这样一点也不优雅。newInstance还会传播构造器所带来的异常,因为Class.newInstance破坏了编译时的异常检查,而是用Builder则弥补了这些不足。

总而言之,如果类的构造器或者静态工厂中具有多个参数(超过5个),设计这种类的时候建议选择Builder模式,特别是当大多数参数都是可选的时候。

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

之单例模式的几种实现方式


单例模式确保某个类只有一个实例,如果希望在系统中某个类的对象只能存在一个,单例模式是最好的解决方案。在计算机系统中,线程池、缓存、日志对象、对话框、打印机、显卡的驱动程序对象常被设计成单例。

Java 1.5以前,实现单例模式有两种方法,几乎所有的实现方式都是将构造器设置为私有。在第一中方法中,公有静态成员是一个final域,指向自己的唯一的实例。

静态域

这样有个漏洞,享有特权的客户端可以借助AccessibleObject.setAccessible方法,通过反射机制调用私有构造器,弥补措施是通过修改构造器,让它在被要求创建第二个实例的时候抛出异常。第二种方法是将静态域设为了私有,利用公有的静态工厂方法返回实例。

一种目前来说比较普及的单例的实现方式是如下的“双重锁”方式,这种方式保证了线程安全,也不会影响效率,但是想要序列化,除了implements Serializable之外还是需要做很多工作的。

public class SingleTon {
    private static SingleTon singleTon = null;
    private SingleTon() {  }
    public static SingleTon getInstance(){
        if (singleTon == null) {
            synchronized (SingleTon.class) {
                if (singleTon == null) {
                    singleTon = new SingleTon();
                }
            }
        }
        return singleTon;
    }
}

本书中提到了另一种方式来实现单例——枚举。编写一个包含单个元素的枚举类型:

enum TestSingleton3 {  
  INSTANCE;  
  public static TestSingleton3 getInstance() {  
    return INSTANCE;  
  }  
}  

这种方法的安全性,“老紫竹”在他的博客中进行了实验验证。

总而言之,使用这种方式实现单例模式的优点在于:
- 更加简洁;
- 天然的序列化机制;
- 绝对的防止多次实例化,即使反序列化或者反射攻击也不会创建第2个实例。

本书中言:虽然这种方法还没有广泛采用,但是单元素的枚举类型已经成为实现Singleton的最佳方法。

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

有时候我们会写一些工具类,通常习惯称之为XXXUtil,这些类里只有一些静态方法,使用时无需实例化;还有时候我们写一个单例,也不希望它在其他地方被实例化,因此我们需要一些手段来使这个类不能实例化。企图通过将类做成抽象类来强制该类不可被实例化是行不通的。这个类能被继承,其子类可以实例化,并且有可能误导用户——这个类是专门为了继承而设计的。有一些简单的方法能够确保类不可以被实例化,我们可以写一个私有构造器,这样其他地方就不能通过new来创建这个类的实例了。为了防止内部实例化或者通过反射实例化,需要在构造器里面抛一个异常:

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

这种方法有个副作用,它使得一个类不能被子类化。所有的构造器都必须是显示或隐式调用超类构造器,而这种情形下,子类就没有可访问的超类构造器可调用了。

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

一般来说,最好能重用对象而不是每次需要的时候就创建一个相同功能的新对象。在SpringMVC中,bean在默认情况下都是单例的,比如一个dao对象,主要作用是跟数据库交互,这种对象只有一个就好,没有必要反复创建新的。考虑下面的语句:

String s = new String("a new string");

该语句每次被执行的时候都会创建一个新的String实例,这完全是没有必要的,String对象时不可变的(immutable),”a new string”本身就是一个对象,存在于常量池中,我们每次使用这个对象再去new一个String对象放在堆中完全是画蛇添足。

所以只需要这么写就可以了:

String s = "a new string";

这个版本只用了一个String实例,而不是每次执行的时候都创建一个新的。

对于同时提供了静态工厂方法和构造器的不可变类,通常可以使用静态工厂方法而不是构造器,以避免创建不必要的对象。例如,使用静态工厂方法Boolean.valueOf(String)机会总是比使用构造器Boolean(String)要好一些。使用构造器每次都创建一个新的对象,而静态方法则从来不要求这样做。

在java1.5之后,有一种创建多于对象的新方法,称作自动装箱,它允许程序员将基本类型和装箱基本类型混用,按需要自动装箱和拆箱。考虑下面的程序,它计算所有int正值的总和,因此需要使用long保存结果:

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

这段程序算出来的答案是正确的,但是比实际情况更慢一些,只因为sum被声明成了Long类型,这意味着程序构造了大约2^31个多余的Long实例,将sum声明改成long,则效率会极大地提高,结论很明显:要优先使用基本类型而不是基本装箱类型,要当心自动装箱

第六条:消除过期的对象引用

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 element){
        elements[size ++ ] = element;
    }
    public Object pop(){
        if(size == 0)
            throw  new EmptyStackException();
        return elements[--size];
    }
}

这段程序中并没有明显的错误,能偶正常使用(省略了容量调整方法),但是它却隐含着一个问题,随着内存占用的不断增加,程序性能的降低会逐渐表现出来,最终导致内存泄露。这种情形虽然比较少见,但是这种问题多了,就是一个隐藏的定时炸弹,直到它爆炸的那一天也不知道究竟是因为什么,只好重启服务器。

那么究竟哪里发生了内存泄露?如果一个栈先增长,再收缩,那么出栈的对象将不会被当成垃圾被回收,即便size已经不会再指向那个对象。因为栈内部还保留着对这些对象的过期引用(obsolete reference)。

这个问题的修复方法很简单:一旦对象引用已经过期,只需清空这些引用即可,对于上述例子中的Stack类而言,只要一个单元被出栈,只想它的引用就过期了,pop方法的修订版如下所示:

public Object pop(){
        if(size == 0)
            throw  new EmptyStackException();
        Object element = elements[--size];
        elements[size] = null;
        return element;
    }

内存泄漏的另一个常见来源是缓存。一旦你把对象引用放到缓存中,它就很容易被遗忘掉,从而使得它不再有用之后很长一段时间内仍然留在缓存中。对于这个问题,有几种可能的解决方案。如果你正好要实现这样的缓存:只要在缓存之外存在对某个项的键的引用,该项就有意义,那么就可以用WeakHashMap代表缓存;当缓存中的项过期之后,它们就会自动被删除。记住只有当所要的缓存项的生命周期是由该键的外部引用而不是由值决定时,WeakHashMap才有用处。

更为常见的情形则是,”缓存项的生命周期是否有意义”并不是很容易确定,随着时间的推移,其中的项会变得越来越没有价值。在这种情况下,缓存应该时不时地清除掉没用的项。这项清除工作可以由一个后台线程(可能是Timer或者ScheduledThreadPoolExecutor)来完成,或者也可以在给缓存添加新条目的时候顺便进行清理。LinkedHashMap类利用它的removeEldestEntry方法可以很容易地实现后一种方案。对于更加复杂的缓存,必须直接使用java.lang.ref。

内存泄漏的第三个常见来源是监听器和其他回调。如果你在实现的是客户端注册回调却没有显式地取消注册的API,除非你采取某些动作,否则它们就会积聚。确保回调立即被当作垃圾回收的最佳方法是只保存它们的弱引用(weak reference),例如,只将它们保存成WeakHashMap中的键。

由于内存泄漏通常不会表现成明显的失败,所以它们可以在一个系统中存在很多年。往往只有通过仔细检查代码,或者借助于调试工具(称为Heap Profiler)才能发现内存泄漏问题。因此,如果能够在内存泄漏发生之前就知道如何预测此类问题,并阻止它们发生,那是最好不过的了。

第七条:避免使用终结方法

避免使用终结方法的最重要的原因是JVM不保证终结方法能被及时执行,甚至不保证它们会被执行。

终结方法一般是发生GC的时候用于释放一些资源,一般认为该资源被回收的时候会执行终结方法,但是一个对象从变得不可达开始到它被回收之间的时间是不确定的,由于终结方法线程的优先级较低,因此可能永远拿不到执行权,导致终结方法不会被执行,导致行为不稳定。由于不同的JVM对于GC的实现有所不同,过于依赖终结方法也会导致可移植性差的问题。此外,使用终结方法还会带来严重的性能损失。

综上,我们不能依赖终结方法这个不可控的方法来释放对象中封装的资源(文件、线程、数据库连接池等)。

如果我们确实需要释放上述的资源,只需要提供一个显示的终止方法,并要求该类的客户端在每个实例不再游泳的时候调用这个方法。因此该实例必须记录下自己是否已经被终止了:显示的终止方法必须在一个私有域中记录下“该对象已经不再有效”。

显示的终止方法的典型的例子就是InputStream、OutputStream和java.sql.Connection上的close方法。通常它们们的使用与try-finally结构结合起来使用,以确保及时终止。在finally子句内部调用显示的终止方法,可保证即使在使用对象的时候有异常抛出,该终止方法也会被执行:

Foo foo = new Foo(...);
try{
    //Do what must be done with foo
}finally{
    foo.terminate();//Explicit termination method
}

看起来终结方法似乎一无是处,实际上不是的,它有两种合法用途。第一种用途是,当对象的所有者忘记调用前面段落中建议的显示终止方法时,终结方法可以充当“安全网”。虽然终结方法不能保证被执行,但是在客户端无法通过调用显示的终止方法来正常结束操作的情况下,迟一点释放资源总比永远不释放要好。

显示终止方法模式的实例中所示的四个类(FileInputStream、FileOutputStream、Timer、Connection),都有终结方法,当它们的终止方法未能被调用的时候,这些终止方法充当了最后一道防线。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值