Effective Java(一) 创建和销毁对象

本章的主题时创建和销毁对象:
1. 何时及如何创建对象;
2. 何时及如何避免创建对象;
3. 如何确保创建的对象能够适时销毁;
4. 如何管理对象销毁之前的清理动作。

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

静态工厂方法与设计模式中的工厂方法模式不同,静态工厂方法并不直接对应于设计模式中的工程方法。
一、静态工厂方法的优势:
1. 它们有名称。一个类只能有一个带有指定签名的构造器,静态工厂方法有名称。当一个类需要多个带有相同签名的构造器时就用静态工厂方法代替构造器。
2. 不必在每次调用它们的时候都创建一个对象。例如单例模式。
3. 静态工厂方法可以返回任何子类型对象。静态工厂方法返回的对象所属的类,在编写包含该静态工厂方法的类时可以不必存在(关键词:服务提供者框架
4. 在创建参数化类型实例时,它们使代码变得更加简洁

//例如不使用工厂方法时(显然已过时):
Map<String, List<String>> m = new HaseMap<String,List<String>>();
//使用工厂方法时:
public static <K, V> HashMap<K, V> newInstance(){
    return new HashMap<K, V>();
}
Map<String,List<String>> m = HashMap.newInstance();

二、静态工厂方法的缺点
1. 如果类中不含public或protected的构造器,就不能被子类化。
2. 它们与其他静态方法实际上没有任何区别,它们没有像构造器那样在API文档中明确标识出来。

三、静态工厂方法的惯用名称
valueOf:返回的实例与参数有相同的值
of:valueOf更为简洁的替代
getInstance:对于Singleton来说,单例、无参
newInstance
getType:工厂方法处于不同类时使用
newType

小结:静态方法通常更加适合,因此应优先考虑静态工厂。

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

静态工厂和构造器有个共同的局限性:都不能很好地扩展到大量的可选参数。
1. 重叠构造器
提供第一个只有必要参数的构造器,第二个参数有一个可选参数,第三个有两个可选参数…最后一个包含所有可选参数。
重叠模式可行,单遇到需要许多参数时,客户端代码会很难编写,且可读性变差。
2. JavaBeans模式。
即无参构造方法+setters
这种方式弥补了重叠构造器的不足,但自身也有严重的缺点:在构造过程中JavaBean可能处于不一致的状态。另外JavaBeans模式阻止了把类变成不可变的可能。
3. Builder模式
既能保证重叠构造器的安全性,也具有JavaBeans模式的可读性。
不直接生成想要的对象,而是让客户端利用所有参数调用构造器(或静态工厂)得到一个Builder对象,然后客户端通过builder对象的setter设置每个相关的可选参数,最后调用无参的build方法生成不可变的对象。这个builder是它构建的类的静态成员类。

pubic class Out{
    private int f1;
    private int f2;
    private int f3
    ...
    public static class Builder{
        private int b1;
        private int b2;
        private int b3;
        ...
        public Builder(int b1){
            this.b1 = b1;
        }

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

        public Builder b3(int b3){
            this.b3 = b3;
            return this;
        }
            ...
        public Out build(){
            return new Out(this);
        }
    }

    public Out(Builder builder){
        this.f1 = builder.b1;
        this.f2 = builder.b2;
        this.f3 = builder.b3;
        ...
    }
}

builder像个构造器一样,可以对参数强加约束条件。
Builder模式的优势:
1. builder可以有多个可变参数
2. 十分灵活,可以用一个builder构建对个对象。
3. 设置了参数的builder生成了一个很好的抽象工厂,可以作为参数传给方法为客户端提供一个或多个对象。
Builder模式的缺点:
1. 创建对象之前要创建它的构建起

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

Singleton通常被用来代表那些实质上唯一的系统组件,比如窗口管理器或者文件系统。singleton会使客户端测试变得困难。
实现Singleton的方法:
在Java1.5之前实现Singleton有两种方法,都是构造器私有,并导出公有静态成员。

1. 第一种方法:公有静态成员是final域

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

2. 第二种方法:公有的成员是个静态工厂方法

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

第一种公有域方法的好处在于组成类的成员的声明很清楚的表明了这个类是一个Singleton,公有域方法在性能上相对静态工厂方法不再有优势,现代的JVM实现几乎能够将静态工厂方法的调用内联化。

第二种静态工厂方法的优势在于其灵活性,在不改变其API的前提下,我们可以改变该类是否应该为Singleton的想法,例如改成每个线程返回唯一实例。
注意:以上方法通过反射机制可以调用私有构造器,如果要抵御这种攻击可以修改构造器,使其第二次实例化时抛出异常。

3. Singleton的序列化
仅仅实现Serializable是不够的,这样每次反序列化一个序列化实例时都会创建一个新实例。为了保证单例,必须声明所有实例域都是transient,并提供一个readResolve方法

4. Java1.5开始,实现Singleton的第三种方法:包含单个元素的枚举类型

public enum Elvis{
    INSTANCE;
}

这种方法更加简洁,无偿提供了序列化机制,绝对防止多次实例化,即使是在面对复杂的序列化或者反射攻击时。单元素的枚举类型已经成为实现Singleton的最佳方法。

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

有些类不需要实例化,实例化对它没有任何意义。这时候可以通过私有化构造器阻止实例化:

public class UtilityClass{
    private UtilityClass(){
        // 避免在类的内部调用构造器
        throw new AssertinonError();
    }
}

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

1. 能复用的对象不要在每次需要时都创建。
反面例子

// "stringette"本身就是一个String
String s = new String("stringette")

2. 对于同时提供了静态工厂方法和构造器的不可变类,通常使用静态工厂方法而不是构造器,以避免创建不必要的对象。例如Boolean.valueOf(String)Boolean(String),构造器每次调用都会创建新的对象,而静态工厂方法则不然。

3. 复用已经不会修改的可变对象

//检测一个人是否是在生育高峰期(1946-1964)出生
//反例
public class Person{
    private final Date birthDate;
    public boolean isBabyBoomer(){
        Calendar cal = Calendar.getInstance(TimeZone.getTimZone("GMT"));
        cal.set(1946,Calendar.JANUARY,1,0,0,0);
        Date boomStart = cal.getTime;
        cal.set(1965,Calendar.JANUARY,1,0,0,0);
        Date boomEnd = cal.getTime;
        return birthDate.compareTo(boomStart)>=0 && birthDate.compareTo(boomEnd)<0;
    }
}

//优化后,如果isBoomer被频繁地调用,这种方法将会显著地提高性能。
public class Person{
    private final Date birthDate;
    private static final Date BOOM_START;
    private static final Date BOOM_END;

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

但是如果改进的Person的isBabyBoomer方法永远不会调用,那就没必要初始化BOOM_START和BOOM_END.通过延迟初始化可以将其初始化推迟到第一次调用isBabyBoomer方法时,但是不建议这样做,延迟初始化通常使方法的实现更加复杂,从而无法将性能显著提高到最高水平。

4. 适配器

5. 自动装箱

// 下面的程序因为sum时Long类型而不是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);
}

因此,要优先使用基本类型而不是装箱基本类型,要当心无意识的自动装箱。
不要错误地认为“创建对象的代价高昂,我们应该尽可能避免创建对象”,相反,小对象的构造器工作量很少,创建和回收动作非常廉价,通过创建附加的对象,提升程序的清晰性、简洁性和功能性,通常是件好事。
当你应该重用现有对象时,请不要创建新的对象;当你应该创建新对象时,请不要重用现有的对象。

第6条 消除过期的对象引用

1. 类自己管理内存
考虑简单栈的实现案例:

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];
    }

    pubic void ensureCapacity(){
        if(elements.length=size){
            elements = Arrays.copyOf(elements, 2*size+1);
        }
    }
}

这段程序并没有明显的错误,但是这段程序隐藏这一个问题:内存泄漏。因为从Stack仍然保留着pop出的对象的引用,这回导致pop出的对象无法回收,栈内维护着对这些对象的过期引用。所谓过期引用,是指永远也无法再被解除的引用。
这类问题的修复方法很简单:一旦引用过期,只需清空这些引用即可。
pop方法修正如是:

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

清空过期引用的另一个好处是,如果它们后来被错误地引用,程序会抛出NullPointerException,而不是瞧瞧运行下去。
并不是每一个对象再使用完之后都要手动清空,这样做既没必要,也不是我们所期望的。清空对象引用应该是一种例外,而不是一种规范。 消除过期引用最好的办法是让包含该引用的对象结束其生命周期。
一般而言,只要类是自己管理内存,程序员就应该警惕内存泄漏。 以以上的Stack为例,存储池包含了elements数组,数组活动区域是已分配的,其他非活动部分这是自由的,但是对于垃圾回收器而言,elements数组的每所有对象引用都是等价的,只有程序员知道非活动部分是不重要的。

2. 内存泄漏的另一个常见来源是缓存
一旦引用放入缓存,就很容易被遗忘。
对于这种问题有几种解决方案:
(1)如果要实现:只要在缓存之外存在对某项的键的应用,该项就有意义,那就可以用WeakHashMap代表缓存,一旦缓存中的项过期后,他们就会自动被删除。记住,只有当所要的缓存项的生命周期是由该键的外部引用而不是由值决定时,WeakHashMap才有用。
(2)更常见的情形是缓存项的生命周期是否有意义并不是很容易确定,随着时间的推移,其中的项变得越来越没有价值。在这种情况下,缓存应该时不时清除无用项。这项清理工作可通过开启线程,利用LinkedHashMap的removeEldesEntry方法实现。对于更加复杂的缓存必须直接使用java.lang.ref

3. 内存泄漏的第三个常见来源是监听器和其他回调
例如客户端实现了一个API,并在API中注册回调,却没有注销的操作。解决这种问题的最佳方法时只保存他们的弱引用。

第7节 避免使用终结方法

(1)终结方法会导致行为不稳定,降低性能以及可移植的问题,应该避免使用终结方法。
(2)终结方法的缺点是不能保证会被及时地执行,注重时间的任务不能由终结方法来执行。
(3)及时执行终结方法这种算法在不同的JVM实现中会大相径庭。
(4)终结方法的优先级比其他线程要低得多。
(5)Java语言规范不仅不保证终结方法及时执行,而且根本不保证终结方法会执行。因此不应该依赖终结方法来更新重要的持久状态。
(6)System.gc和System.runFinalization只能增加终结方法被执行的机会,不保证终结方法一定会执行。唯一声称保证终结方法执行的方法System.runFinalizersOnExit和Runtime.runFinalizersOnExit因为致命的缺陷而被废弃。
(7)如果在终结方法中抛出异常,这种异常会被忽略,并且该对象的终结过程也会终止,未被补货的异常会使对象处于被破坏的状态。
(8)使用终结方法会有一个非常严重的性能损失
如果类的对象中封装了资源(例如文件或者线程)确实需要终止,只需提供一个显式的终止方法。典型例子如InputStream、OutputStream的close方法和Timer的cancel
显式的终止方法通常与try-finally结构结合起来使用,以确保及时终止。
终结方法的两种合法用途:
1. 显式终止方法忘记调用时,终结方法充当安全网。但是如果终结方法发现资源还未被终止,则应该在日志中记为一个bug,使用终结方法作为安全网要事先考虑清楚这种保护需要付出的代价。
2. 与对象的本地对等体有关。本地对等体是一个本地对象,与普通对象不同,垃圾回收器不会知道它。
如果子类覆盖了终结方法,子类的终结方法中必须手动调用超类的终结方法。你应该在一个try块中终结子类,并在相应的finally块中终结父类的终结方法:

@Override
Protected void finalize()throws Throwable{
    try{
        ...//终结子类状态
    }finally{
        super.finalize();
    }
}

终结方法守卫者(略)
总之,除非作为安全网,或者为了终止非关键的本地资源,否则请不要使用终结方法。极少情况下,如果使用了终结方法,记住要调用super.finalize()。如果要作为安全网请记住终结方法的非法用法,最后如果要把终结方法和公有的非final类关联起来,请考虑使用终结方法守卫者,以确保即使子类的终结方法没有调用super.finalize,该终结方法也能执行。

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值