effective java读书笔记1——创建和销毁对象

今天刚开始读effective java,中文版的读起来很拗口,但感觉收获很多。

另外,这本书的内容是针对Java 1.5和1.6的。

在这里整理一下第2章:创建和销毁对象 的内容。

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

      这一条针对的情景是要获得类的实例时。一般说来,想要获得类的实例,都是通过构造函数(书里叫做构造器)。

      最常见的构造函数是这样的,没有返回参数,名字和类名相同。

public class A{
   public A(int a){
   //构造函数内容
   ...
  }
}

     而所谓的静态工厂,是一个静态的函数,名字和类名不同,返回值是本类的对象。示例是将boolean基本类型值转化成Boolean对象引用:

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

类可以通过它的静态工厂方法来访问它的客户端(也就是获得对象的实例),而不是通过构造器,那么我们为什么要使用静态工厂呢?它的优势是什么?

优势一:静态工厂方法有名称

如果构造器的参数不能正确描述返回的对象,那么静态工厂会更容易使用,代码也会更容易阅读。

比如说一个类A,有两个参数不同的构造函数,分别做两件事情A,B。代码如下:

public class A{
    public A(int a,String s){
         //do A
     }
    public A(String s,int a){
        // do B
     }
}

       这是对构造函数的重载,针对不同的参数做不同的动作,但是用户在调用的时候并不知道哪个构造器做哪个活动,他们可能把参数的顺序写错,调用了自己不想用的那个构造器。而静态工厂方法就不受限制了,可以根据要做的行为给方法起不同的名字,比如doA(),doB(),这样可以避免用户的错误调用,也加强了代码的可读性。

  优势二:不必每次调用它们的时候都创建一个新的对象

这是静态方法的特点了,用static声明一个方法的时候,这个方法可以直接用类名调用,而不需要用对象调用。

比如方法methodA是静态方法,属于类A,那么可以这样调用:A.methodA().而不用A a = new A();a.methodA();

静态方法能够为重复的调用返回相同对象,这样有助于类严格控制在某个时刻哪些实例应该存在。这种类叫做实例受控类。使用实例受控类有几个原因:

1.可以确保它是singleton(单例)的,或者是不可实例化的。

      2.可以使不可变的类可以确保不会存在两个相等的实例,即当且仅当a==b时才有a.equals(b)为true。这样用户就可以用==操作符来替代equals方法,提升性能。

优势三:静态工厂可以返回原返回类型的任何子类的对象

      构造器只会返回本类的对象,而静态工厂方法可以返回本类以及本类的子类对象,这个是多态性的一个体现。使用这种静态工厂方法时,用户可以通过接口来引用被返回的对象,而不是通过它的实现类,这是一种良好的习惯。

      举个例子,比如说对集合类,List l = new ArrayList<String>().List是接口,而ArrayList是实现类。我们可以通过接口List来引用,而没必要用实现类ArrayList来引用。这样有一个好处,当程序以后的实现类改变时,只需要在这一处改变实现类声明就可以了。

     同时,还有一条:静态工厂方法返回的对象所属的类,在编写包含该静态工厂方法的类时可以不必存在。比如说:(这里我不知道理解的对不对)

public class A{
     public static B method(){
     //静态工厂方法
     }
}
public class B extends A{
//B类的实现
}

    这种灵活的静态工厂方法构成了服务提供者框架的基础。什么叫“服务提供者”框架呢?书里是这么定义的:

   它是指这样一个系统:多个服务提供者实现一个服务,系统为客户端提供多个实现,并把他们从多个实现中解耦出来。

     服务提供者框架中有3个重要的组件:1,服务接口。这是提供者实现的。2,提供者注册API,这是系统用来注册实现的,让客户端访问他们。3.服务访问API,是客户端用来获取服务的实例的。

    我的理解是:这3个组件构成了一个分层的结构。从上到下分别是3,2,1。用户见到的是3服务访问API,用户调用了3之后,3会调用2提供者注册API,得知服务实现的信息(比如提供者,名称之类的),2再调用不同提供者提供的服务接口1,完成服务的实现。用户只知道结果(得到了一个service),但是不必知道是哪个提供者提供的,也不必知道调用了哪个具体的服务实现接口。

     优势四:在创建参数化类型实例的时候,它们使代码变得更加简洁

     用代码说明:

Map<String,List<String>> m = 
         new HashMap<String,List<String>>();
//这里声明了两次<String,List<String>>
//下面是用静态工厂方法的,只声明了一次
Map<String,List<String>> m = HashMap.newInstance();

     当然,静态工厂方法也有缺点。

     缺点一:类如果有私有的构造器,就不能被子类化

     缺点二:它们与其他的静态方法实际上没有任何区别。你没有办法从名字看出来,一个静态方法到底是静态工厂方法还是仅仅是一个普通的静态方法。遵守惯用名称可以弥补这一劣势,常用的名称有:valueOf,of,getInstance,newInstance,getType,newType。

     总的来说,这本书的作者推荐我们首先考虑静态工厂方法,而不是共有的构造器。

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

      考虑这一的场景:我们需要一个类来表示包装食品外面的营养成分标签。这些标签中有几个域是必须的:每份的含量,每罐的含量,卡路里。还有超过20个可选域:总脂肪量,饱和脂肪量,转化脂肪,胆固醇,钠等等。对于这样的类,应该用哪种构造器或者静态工厂方法来编写呢?

      常用的是提供重载构造器,通过重载构造函数中的参数来实现。但是当有很多参数的时候,客户端代码会非常难写。

      还有第二种方法,JavaBean模式,即调用一个无参构造器来创建对象,然后用setter方法来设置每个必要的参数。但是这个方法有严重的缺点:可能会使JavaBean处于不一致的状态;线程安全难以保证。

      作者提出了第三种方法:构建器。不直接生成想要的对象,而是让客户端利用所有必要的参数调用构造器(或静态工厂),得到一个builder对象,然后客户端在builder对象上调用类似于setter的方法,来设置每个相关的可选参数。最后,客户端调用无参的build方法来生成不可变的对象。这个builder是它构建的类的静态成员类。

      听上去有点复杂,上代码:

//Builder Pattern
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 final int calories=0;
          private final int fat=0;
          private final int sodium=0;
          private final int carbohydrate=0;
          public Builder(int servingSize,int servings){
                this.servingsize= servingSize;
                this.servings = servings;
          }
          public Builder calories(int val){
                calories = val;
                return this;}
          public Builder fat(int val){
                fat = val;
                return this;}
          public Builder carbohydrate(int val){
                carbohydrate = val;
                return this;}
          public Builder sodium(int val){
                sodium = val;
                return this;}  
           public NutritionFacts build(){
                return newNutritionFacts(this);}
    }    
     private NutritionFacts(Builder builder){
            servingSize = builder.servingSize;
            ...
    }
}

  客户端代码:

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

  builder模式模拟了具名的可选参数。

  当然,Builder模式更冗长,所以它只适合用于多个参数,且数目不定,有可选参数时使用。

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

  singleton是指仅仅被实例化一次的类。实现方法有两种:

      第一种,公有静态成员是个final域。

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

    public void otherMethods(){...}
}

  第二种,公有的成员是个静态工厂方法。

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

  用上面这两种方法实现的singleton在反序列化时会出现问题,每次反序列化时都会创建一个新的实例

      从Java 1.5后,有第三种方法,编写一个含有单个元素的枚举类型:

public enum Elvis{
    INSTANCE;    
    public void otherMethod(){...}
}

  这种方法更加简洁,而且无偿的提供了序列化机制,防止多次实例化。所以单元素的枚举类型已经成为实现singleton的最佳方法。

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

  有一些工具类,可能只包含静态方法和静态域,我们不希望它们被实例化(比如?),但是在缺少构造函数的情况下,编译器会自动提供一个公有的无参的构造函数。同时,把它们声明为抽象类企图强制该类不可被实例化,这也是行不通的。抽象类可以有子类,子类也可以实例化,甚至声明会误导用户,以为这个类是专门为了继承而设计的。

  我们只需要让类包含私有构造器就可以达到这个目的。当然,这样也有一个副作用,会使得这个类并不能被子类化。因为所有构造器都会显示或隐式调用父类构造器。

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

  考虑:String s = new String("abdcle");这个语句每次被执行的时候都会创建一个新的String实例,但是这些动作都是不必要的,如果这句在一个循环中,会创建很多个不必要的String实例。改为 String s = "abdcle"更好。(因为java有常量缓冲池,字符串常量只需要创建一次)

  另一个例子:

 

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

 

  这个例子在功能上没有问题,但是要慢很多。因为sum被声明为Long类型,造成了程序构造了231个多余的Long实例。所以说:优先使用基本类型而不是装箱类型,要当心无意识的自动装箱。

  另外,小对象的创建和回收代价是很廉价的,所以用维护自己的对象池来避免创建对象并不是很好的做法,除非对象是重量级的(比如数据库的连接池,因为创建一个数据库连接的代价很昂贵,因此重用这些连接有意义。)

第六条:消除过期的引用

  考虑一个实现栈的例子:

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 void push(Object e){
        ensureCapacity();
        elements[size++] = e;
    }

    public Object pop(){
        if(size == 0)
            throws new EmptyStacException();
        return elements[--size];
    }
    private void ensureCapacity(){
    ....
    }
}

  这样会发生内存泄露。因为栈中元素弹出时,被弹出的元素并不会被当做垃圾回收,即使使用栈的程序不再引用这些对象,它们也不会被回收。因为栈内部维护了对这些对象的过期引用。

  如果一个对象引用被无意识的保留了,那么,GC不仅不会处理这个对象,而且也不会处理被这个对象所引用的其他对象。

  修复这个问题很简单,在栈的pop方法中,添加一句: element[size] = null;即可。

  内存泄露的另一个来源是缓存,有两种情况:第一种是缓存中的元素已经在内存中被清除了,那么这个元素是无效的,应该被清除。第二种是缓存中的元素很久没有被访问到,这时候应该替换掉这个元素。

  第三个来源是监听器和其他回调(这个我没接触过,不写了)

第七条:避免使用finalize方法

  finalize方法的缺点在于,不能保证会被及时的执行,因为GC的启动时间并不确定。甚至没有办法保证会被执行。所以不能用来释放资源,更新持久状态等等。这部分内容应该由try-finally来完成。

  如果确实需要终止方法,只需要提供一个显示的终止方法,如InputStream,OutStream,sql.Connection上的close()等。

  值得注意的是,终结方法链并不会被自动执行。也就是说,如果子类覆盖了终结方法,那么在子类的终结方法中必须手工调用父类的终结方法,否则不会执行父类的终结方法。

 

转载于:https://www.cnblogs.com/cangyikeguiyuan/p/4384448.html

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值