Effective Java 创建和销毁对象(1~7)

来源:互联网 发布:oracle数据库表空间 编辑:程序博客网 时间:2024/06/03 04:54

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

public class Test{     private Test(){}     public static Test getInstance(){          return new Test();     }}

优点:
1. 静态工厂方法有名字,而构造器没有,所以我们可以将名字取的更加形象,方便客户端理解,而不像构造器传不同的参数并不知道是因为什么.
2. 不必每次调用都创建新的对象,这使得有一些不可变类或可以重复利用的类可以将构件好的实例缓存起来.
3. 可以返回原返回类型的任何子类型的对象.对客户端隐藏了具体实现.这在架构时非常好用,返回的对象可能随着版本的不同而不同,客户端可以在不修改代码的情况下升级版本.

缺点:
1. 类如果不含公有的或者受保护的构造器,则没办法被继承.(但这一定程度上也让大家多使用复合,而不是继承)
2. 与其他的静态方法本质上没有区别


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

public class Test {    private Integer test1;    private Integer test2;    private Integer test3;    private Integer test4;    private Integer test5;    private Test(Builder builder) {        this.test1 = builder.test1;        this.test2 = builder.test2;        this.test3 = builder.test3;        this.test4 = builder.test4;        this.test5 = builder.test5;    }    public static class Builder {        private Integer test1;        private Integer test2;        private Integer test3;        private Integer test4;        private Integer test5;        public Builder(Integer test1, Integer test2) {            this.test1 = test1;            this.test2 = test2;        }        public Builder test3(Integer test3) {            this.test3 = test3;            return this;        }        public Builder test4(Integer test4) {            this.test4 = test4;            return this;        }        public Builder test5(Integer test5) {            this.test5 = test5;            return this;        }        public Test build() {            return new Test(this);        }    }}

客户端可以这样构造Test实例:

Test test = new Test.Builder(1, 2).test3(3).test4(4).test5(5).build();

使用Builder模式的原因:
1.当参数过多时,使用构造器十分的不方便,可读性差.
2.如果使用JavaBeans模式(就是提供无参构造器,使用set方法设置实例的域)的话就将构造过程分到了多处调用中,在构造的过程中,实例可能处于不一致的状态,这就需要额外去保证初始化过程中的线程安全.


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

1.通过私有构造函数实现单例

public class Singleton {    private Singleton() {    }    private static final Singleton SINGLETON = new Singleton();    public static Singleton getSingleton() {        return SINGLETON;    }}

问题:
1)通过反射机制可以打破这种单例.
2)序列化反序列化会打破这种单例,所以要保证每个实力域都是瞬时的,并且要加入readResolve()方法.

2.通过编写一个包含单个元素的枚举类型实现单例

public enum Singleton {    SINGLETON;}

不会出现上面提到的两个问题.


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

有时候,我们可能会需要一些只包含静态方法和静态域的类,在实际的工作中,这种类通常被称为工具类.

这种类实例化是没有意义的,所以我们并不想让这种类可以被实例化.

将该类定义成抽象类是一种方法,但是抽象类是可以被继承的,所以一般的做法是将自动提供的无参构造器私有化:

public class Test {    private Test() {    }}

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

一般来说,能重用的对象就重用而不是在每次需要的时候就创建一个相同功能的新对象.

1.重用不可变对象

一个极端的反例:

String s = new String("a");

该语句每次执行都会创建一个新的String实例,但其实”a”本身就是一个String实例,并且可被重用.

2.重用可变对象

public class Person {    private final Date brithDate;    public Person(Date brithDate) {        this.brithDate = brithDate;    }    public boolean check() {        Calendar calendar = Calendar.getInstance();        calendar.set(1946, Calendar.JANUARY, 1, 0, 0, 0);        Date start = calendar.getTime();        calendar.set(1965, Calendar.JANUARY, 1, 0, 0, 0);        Date end = calendar.getTime();        return brithDate.compareTo(start) >= 0 &&                brithDate.compareTo(end) < 0;    }}

上述例子中check是判断一个人的出生日期是否在1946年到1965年之间,但是如果这个方法的调用量特别大时,就会频繁创建Calendar实例,我们可以这样做:

public class Person {    private final Date brithDate;    public Person(Date brithDate) {        this.brithDate = brithDate;    }    private static final Date START;    private static final Date END;    static {        Calendar calendar = Calendar.getInstance();        calendar.set(1946, Calendar.JANUARY, 1, 0, 0, 0);        START = calendar.getTime();        calendar.set(1965, Calendar.JANUARY, 1, 0, 0, 0);        END = calendar.getTime();    }    public boolean check() {        return brithDate.compareTo(START) >= 0 &&                brithDate.compareTo(END) < 0;    }}

这种方法只有在类加载的时候会创建一次.你可能会说如果这个方法永远不被调用,那就没有必要初始化了,是不是可以使用延迟加载.实际上,如果使用延迟加载会使方法实现更加复杂,可能无法将性能显著提高到已达到的水平.(这本书时刻在提醒我们,不要做一些自认为好的操作.)

3.自动装箱

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

由于Java 1.5之后的自动装箱功能,上面这个例子在被调用时,由于sum使用了Long而不是long,意味着程序大约要构造2的31次方个多余的Long实例.


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

虽然Java不用我们手动的去申请和释放内存,而是使用了GC做到了自动清理内存,但这并不代表Java没有内存泄露的问题.

如下一个简单的Stack的例子:

public class Stack {    private Object[] elements;    private int size;    private static final int DEFAULT_CAPACITY = 16;    public Stack() {        elements = new Object[DEFAULT_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.copyOf(elements, size * 2 + 1);        }    }}

上述类可能在使用时也能保证一个栈的基本功能,也可以正常使用,但是有一个非常严重的问题,他可能会造成内存泄露,主要是因为pop()方法中,虽然我们将size减小,但实际上数组上Object的引用还是被elements持有者,所以垃圾回收器并不会回收该Object.

我们可以将pop方法改为:

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

这样就不会造成内存泄露,因为我们将引用从elements中移除了.

这个例子并不是告诉我们需要过分小心的对每个引用不使用时就清空,这样会使得代码变得非常乱.实际上,通常如果类是自己管理内存的,我们就应该警惕内存泄露的问题.


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

protected void finalize() throws Throwable { }

这个方法相信大家都挺陌生的,他是Java中的终结方法,Object类中方法.

Java终结方法类似C++中的析构器,但是和析构器又有很大的差别.

当GC回收对象时,会去执行finalize()方法,所以finalize()方法的执行时不及时的,因为GC不是随时都在不停的回收内存的.并且终结方法线程的优先级比较低,也不能保证被及时执行.
并且Java语言规范不仅不保证终结方法会被及时执行,甚至不保证它们一定会被执行.比如一个程序终止时,某些已经无法访问的对象上的终结方法缺根本没有被执行,也是有可能的.

所以,我们不应该依赖终结方法来更新重要的持久状态.例如,如果依赖终结方法来释放数据库的永久锁,很容易让整个系统垮掉.

那我们如何处理这种问题,我们一般会提供一个显示的终止方法,要求该类的客户端在不使用时调用.比如InputStream或java.sql.Connection上的close()方法.

其实终止方法还是有点用的:
1.当客户端忘记调用显示的终止方法,终结方法虽然不能保证及时执行,但是迟一点释放资源总比不释放要好.
2.当我们调用本地方法时,可以利用终结方法去释放一些本地对象(因为GC也并不知道本地对象的存在,但也是建立在本地对象并不拥有关键资源的前提下,不然还是建议使用显示的终止方法).

这里有一个问题值得注意,就是终结方法链并不会被自动执行,也就是说如果调用了子类的终结方法,并不是自动去调用父类的终结方法,所以我们必须要在子类的终结方法里显示调用父类的终结方法(super.finalize()).

注:为了防止一些子类忘记或者故意不去调用父类的终结方法,第22条中我们会介绍把终结方法放在一个匿名的类中来终结外围实例,它被称为终结方法守卫者.

0 0
原创粉丝点击