和我一起学Effective Java之创建和销毁对象

前言

主要学习创建和销毁对象:

  • 1.何时以及如何创建对象
  • 2.何时以及如何避免创建对象
  • 3.如何确保它们能够适时地销毁
  • 4.如何管理对象销毁之前必须进行的清理动作

正文

一、用静态工厂方法代替构造器

获取类的实例的常用方法有:

  • 1.公有的构造器
  • 2.公有的静态工厂方法

下面通过Boolean类(基本类型boolean的包装类)的简单示例来学习:

//公有的构造器
  public Boolean(String s) {
        this(parseBoolean(s));
    }
//公有的静态工厂方法
public static Boolean valueOf(boolean b) {
        return (b ? TRUE : FALSE);
    }

静态工厂方法相对于构造器的优势:

1.有名称

具有适当名称的静态工厂方法更易使用,产生的代码更易阅读。可用名称来突出它们之间的区别。

如构造器BigInteger(int,int,Random)返回的BigInteger可能为素数,若用名为BigInteger.probablePrime的静态工厂方法来表示,显得更加清楚。

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

不可变类可以使用预先构建好的实例,或者是将构建好的实例缓存起来,进行重复利用,从而避免创建不必要的重复对象。

例如之前的Boolean的静态工厂方法就说明了这项技术,这种方法类似于FlyWeight模式。如果程序经常请求创建相同的对象,并且创建对象的代价很高,这项技术极大地提升了性能。

//缓存起来的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);
    }

静态工厂方法为重复的调用返回相同的对象。

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

4.创建参数化类型实例时,可使代码变得更加简洁

由于《Effective Java》这本书在编写的时候,JDK1.7还没有推出,因此在调用参数化类的构造器时,类型参数都必须要指明。而静态工厂方法能替我们实现类型推导的功能。

  //JDK7之前
        Map<String,List<String>> map = new HashMap<String,List<String>>();
  //JDK7
        Map<String,List<String>> m = new HashMap<>();
   //使用静态工厂方法
   public class MyHashMap extends HashMap {
    public static <K,V> HashMap<K,V> newInstance(){
        return new HashMap<K,V>();
    }
   
   //静态工厂方法实现类型推导 
   Map<String,List<String>> m = MyHashMap.newInstance();
}

静态工厂方法的缺点:

  • 1.类如果不含公有的或受保护的构造器,就不能被子类化。
  • 2.静态工厂方法和其他的静态方法实际上没有任何区别。
/*
 *静态工厂方法的惯用名称 
 */
//valueOf:返回的实例与它的参数具有相同的值
//String.valueOf(int)方法
 public static String valueOf(int value) {
        return Integer.toString(value);
    }
 
 //of:valueOf的简洁版
 //EnumSet.of(E)方法
 public static <E extends Enum<E>> EnumSet<E> of(E e) {
        EnumSet<E> set = EnumSet.noneOf(e.getDeclaringClass());
        set.add(e);
        return set;
    }   
    
  //getInstance:返回的实例是通过方法的参数来描述的。
  //newInstance:返回的每个实例与其他的所有实例不同。
  //getType:Type表示工厂方法返回的对象类型
  //newType:与newInstance类似
   

二、多个构造器参数考虑用构建器

静态工厂和构造器的局限性:不能很好地扩展到大量的可选参数

对于有大量的参数的类的编写:

/**
 *重叠构造器模式
 *提供一个只有必要参数的构造器,第二个有一个可选参数,第二个有两个,依此类推。
 */
 
 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 NutritionFacts(int servingSize,int servings){
        this(servingSize,servings,0);
    }
    /**
     * 有一个可选元素的构造方法,以下依此类推
     */
    public NutritionFacts(int servingSize,int servings,int calories){
        this(servingSize,servings,calories,0);
    }
    
    public NutritionFacts(int servingSize,int servings,int calories,int fat){
        this(servingSize,servings,calories,fat,0);
    }
    
    public NutritionFacts(int servingSize,int servings,int calories,int fat,int sodium){
        this(servingSize,servings,calories,fat,sodium,0);
    }
    
    public NutritionFacts(int servingSize,int servings,int calories,int fat,int sodium,int carbohydrate){
        this.servingSize = servingSize;
        this.servings = servings;
        this.calories = calories;
        this.fat = fat;
        this.sodium = sodium;
        this.carbohydrate = carbohydrate;
    }
}

 

此方法虽可行,但如果有许多参数时,客户端代码很难编写,并且难以阅读

/**
 *JavaBeans模式
 *无参构造方法创建对象,使用setter方法设置必选或可选参数。
 */
 public class NutritionFactsWithJavaBeans {
    /**
     * 必选元素
     */
    private int servingSize;
    private int servings;
    /**
     * 可选元素
     */
    private int calories;
    private int fat;
    private int sodium;
    private int carbohydrate;

    /**
     * 无参构造方法
     */
    public NutritionFactsWithJavaBeans(){

    }
    /**
     * Setter方法
     */
    public void setServingSize(int servingSize) {
        this.servingSize = servingSize;
    }

    public void setServings(int servings) {
        this.servings = servings;
    }

    public void setCalories(int calories) {
        this.calories = calories;
    }

    public void setFat(int fat) {
        this.fat = fat;
    }

    public void setSodium(int sodium) {
        this.sodium = sodium;
    }

    public void setCarbohydrate(int carbohydrate) {
        this.carbohydrate = carbohydrate;
    }

    public static void main(String[] args) {
        NutritionFactsWithJavaBeans cocaCola = 
                new NutritionFactsWithJavaBeans();
        
        cocaCola.setServingSize(240);
        cocaCola.setServings(8);
        cocaCola.setCalories(100);
        cocaCola.setSodium(35);
        cocaCola.setCarbohydrate(27);
    }
}

JavaBeans模式创建实例容易,代码易读。但有很严重的缺点

  • 1.构造过程中可能出现不一致的状态,调试困难。
  • 2.因为有set方法,使得在JavaBeans模式中,不能将类变为不可变的,需要额外的努力来确保它的线程安全。

重叠构造器的安全性+JavaBeans模式的可读性====>Builder模式

易写易读,模拟了具名的可选参数。build方法可检验约束条件。

public class NutritionFactsWithBuilder {
    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 int sodium = 0;
        private int carbohydrate = 0;

        public Builder(int servingSize, int servings) {
            this.servingSize = servingSize;
            this.servings = servings;
        }

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

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

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

        public Builder carbohydrate(int carbohydrate){
            this.carbohydrate = carbohydrate;
            return this;
        }
        public NutritionFactsWithBuilder build(){
            return new NutritionFactsWithBuilder(this);
        }
    }

    private NutritionFactsWithBuilder(Builder builder){
        servingSize = builder.servingSize;
        servings = builder.servings;
        calories = builder.calories;
        fat = builder.fat;
        sodium = builder.sodium;
        carbohydrate = builder.carbohydrate;
    }

    public static void main(String[] args) {
        NutritionFactsWithBuilder nutritionFactsWithBuilder
                = new NutritionFactsWithBuilder.Builder(20,30).calories(3).build();
    }

}

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

Singleton表示仅仅被实例化一次的类。

实现Singleton的方法:

  • 1.public static final域实现
public class SingletonClazz{
   //public属性将本类唯一实例暴露出去
   public static final SingletonClazz INSTANCE = new SingletonClazz();
   //构造方法私有 保证全局唯一性
   private SingletonClazz(){
   }
   public void test(){
      System.out.println("test method");
   }
}

缺点:利用反射机制可调用到私有构造方法

 try {
            Constructor<SingletonClazz> constructor = SingletonClazz.class.getDeclaredConstructor();
            constructor.setAccessible(true);

            SingletonClazz clazz =  constructor.newInstance();
            clazz.test();
        } catch (NoSuchMethodException | IllegalAccessException | InstantiationException | InvocationTargetException e) {
            e.printStackTrace();
        }

如何防止调用私有构造方法,修改构造方法,在创建第二个实例的时候抛出异常即可。

 private SingletonClazz(){
        if(INSTANCE!=null)
            throw new RuntimeException("cannot create more than one instance of SingletonClazz");
    }
  • 2.静态工厂方法实现
public class SingletonClazz {

    private static final SingletonClazz INSTANCE = new SingletonClazz();
    private SingletonClazz(){

    }
    public static SingletonClazz getInstance(){
        return INSTANCE;
    }

    public void test(){
        System.out.println("test method");
    }
}

这个方法相比于之前的方法的优势是,灵活性,不需要更改API的前提下,可以改变该类是否应该为Singleton。

使Singleton类变成为可序列化的:

  • 实现Serializable接口
  • 需要声明所有实例为瞬时的(transient)
  • 提供一个readResolve()方法。

3.编写一个包含单个元素的枚举类型来实现

public enum SingletonClazz {
    INSTANCE;
    
    public void test(){
        System.out.println("test method");
    }
}

优点:简洁,并提供了序列化机制,而且能保证不会被多次实例化(即使是面对复杂的序列化或是发射攻击的时候)----->实现Singleton的最佳方法

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

在缺少显式构造器的情况下,编译器会自动提供一个公有的,无参的缺省构造器

public class DefaultConstructor {
    //没有显示构造器
    public static void main(String[] args) {
        //使用编译器提供的公有的,无参的缺省构造器
        DefaultConstructor defaultConstructor = new DefaultConstructor();
    }
}

通过将类做成抽象类来实现不可实例化的目的是不可取的。继承抽象类,子类也可以被实例化。同时也会误导用户,以为是为了继承而设计的。

public abstract class AbstractClazz {
   //通过抽象类来实现不可实例化是不可取的
   public static void main(String[] args) {
       //仍旧可以通过继承抽象类,来达到实例化子类的目的
       SubClazz subClazz = new SubClazz();
   }
}

class SubClazz extends AbstractClazz{
    
}

正确做法是:将构造器显式地声明为私有的。

public class UtilityClazz {
    //防止类被实例化
    private UtilityClazz(){
        throw new AssertionError();
    }

}

避免创建不必要的对象

最好能重用对象,而不是在每次需要的时候创建一个相同功能的新对象。

重用不可变对象。

public class NoNeedObject {

    public static void main(String[] args) {
        String s1 = new String("12345");//错误做法
        String s2 = "12345";//正确做法
        String s3 = "12345";
        String s4 = new String("12345");

        check(s1,s2);//不同
        check(s2,s3);//相同 重用了对象
        check(s1,s4);//不同

    }
    public static void check(String one,String two){
        //==比较的是两个引用是否指向同一个对象
        if(one == two)
            System.out.println("same address");
        else
            System.out.println("different address");
    }
}

重用那些已知不会被修改的可变对象。

class Person{
    private final Date birthDate;

    public Person(Date birthDate){
        this.birthDate = birthDate;
    }

    public boolean isBabyBoomer(){
        //没有必要的对象创建
        //每次判断都会生成Calendar,Date,TimeZone实例
        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&&birthDate.compareTo(boomEnd)<=0;
    }
}

用一个静态的初始化器来避免上面那种效率低下的情况。

class Person{
    private final Date birthDate;

    public Person(Date birthDate){
        this.birthDate = birthDate;
    }

    private static final Date BOOM_START;
    private static final Date BOOM_END;

    static {
        System.out.println("创建Calendar等对象");
        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();
    }

    public boolean isBabyBoomer(){
        return birthDate.compareTo(BOOM_START)>=0&&birthDate.compareTo(BOOM_END)<=0;
    }
   }

自动装箱(JDK5引入):Java编译器能在基本类型和包装类之间自动转换。如intInteger,doubleDouble等等。

相关学习链接:
1.https://docs.oracle.com/javase/tutorial/java/nutsandbolts/datatypes.html
2.https://docs.oracle.com/javase/tutorial/java/data/autoboxing.html

 public static void main(String[] args) {
        //使用包装类Long
        long beforetime = System.currentTimeMillis();
        Long sum = 0L;
        for(long i = 0;i<Integer.MAX_VALUE;i++){
            sum+=i;
        }
        long aftertime = System.currentTimeMillis();

        System.out.println("Long--->sum="+sum);
        System.out.println("time="+(aftertime-beforetime));

        //使用基本类型long
        beforetime = System.currentTimeMillis();
        long sum2 = 0L;
        for(long i = 0;i<Integer.MAX_VALUE;i++){
            sum2+=i;
        }
        aftertime = System.currentTimeMillis();
        System.out.println("long--->sum="+sum2);
        System.out.println("time="+(aftertime-beforetime));

 }

运行结果:

Long--->sum=2305843005992468481
time=9642
long--->sum=2305843005992468481
time=777

结论:优先使用基本类型而不是装箱基本类型,当心无意识的自动装箱。

注意: 很多规定只是建议,不要矫枉过正,犯了教条主义的错误,一定要与实际的开发情况结合,实事求是。

不要错误地认为“应该尽可能地避免创建对象”,应该是“避免创建不必要的对象”,注意是不必要的!

通过维护自己的对象池(object pool)来避免创建对象并不是一种好的做法,除非池中的对象非常重要。

消除过期的对象引用

手工管理内存的语言:C或C++

具有垃圾回收功能的语言:Java,简化程序员的工作

public class Stack {
    private Object[] elements;
    private int size = 0;
    private static final int DEFAULT_INITIAL_SIZE = 16;

    public Stack(){
        elements = new Object[DEFAULT_INITIAL_SIZE];
    }

    public void push(Object e){
        ensureCapacity();
        elements[size++] = e;
    }

    /**
     * stack: push(1) push(2) push(3) push(4) push(5) pop() pop()
     *      5
     *      4
     *      3  ---->栈顶
     *      2
     *      1  
     *      
     *      栈内部维护着过期的引用,也就是永远不会再被解除的引用,如4和5
     *    fixed:
     *        elements[size]=null;
     *        
     */
    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);
        }
    }
}

上面这段代码存在内存泄露的问题,当栈先是增长,然后弹出,从栈中弹出的对象都不会被当做垃圾回收。

栈内部维护着这些对象的过期引用,也就是永远不会被解除的引用。栈数组中下标大于或等于size的元素的引用都是过期的引用。

在支持垃圾回收的语言中, 内存泄露(也就是无意识的对象保持)很隐蔽。

修复办法:一旦对象引用过期,就清空这些引用。

注意:清空对象引用应该是一种例外,而不是一种规范行为。

容易导致内存泄露的几种情形:

  • 类自己管理内存
  • 缓存
  • 监听器和其他回调

避免使用终结方法

终结方法(finalizer):不可预测,危险,一般情况下是不必要的。

终结方法的缺点:

  • 不能保证会被及时地执行,而且根本不会保证它们会被执行。(所以不应该依赖终结方法来更新重要的持久状态
  • 严重的性能损失

终止类的对象中封装的资源(文件或线程),只需提供一个显式的终止方法。不需要编写终结方法。

例子:

  • InputStream,OutputStream,java.sql.Connection的close方法
  • java.util.Timer的cancel方法

一般与try-catch结合起来使用,以确保及时终止

posted @ 2016-03-15 23:47  onerepublic  阅读(784)  评论(0编辑  收藏  举报