Effective Java读书笔记——第二章 创建和销毁对象

来源:互联网 发布:听相声的软件 编辑:程序博客网 时间:2024/06/13 09:33

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

每个类都可以提供一个公有的静态工厂方法(static factory method),这就是一个返回类的实例的静态方法:

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

上面的方法将boolean基本类型值转换成一个Boolean对象的引用,即Boolean类型的一个实例。

只公有的静态方法而不是构造器来提供类的一个实例有如下几点优势:

  • 静态工厂方法可以做到见名知意:调用构造器BigInteger( int, int, Random )可能返回一个素数,但是在没有注释的情况下很难得知这个构造器的作用,不过使用BigInteger.probablePrime的惊涛方法就显得更为清楚。

  • 不必每次调用静态工厂方法时都创建一个新的实例:这可以增加复用的几率,减少内存的开销。对于大型对象来说可以大大提高性能。这种不必每次都创建一个新的实例的类叫做实例受控的类(instance-controlled),这种类型确保该类是一个单例类(Singleton)不可直接实例化的类;另外,单例类还保证了a==b 与 a.equals(b)为true 互为充要条件,这样的话,可以使用==操作符代替equals方法,可以提升效率。

  • 静态工厂方法可以返回原来类型的任意子类型,灵活性大大提高。下面这个栗子中,Provider负责提供Service的实现实例,而所有的Provider都被保存于静态的Map

public interface Service {//Service中的具体方法}public interface Provider {    Service newService();}//负责保存Service的实例,获取、注册Service实例public class Services {    private Services() {    }    private  static final Map<String,Provider> providers = new ConcurrentHashMap<>();    public static final String DEFAULT_PROVIDER_NAME = "<def>";    //注册一个默认的Provider    public static void registerDefaulrProvider(Provider p){        registerProvider(DEFAULT_PROVIDER_NAME, p );}    public static void registerProvier(String name, Provider p)    {    providers.put(name,p);} //客户端调用的APIpublic static Service newInstance() {    return newInstance(DEFAULT_PROVEIDER_NAME);}public static service newInstance(String name) {Provider p = providers.get(name);if(p == null){throw new IllegalArgumentExecption("No provider registerd with name: " + name);}return p.newService();}}
  • 静态工厂方法使得实例化变得更加简洁:首先,使用常规的构造器形式实例化Map<String, List<String>> m = new HashMap<String,List<String>>;;而如果HashMap提供了静态方法
public static <k, v> HashMap<K,V> newInstance() {     return new HashMap<K , V>();

}

那么在调用端就会变得简单:

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

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

一句话: 重叠构造器可行。但是当有许多参数的时候。客户端代码会很难编写,并且仍较难以阅读。若读者相纸到那些值是啥意思,必须仔细了解这些参数的意义。这很容易导致错误:若不小心导致了错误,编译器也不会报错,但在运行时会出现错误行为。

另一种方式是使用JavaBeans的方式。这种方式可读性强,但JavaBeans存在一个先天的不足,那就是初始化一个对象的过程并不是一步完成,而要分成多次,若漏掉某个参数的初始化,那么查错会变得困难。

第三中方式兼顾了第一章方式的安全性和第二种方式的可读性。即Builder模式。不直接生成想要的对象,而是让客户端利用所有必要的参数调用构造器,得到一个builder对象。然后客户端在builder对象上调用类似于setter的方法,来设置每个相关的可选参数。最后,客户端调用午餐的build方法来生成不可变的对象。这个builder是它构建的类的静态成员类:

public 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 carbohydrate = 0;        private int sodium = 0;        //Builder构造方法        public Builder(int servingSize, int servings) {            this.servingSize = servingSize;            this.servidngs = servings;        }        public Builder calories(int val) {            calories = val;        }        public Builder fat(int val) {            fat = val;            return this;        }        public Builder carbohvdrate(int val) {            carbohvdrate = 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;    }}

那么客户端代码就可以写成:

NutritionFacts cocaCola = new NutritionFacts.Builder(240,8).calories(100).fat(0).sodium(35).build();

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

Singleton通常被用来代表那些本质上唯一的系统组件:

在JDK1.5之前,一般定义一个私有的构造器并导出公有的静态成员:

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

这种方式有一个问题:享有特权的客户端可以借助AccessibleObject.setAccessible方法,通过反射调用私有的构造器。

在JDK1.5 之前 还有一种方式实现单例类,即静态工厂方法:

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

JDK1.5版本以后,可以使用单元素的枚举类型创建单例类:

public enum Elvis {    INSTANCE;}...

这种方式目前是实现单例类的最佳方式。

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

略…

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

String s = new String("stringette");

若在一个循环中每次都创建一个String实例,是完全不必要的,实际上只需要这样:

String s  = "stringette";

这保证了在同一台虚拟机内,只要包含相同的字符串字面常量,该对象就会被重用。

自动装箱和拆箱也暗含着创建不必要的对象:

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

该循环每执行一遍,都会实例化一个Long的实例,这样相当耗费内存,指引自动装箱的缘故,只需要将Long 改为long。

有些对象的初始化很耗费资源,如Calendar类,对于这种类,根据实际的应用场合,只需要实例化一次,所以可以把该类的实例化放在静态初始化块中。

首先举个反例:

public class Person {    private final Date birthDate;    //错误的写法    public boolean isBatyBoomer() {        Canlendar gmtCal = Calendar.getInstance(TimeZone.getTimeZone("GMT"));        gmtCal.set(1946,Calendar.JANUARY,1,0,0,0);        Date boomStart = gmtCal.getTime();        gmtCal.set(1965,Calendar.JANUARY,1,0,0,0);        Date boomEnd = gmtCal.getTime();        return birthDate.compareTo(boomStart) >= 0 && birthDate.compareTo(boomEnd) < 0;    }}

每调用一次方法,都新建一个Calendar,一个TimeZone,两个Date对象,这是不必要的。

在静态初始化块中初始化Calendar,避免其重复创建:

public class Person {    private final Date birthDate;    private static final Date BOOM_START;    private static final Date BOOM_END;    static {        Calendar gmtCal = Calendar.getInstance(TimeZone.getImeZone("GMT"));        gmtCal.set(1946, Calendar.JANUARY,1,0,0,0);        BOOM_START = gmtCal.getTime();        gmtCal.set(1965,Calendar.JANUARY,1,0,0,0);        BOOM_END = gmtCal.getTime();    }    public boolean isBabyBoomer() {        return birthDate.compareTo(BOOM_START) >= 0 && birthDate.compareTo(BOOM_END) < 0;    }}

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

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

上述代码中存在内存泄漏的风险,当元素从数组中弹栈的时候,我们只是单纯地将数组的范围减少1,但是并没有把该范围之外的数组中引用的对象释放掉,这就造成了过期的引用无法被释放的问题,从而造成了内存泄漏。

解决办法是,将那些过期的引用置空:

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

这种做法的另一个好处是,当数组中过期的引用被错误的引用时,会抛出NullPointerExecption异常。

内存泄漏还有一种常见的来源,就是缓存——一旦把对象引用放到缓存中,它就很容易被遗忘掉,从而使得它不再有用之后很长一段时间仍然留在缓存中。

推荐几篇有关内存泄漏的文章,值得一读~:
1、内存泄露从入门到精通三部曲之基础知识篇
2、内存泄露从入门到精通三部曲之常见原因与用户实践
3、内存泄露从入门到精通三部曲之排查方法篇


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

总结起来就一句:尽量避免使用终结方法。

原因是:终结方法不能保证会被及时地执行——从一个对象变得不可到达(即没有任何引用再只想这个对象)到它的终结方法被执行(finalize()执行),这段时间是任意长的,不可控的;第二,不使用finialize()作为回收资源的方式,是因为不同的JVM回收算法实现起来大相径庭,时间不一样;第三,finialize()方法根本不保证会被执行。

0 0
原创粉丝点击