如何正确的创建和销毁Java对象

Java是一门强大的高级语言。在学习了其基础知识后,我们仍需要理解其深刻的内涵。接下来,我们会以《Effective Java》一书做为Java进阶学习的载体,对Java进行一个系统的、全新的认识。接下来,就让我们来感受Java高深的内涵吧。


第一章:创建和销毁对象


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

  对于类而言,为了让客户端获取它自身的一个实例,我们最常用的方法就是提供一个公有的构造器了。但是,其实还有一种好方法(它应该在每个程序员的工具箱中占有一席之地),即是类提供一个公有的静态工场方法。(一个返回类的实例的静态方法,并不同于设计模式中的工厂方法模式
提供静态工厂方法而不是公有构造器有以下几大优势
  
  1. 静态工厂方法有名称而构造器没有自己的名称。
   如果构造器的参数本身没有确切地描述正被返回的对象,那么就会出现客户无法判断所拿到的东西是什么的尴尬局面了。举个栗子:构造器BigInteger(int,int,Random)返回的BigInteger可能为素数,如果用名为BigInteger,probablePrime的静态工厂方法来表示则更为清楚。(1.4发行版最后就增加了这个方法。)
   还有一种情况,就是当需要构建的内容一个构造器不够使用时,我们往往会提供两个构造器,这其实是很不好的方法,因为客户根本就不知道该用哪个构造器,调用错误的构造器时,就会出现数据出错。因此,可以使用静态工厂方法,并且为静态工厂方法选择能够突出其作用的名称。
   
  2.不必多次创建一个新对象
   这样不可变类就可以使用预先构建好的实例,或者将构建好的实例缓存起来,进行重复利用,从而避免创建不必要的重复对象。举个栗子:Boolean.valueOf(boolean)。它从来不创建对象!
   另外,还有一个小优点。静态工厂方法能够为重复的调用返回相同的对象,可严格控制实例的存在
   
  3.可以返回原返回类型的任何子类型的对象
   使用静态工厂方法时,要求客户端通过接口来引用被返回的对象,而不是通过它的实现类来引用被返回的对象。客户端永远不需要关心他们从工厂方法中得到的对象的类,只需要关心是某个类的某个子类即可。
   
  4.创建参数化类型实例时,使代码变得更加简洁
   在调用参数化类构造器时,即使类型参数很明显,还是必须声明。
   以创建一个Map类为例:

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();
//Java1.6还没有这种工厂方法,但你可以把这种方法放在你的工具类中。

  静态工厂方法的缺点:
  
  1.类如果不含公有的或者受保护的构造器,就不能被子类化
  
  2.它与其他的静态方法实际没有任何差别
   没有任何差别意味着什么呢?就是说,对于提供了静态工厂方法而不是构造器的类来说,没有像构造器那样在API文档中明确标识出来,因此,想要查明如何实例化一个类是很困难的。那该怎么办呢?其实还是有方法的,你可以通过在类或者接口注释中关注静态工厂,并遵守标准的命名习惯即可。

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

  上面讲到了静态工厂类,静态工厂类与构造器相比好处还是蛮多的,但是,两者有个共同的局限性:他们都不能很好地拓展到大量的可选参数。
  解决方法:
  
  1、我们一般想到的就是采用重叠构造器模式(可自行百度)

//演示调用代码
NutritionFacts cocaCola=new NutritionFacts(240,15,6,7,0,5,6);

  当有许多参数时,重叠构造器模式会让客户端代码很难编写,且难以阅读
  
  
  2、JavaBean模式

//演示调用代码
NutritionFacts cocaCola=new NutritionFacts();
cocaCola.setServingSize(240);
cocaCola.setServings(8);
cocaCola.setCalories(100);

  JavaBean模式也有很严重的缺点。因为构造过程被分到了几个调用中,所以构造过程中不能保证JavaBean的一致性。如果使用处于不一致状态的对象,会导致失败,且调试起来十分困难。另外,JavaBean模式阻止了把类做成不可变得可能,所以需要我们再费力气去确保其线程安全。(手动“冻结”对象)但是手动的方法十分笨拙,在实践中很少使用。
  
  幸亏,还有第三种方法。
  3、Builder模式
   原理:不直接生成想要的对象,而是让客户端利用所有必要的参数调用构造器(或者静态工厂),得到一个builder对象。然后客户端在builder对象上调用类似于setter的方法,设置每个相关的可选参数。最后调用无参的build方法生成不可变对象。弥补了前两种方法的不足。
   代码如下:

public class NutritionFacts{
    private final int servingSize;
    private final int servings;
    private final int fat;
    public static class Builder{
        //Required parameters
        private final int servingSize;
        private final int servings;


        private int fat=0;
        public Builder(int servingSize,int servings){
            this.servingSize=servingSize;
            this.servings=servings;
        }
        public Builder fat(int val){
            fat=val;
            return this;
        }

        public NutritionFacts builder(){
            return new NutritionFacts(this);
        }
    }
    private NutritionFacts(Builder builder){
        servingSize=builder.servingSize;
        servings=builder.servings;
        fat=builder.fat;
    }
}
//实验调用代码
NutritionFacts cocaCola=new NutritionFacts.Builder(200,8).fat(60).builder();
//创建成功  

  于构造器相比,builder的优势在于可以有多个可变(varargs)参数。构造器则只能有一个可变参数。又因为builder利用单独的方法来设置每个参数,要多少个就有多少个,要怎么调整就怎么调整。
  总之,如果类的构造器或者静态工厂中具有4个以上参数,设计这种类时就首选Builder模式,特别是当大多数参数都是可选的的时候。
  

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

   
   Singleton指仅被实例化一次的类。通常代表如窗口管理器或文件系统等本质上唯一的系统组件(使类成为Singleton会使它的客户端测试变得很困难,因为无法给Singleton替换模拟实现,除非它实现一个充当其类型的接口
   Java1.5发行前有两种方法实现Singleton。

第一种:公有静态成员为final域

public class Lin{
    public static final Lin INSTANCE =new Lin();//使用final修饰,清楚地表明了这个类是单例
    private Lin(){...}
    public void leaveTheBuilding(){...}
}

  由于缺少了公有的或者受保护的构造器,所以保证了Lin的全局唯一性:一旦Lin类被实例化,只会存在一个Lin实例,客户端的任何行为都不会改变这一点。但要注意:享有特权的客户端可以借助AccessibleObject.setAccessible方法,通过反射机制调用私有构造器。(若要抵御这种攻击,则可以修改构造器,在要创建第二个实例时抛出异常)

第二种:公有成员是静态方法

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

    public void leaveTheBuilding(){...}
}

  对于所有调用都会返回同一个对象引用,所以保证了只有一个Lin实例。
  
  以上两种方法皆是把构造器保持为私有的,并导出公有的静态成员变量,以便允许客户端访问该类的唯一实例

  但如今,以上的公有域方法在性能上不再有优势,因为现在的JVM实现几乎能将静态工厂方法的调用内联化。(是不是很奔溃,讲了那么久博主你居然跟我说上面的方法不再有优势,那有卵用?)别着急~凡是都要知道了它的内涵之后才能真正理解它高效运行的原理嘛。接下来就来介绍实现Singleton的最佳方法了

第三种:编写一个包含单个元素的枚举类型

public enum Lin{
    INSTANCE;

    public void leaveTheBuilding(){...}
} 

  无偿地提供了序列化机制,即使是在面对复杂的序列化或者反射机制的时候也能绝对防止多次实例化。

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

public class UtilityClass{
  private UtilityClass(){
       throw new AssertionError();
     }
     ...//Remainder omitted
     

  这种习惯用法有一点副作用,它使得一个类不能被子类化。子类就没有可访问的超类构造器可调用了

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

  伴侣。最好是一直陪伴的同一个,而不是在寂寞时随便约炮找对象。而对象,也最好是能够重用的,而不是每次需要时就创建一个相同功能的新对象。

  如果对象是不可变的,它就始终可以被重用。

  Don’t do this!

String s=new String("new");

  这样如果循环的话,会创建无数个不必要的实例

  正确打开方式

String s="reuse";

  对于已知不会被修改的可变对象也可以进行重用

class Person{
    private final Date birthDate;//出生就确定了,所以不可修改

    private static final Date BOOM_START;//final证明已被当做常量对待
    private static final Date BOOM_END;//同上

    static{//初始化时建立,因此之后都是不可变类
      Calender gmtCal=Calender .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 ;
    }
}

  以上的这种方法,在多次调用时的速度会大大提高(因为已经是拿创建好的东西来重用)。

  通过此点以上的判断,我们知道了正确第使用对象的重要性。通过上面的重用方法,有的人可能会想到对象池。难道上述那些对象都适合放在对象池吗?其实不然。

  通过维护自己的对象池来避免创建对象并不是一种好的做法,除非池中的对象是非常重量级的。真正正确使用对象池的典型对象就是数据库连接池。建立数据库连接的代价非常昂贵,再加上数据库许可可能限制你使用一定的数量连接,所以重用这些对象非常有意义。但一般而言,维护自己的对象池必定会把代码弄得很乱,也增加内存占用,损害性能。另外,现代的JVM实现具有高度优化的垃圾回收器,所以性能很容易能超过轻量级对象池性能。所以,一般都用重量级对象池。
  

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

  
  还记得我们这一章的标题叫什么吗?(前面讲了太多静态构造器的内容,估计大家都忘了这一章的标题了。)对,就是创建和销毁对象。
  
  今天,我们就来讲讲过期对象引用的消除。

  由于Java有着强大的垃圾回收功能,我们可能会觉得,咦,我们现在就不再需要考虑内存管理的事了,真爽!但,其实不然。看下下面这个例子你就知道了

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

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

    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=Array.copyOf(elements,2*Size+1);
    }
}

  上面程序中并无明显错误,运行也一切正常。但程序中隐藏着一个问题:内存泄漏。随着垃圾回收器活动的增加,内存占用的增加,程序性能会不断地降低。极端情况下还有可能导致磁盘交换或程序失败。

  我们来看看这个问题是怎么出现的:
  我们要知道,如果一个栈先是增长,然后再收缩。那么,从栈中弹出的对象其实是不会被当做垃圾回收的,即使栈程序不再引用这些对象。为什么呢?因为啊,栈内部维护着对这些对象的过期引用(永远不会被解除的引用)。而且,如果一个对象引用被无意识地保留起来了,那么垃圾回收机制不但不会处理这个对象,而且也不会处理被这个对象所引用的其他所有对象!对性能的影响得多大,想想就知道了……

  如何解决这个问题呢?其实很简单,我们只要在pop函数中做如下修改

        Object result = elements[--Size];
        elements[Size] = null;
        return result;

  这叫做清空过期引用。原理解析:数组活动区域的元素是已经分配了的,而数组其余部分的元素则是自由的。但是垃圾回收器并不知道这一点;对于垃圾回收器而言,elements中所有对象引用都同等有效。只有我们知道数组的非活动部分是不重要的,所以可以手动清空。

  以上是内存泄漏的一种常见来源。另外两个常见的泄漏来源是缓存、监视器和其他回调。这两种情况的解决方法则是使用WeakHashMap中的键来保存。由于比较少用到,所以我们就不展开细讲了。有遇到是再Google一下。

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

  
  首先需要强调:终结方法通常是不可预测的,也是很危险的,一般情况下是不必要的。使用终结方法会导致行为不稳定,降低性能,以及可移植性问题。
  其次,再来说说终结方法的好处:两种合法用途

  1、当对象所有者忘记调用显示终止方法时,可以充当“安全网”  

以下是显示终止方法代码:

Foo foo = new foo(...);
try{
    ...
}finally{
    foo.terminate();//显示终结方法!
}

  如果客户端无法调用以上方法来正确结束操作时,“安全网”就起了作用。

  2、终止非关键的本地资源


  如果不是以上的两种用途,则不要使用终结方法。原因:Java语言规范不仅不保证终结方法会被及时地执行,而且根本就不保证它们会被执行!当一个程序终止时,某些已经无法访问对象上的终结方法依旧没有被执行,这种情况是存在的!所以,慎用终结方法!

总结

  在这一章中,我们讲了对象的创建和销毁习惯,在什么时候应该使用哪种方式创建和销毁对象,哪种方式是安全的,哪种方式应该尽量避免使用……让我们对对象这一概念有了更清晰的认识,由于对象是Java的核心内容,所以,如何正确地创建和销毁就显得至关的重要了。

  下一节,我们将会继续学习Java编程的另一个核心——方法

你可能感兴趣的:(Effective,Java)