Effective Java 学习笔记——第二章

来源:互联网 发布:郑州网络销售抓人 编辑:程序博客网 时间:2024/06/06 00:04

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

静态工厂方法的优势:

1. 更加语义化

较之于new A() 的构造方法,静态工厂方法有函数名称,函数的功能描述更加清晰;

2. 不必在每次调用时都创建新的对象(即允许单例)

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

可以根据传入的参数、所需的功能,返回所需要的子类型对象。
如下代码,传入一个对象后,静态工厂方法根据传入对象的长度来返回不同的枚举类型:小于等于64个元素,返回RegalarEumSet实例;大于64个元素,返回JumboEnumSet实例。

  public abstract class EnumSet<E extends Enum<E>> extends AbstractSet<E>    implements Cloneable, java.io.Serializable {    EnumSet(Class<E>elementType, Enum[] universe) {    }    //RegularEnumSet与JumboEnumSet均为EnumSet的子类    public static <E extends Enum<E>> EnumSet<E> noneOf(Class<E> elementType) {        if (universe.length <= 64)            return new RegularEnumSet<>(elementType, universe);        else            return new JumboEnumSet<>(elementType, universe);    }}

4. 使创建对象更加简洁

对比如下:

//常规实例化方式Map<String, List<String>> m =    new HashMap<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. 与其他静态方法没有本质区别

不能够突出静态工厂方法的构造特点。




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

遇到多个构造器参数,且有必需与可选参数时,向对象传入属性的方法有如下三种(以下例子中,name、age为必需属性,sex、height、QQ为可选属性):

1. 重载构造器

public People(String name,int age){...}public People(String name,int age, char sex){...}public People(String name,int age, char sex, int height){...}public People(String name,int age, char sex, int height, int QQ){...}

由上可见,重载构造器的方法过于复杂,不够语义化,且容易出现错误(如age与height参数输入反了,并不会产生错误提示)不适于多个参数。


2. JavaBeans

JavaBeans即通过set方法对属性进行逐一注入,相对语义化一些,但仍然非常繁琐,且容易使JavaBean处于不一致状态(如一个只set了A属性,一个只设置了B属性,这两个实例不一致,不能保证通过该类的同一个构造器保证构造出来的对象是属性相同的),以及阻止了把类做成不可变的可能


3. Builder

public class People{    private final String name;    private final int age;    private final char sex;    private final int height;    private final int QQ;    public static class Builder    {        private final String name;        private final int age;        private final char sex;        private final int height;        private final int QQ;        public Builder( String name, int age ){            this.name = name;            this.age= age;        }        public Builder sex( char sex ){            this.sex = sex ;            return this;        }        public Builder height( int height){            this.height= height;            return this;        }        public Builder QQ( int QQ ){            this.QQ= QQ;            return this;        }        public People build(){            return new People( this );        }    }    private People ( Builder builder ){        height= builder.height;        sex = builder.sex;        QQ = builder.QQ;    }}//call codePeople people = new People.Builder( "zhangsan", 8 ).height(150).sex('m').build();

如上所示,对于大量可选参数而言,Builder模式(构建器模式)更加简便,代码逻辑更加清晰。而对于可选参数较少的情况,Builder模式会显得繁杂一些。




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

本条有部分内容为扩展内容,参考来源http://www.cnblogs.com/blogofcookie/p/5793865.html。

1. 常见的4种单例模式实现方案:

①饿汉式

public class SingletonDemo01 {    //类初始化时,立刻加载这个对象(没有延时加载的优势)。线程是天然安全    private static SingletonDemo01 instance = new SingletonDemo01();    private SingletonDemo01(){}    //方法没有同步,线程安全,调用效率高。但是,不能延时加载    public static SingletonDemo01 getInstance(){        return instance;    }}

②懒汉式

public class SingletonDemo02 {    //类加载时,不初始化对象(延时加载:资源利用率高)    private static SingletonDemo02 instance;    private SingletonDemo02(){}             //synchronized 防止并发量高的时候,出现多个对象方法同步,调用效率低,    public static synchronized SingletonDemo02 getInstance(){        if(instance==null){//真正用的时候才加载            instance = new SingletonDemo02();        }        return instance;    }}

③静态内部类实现(实质是利用了静态内部类只在调用时初始化一次的特性,保证线程安全与延迟加载)

public class SingletonDemo04 {    //类加载时静态内部类不会加载,只有调用getInstance方法时,才会加载(实现延时加载)    private static class SingletonClassInstance {        private final static SingletonDemo04 instance = new SingletonDemo04();    }    private SingletonDemo04() {    }    // 线程安全,方法不同步,调用效率提高    public static SingletonDemo04 getInstance() {        return SingletonClassInstance.instance;    }}

④枚举式

public enum SingletonDemo05 {    INSTANCE;// 这个枚举元素,本身就是单例模式    // 添加自己需要的操作    public void singletonOperation() {...}}

以上四种方案对比如下:
饿汉式:线程安全,调用效率高(因为无锁),不能延迟加载,会被反射、序列化破坏单例安全;
懒汉式:线程安全,调用效率低(因为有锁),可以延迟加载,会被反射、序列化破坏单例安全;
静态内部类式:线程安全,调用效率高,可以延迟加载,会被反射、序列化破坏单例安全;
枚举式:线程安全,调用效率高,不能延迟加载,保证绝对单例安全;

选用方案:
单例对象占用资源少时,可以不延迟加载,选用枚举式;
单例对象占用资源多时,必须延迟加载,选用静态内部类式;


2. 破坏单例安全的方式

①克隆

只有实现Cloneable接口的类才可以使用clone方法,该方法从内存直接copy对象,绕开了构造器。所以,单例模式的类切勿实现Cloneable接口。

②反射

    public class Client {        public static void main(String[] args) throws Exception {            SingletonDemo06 s1 = SingletonDemo06.getInstance();            //使用反射方式直接调用私有构造器            Class<SingletonDemo06> clazz = (Class<SingletonDemo06>)Class.forName("com.bjsxt.singleton.SingletonDemo06");            Constructor<SingletonDemo06> c = clazz.getDeclaredConstructor(null);            c.setAccessible(true);//绕过权限管理,即在true的情况下,可以通过构造函数新建对象            SingletonDemo06 s3 = c.newInstance();            System.out.println(s1==s3);        }    }

通过反射机制,可调用私有构造器创建对象。
解决方案:修改构造器,在创建第二个实例时抛出异常。枚举式无此问题。

③序列化

public class Client {    public static void main(String[] args) throws Exception {        SingletonDemo06 s1 = SingletonDemo06.getInstance();        //通过反序列化的方式创建多个对象        FileOutputStream fos= new FileOutputStream("d:/a.txt");        ObjectOutputStream oos = new ObjectOutputStream(fos);        oos.writeObject(s1);        oos.close();        fos.close();        ObjectInputStream ois = new ObjectInputStream(new FileInputStream("d:/a.txt"));        SingletonDemo06 s5= (SingletonDemo06) ois.readObject();        System.out.println(s5==s1);    }}

在将对象持久化或远程传输时会涉及序列化,序列化与反序列化会导致创建一个新的对象,破坏了单例模式。
解决方案:定义readResolve函数,返回已创建的对象。枚举式无此问题。

private Object readResolve() {        return Singleton.instance;}



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

比如工具类,其所有方法均为静态方法,实例化对其没有任何意义,所以应将其构造器私有化,以避免被错误实例化。

public class Utility{    private Utility(){        //内部调用构造器,抛出异常;外部禁止调用构造器。        throws new AssertionError();    }    ...//其他静态方法}



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

1. 能够重用对象的时候,就不要创建新的对象

for(int i=0;i<100;i++){    String a="a";    String b=new String("b");}

如上循环,由于String不可变性,只产生了一个a实例,和100个b实例。
所以采用创建a对象时的创建方法。

再如

public class Person{    private final Date birthDate;    //Donot do this    public boolean isBabyBoomer()    {        Calendar 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                && birthData.compareTo( boomEnd )<0;    }}

如上代码,isBabyBoomer被调用时,每次都会创建Calendar和Date对象(其中Calendar是大对象),消耗内存。
下面是对上面代码的改进方案,采用静态不可变属性与静态代码块实现。

//better implementspublic class Person{    private final Date birthDate;    private static final Date BOOM_START;    private static final Date BOOM_END;    static    {        Calendar gmtCal = Calendar.getInstance( TimeZone.getTimeZone( "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();    }    //Do this    public boolean isBabyBoomer()    {        return birthDate.compareTo( BOOM_START)>=0                && birthData.compareTo( BOOM_END)<0;    }}

该设计中,无论该函数被调用多少次,都只产生了一个Calendar对象,节约空间与性能。更好的设计应是能够延迟加载的。


2. 有静态工厂方法时,不要使用构造器方法创建对象

原因同1。构造器方法每次被调用都会产生一个新对象,而静态工厂方法则不一定,有时是为避免创建不必要对象而设立的。


3. 注意自动装箱与自动拆箱

如下代码

for(Long sum=0,long i=0;i<Integer.MAX_VALUE;i++){    sum+=i;}

由于sum定义为Long,在计算时Long会拆箱为long,计算完后又会装箱为Long,循环多次后极大消耗性能。所以应定义为基本数据类型long。


4. 关于对象池

小对象的创建与销毁,随着JVM的提升,已经非常完善。
对象池应关注大对象的反复创建与销毁,如数据库连接池,关注的是Connection对象的创建与销毁,该对象创建代价非常昂贵,因此重用这些对象非常有意义。




第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 Object pop(){        if(size==0){            throw new EmptyStackException();        }        return elements[--size];    }    ...//其他函数}

如上所示,由于Object[]长度不变,size只限定了能够使用的范围,这会导致范围之外的元素变为过期引用(永远也不会再被解除的引用)。这就导致内存泄露。使用久了,会导致内存溢出。
解决上述问题也很简单:

public Object pop(){    if(size==0){        throw new EmptyStackException();    }    Object[element]=null;//防止内存泄露    return elements[--size];}

2. 内存泄露的另一个来源是缓存

将强引用放入缓存中,在缓存失效之前,该强引用指向的对象不会被GC,占用空间。


3. 内存泄露的第三个来源是监听器和其他回调

确保回调立即被当做垃圾回收的最佳方法是只保存它们的弱引用




第7条 避免使用终结方法

1. 终结方法的缺点

因为终结方法的线程优先级很低,所以不能保证被及时执行。
终结方法对性能消耗严重。

2. 终结方法替代方案

显式终结,如Connection的close方法。

3. 终结方法应用场景

为本应显示终结但并未调用显示终结的对象充当安全网。
如Connection对象已经使用完毕,但并未显式调用,此时可以使用终结方法来终结该对象,虽然可能很久以后才会终结,但至少比不终结要好很多。
基本上,不建议使用终结方法,会带来很多问题。