EffectiveJava第2章-创建和销毁对象

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

获取类实例的两种方法:公有的构造器、公有的静态工厂方法返回类的实例。

静态工厂方法的优势
1.它们有名称
当一个类需要多个带有相同签名的构造器时,就用静态工厂方法代替构造器,并且慎重的选择名称以便突出它们之间的区别。

2.不必每次调用它们的时候都创建一个新对象
可以避免创建不必要的重复对象。这种方法类似于享元模式,如果程序经常请求创建相同的对象,可以使用静态工厂方法。
创建单例的时候,也用到静态工厂方法。

3.它们可以返回原返回类型的任何子类型的对象
这比较适用于基于接口的框架。API可以返回各种子对象,同时又不会使对象的类变成公有的。这样的API变得非常整洁。比如java.util.Collections。客户端永远不知道也不关心他们从工厂方法中得到的对象的类,而且在后续的版本中发生变化也不会对客户端造成影响。

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.类如果不含公有的或者受保护的构造器,就不能被子类化。
不能被子类化就是不能被继承。

2.静态工厂方法与其他的静态方法实际上没有任何区别。

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

遇到参数比较多时,一般的构造器(重叠构造器),首先提供一个只有必要参数的构造器,第二个构造器有一个可选参数,第三个有两个可选参数,以此类推……重叠构造器模式可行,但是当有许多参数的时候,客户端代码会很难写,并且较难阅读。

//重叠构造器的代码示例
public class NutritionFacts {
   private final int servingSize;  //(ml)
   private final int servings;     //(per container)
   private final int calories;     //
   private final int fat;          //(g)
   private final int sodium;       //(mg)
   private final int carbohydrate; //(g)

   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方法来设置参数。在构造过程中,JavaBean可能处于不一致的状态,类无法通过检验参数的有效性来保证一致性

public class NutritionFacts {
    private  int servingSize=-1;  //(ml)
    private  int servings=-1;     //(per container)
    private  int calories=0;     //
    private  int fat=0;          //(g)
    private  int sodium=0;       //(mg)
    private  int carbohydrate=0; //(g)

    public NutritionFacts () {}

    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;}
}

第三种方式就是Builder模式。builder像个构造器一样,可以对其参数强加约束条件。build方法可以检验这些约束条件。其实Builder就是一个JavaBean,只不过增加了一个build方法来检查参数的有效性。

public class NutritionFacts {
    private  int servingSize=-1;  //(ml)
    private  int servings=-1;     //(per container)
    private  int calories=0;     //
    private  int fat=0;          //(g)
    private  int sodium=0;       //(mg)
    private  int carbohydrate=0; //(g)

    public static  class Builder{
        //Required parameters
        private final int servingSize;
        private final int servings;
        //Optional parameters - initialized to default values
        private int calories        =0;
        private int fat             =0;
        private int carbohydrate    =0;
        private int sodium          =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 carbohydrate(int carbohydrate) {
            this.carbohydrate = carbohydrate;
            return this;
        }

        public Builder sodium(int sodium) {
            this.sodium = sodium;
            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;
    }

}

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

1.饿汉式(类加载时创建实例)

//final公有的静态成员
public class Elvis01 {  
    public static final Elvis01 INSTANCE = new Elvis01();  
    private Elvis01(){  //私用构造器仅被调用一次
        if(INSTANCE != NULL){
            throw new SomeException();
        }
        ......
    }  
    public void leaveTheBuilding(){  
        System.out.println("Elvis01 leaveTheBuilding");  
    }  
} 

客户端通过反射机制调用私有构造器,将构造器修改成可访问的,从而创建出另一个实例。所以可以在创建第二个实例的时候抛出异常。

2.饿汉变种(提供静态方法获取实例)

//公有的成员是个静态工厂
public class Elvis02 implements Serializable{  
    private static final Elvis02 INSTANCE = new Elvis02();  
    private Elvis02(){  
  
    }  
    public void leaveTheBuilding(){  
        System.out.println("Elvis02 leaveTheBuilding");  
    }  
    public static Elvis02 getInstance(){  
        return INSTANCE;  
    }  
}

3.懒汉式

public class Elvis04 implements Serializable{  
    private Elvis04 instance;  
  
    private Elvis04(){  
  
    }  
    public Elvis04 getInstance(){  
        if(instance == null){  
            instance = new Elvis04();  
        }  
        return instance;  
    }  
} 

这是线程不安全的。

上述三种方式,在序列化的时候都一些额外的工作,否则没有办法保证单例,因为每次反序列一个序列化的实例的时候,都会创建一个新的实例。
必须声明所有的实例域都是瞬时的(transient),并提供一个readResolve方法。

private Object readResolve(){
    return INSTANCE;
}

4.枚举单例

public enum  Elvis03 implements Serializable{  
    INSTANCE;  
    public void leaveTheBuilding(){  
        System.out.println("Elvis03 leaveTheBuilding");  
    }  
}  

5.静态内部类
实现了懒加载,并且是线程安全的。
利用类加载机制,实现了线程安全:客户端代码调用了getInstance时,JVM加载SingletonHolder,初始化静态成员,从而实例化了instance。

public class Elvis05 implements Serializable{  
    private Elvis05(){  
  
    }  
    public Elvis05 getInstance(){  
        return SingletonHolder.instance;  
    }  
    private static class SingletonHolder{  
        private static Elvis05 instance = new Elvis05();  
    }  
    public void leaveTheBuilding(){  
        System.out.println("Elvis05 leaveTheBuilding");  
    }  
} 

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

有些类不希望被实例化,比如说一些工具类:java.util.Collections。
在缺少显式构造器的情况下,编译器会自动提供一个缺省的公有的无参构造器。这样的类仍然可能被实例化。
要写类不能被实例化,只要让这个类只提供一个私有构造器(private),就不能在外部被实例化。
这种做法使得这个类不能被子类化,因为所有的构造器都会显式或则隐式地调用超类构造器,在这种情况下,子类就没有可访问的超类构造器可调用了。

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

最好能重用对象,而不是每次需要的时候就创建一个相同功能的新对象。
如果对象是不可变的,它始终可以被重用。

String s = new String("stringette")//每次都会创建一个新的实例
String s = “stringette” //重用同一个对象

除了重用不可变的对象之外,也可以重用那些一直不会被修改的可变对象。

//书上的一个坑
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,每次执行sum += i的时候都要自动装箱,创建了很多不必要的Long对象。要优先使用基本类型而不是装箱基本类型,当心无意识的自动装箱。

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

Java虽然有垃圾回收机制,但是并不意味着你不用手动去管理内存。
只要类是自己管理内存,就应该警惕内存泄漏的问题。
下面就是内存泄漏常见的三种情形:

//简单的栈实现,从栈中取出一个元素
public Object pop(){
    if(size == 0){
           throw new EmptyStackException();
     }
     return elements[--size];
}

这段程序存在着“内存泄漏”。如果一个栈先是增长的,然后再收缩。那么,从栈中弹出来的对象将不会被当做垃圾回收。因为栈内部维护着对这些对象的“过期引用”。

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

内存泄漏的另一个常见的来源是缓存。一旦你把对象引用放到缓存中,他就很容易被遗忘掉,从而使它不再有用很长时间内仍然留在缓存中。
可以使用WeakHashMap,自动清除没有被外部引用的键值。(缓存本身就是为了提高数据的读取速度,如果在缓存中没有读到数据,再去内存中找。这并不会影响程序的正确性,所以WeakHashMap可以加快数据读取速度,也能够避免出现内存泄漏,因为被弱引用关联的对象只能生存到下一次垃圾收集发生之前。)
LinkedHashMap构建LRU缓存。

第三个常见的来源:监听器和其他回调。客户端通过API注册了回调,却没有显式地取消注册,导致对象积聚。
最佳方法是只保存回调的弱引用。

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

终结方法只会被调用一次。

大致描述一下finalize流程:当对象变成(GC Roots)不可达时,GC会判断该对象是否覆盖了finalize方法,若未覆盖或者finalize方法已经被执行过,则直接将其回收。否则,若对象未执行过finalize方法,将其放入F-Queue队列,由一低优先级线程执行该队列中对象的finalize方法。执行finalize方法完毕后,GC会再次判断该对象是否可达,若不可达,则进行回收,否则,对象“复活”。(可对着下面的代码看)

上述流程存在一个严重问题就是将fianlize方法交给一个低优先级线程执行,因此不能保证被及时地执行,甚至根本不会保证被执行。

替代方法就是提供一个显式的终止方法。比如InputStream、OutputStream的close方法以及Timer的cancel方法。
显式的终止方法通常与try-finally结构结合起来使用,以确保及时终止。即使有异常抛出,finally块也始终会执行,因此终止方法肯定会被执行。

对象再生问题:finalize方法中,可将待回收对象赋值给GC Roots可达的对象引用,从而达到对象再生的目的。

public class GC {  
 
   public static GC SAVE_HOOK = null;  
 
   public static void main(String[] args) throws InterruptedException {  
       SAVE_HOOK = new GC();  
       SAVE_HOOK = null;  
       System.gc();  
       Thread.sleep(500);  
       if (null != SAVE_HOOK) { //此时对象应该处于(reachable, finalized)状态  
           System.out.println("Yes , I am still alive");  
       } else {  
           System.out.println("No , I am dead");  
       }  
       SAVE_HOOK = null;  
       System.gc();  
       Thread.sleep(500);  
       if (null != SAVE_HOOK) {  
           System.out.println("Yes , I am still alive");  
       } else {  
           System.out.println("No , I am dead");  
       }  
   }  
 
   @Override  
   protected void finalize() throws Throwable {  
       super.finalize();  
       System.out.println("execute method finalize()");  
       SAVE_HOOK = this;  
   }  
}  

代码执行结果
execute method finalize()
Yes , I am still alive
No , I am dead

终结方法有两种合理用途:
1.终结方法可以充当安全网,如果对象的所有者忘记调用了显式的终止方法,那么终结方法可以充当安全网,迟一点释放资源总比不释放资源要好。同时输出日志提示用户去调用显式的终止方法。

2.终止非关键的本地资源。如果是关键资源,必须调用终止方法。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 206,839评论 6 482
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 88,543评论 2 382
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 153,116评论 0 344
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 55,371评论 1 279
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 64,384评论 5 374
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,111评论 1 285
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,416评论 3 400
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,053评论 0 259
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 43,558评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,007评论 2 325
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,117评论 1 334
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,756评论 4 324
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,324评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,315评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,539评论 1 262
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,578评论 2 355
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,877评论 2 345

推荐阅读更多精彩内容

  • 1. Java基础部分 基础部分的顺序:基本语法,类相关的语法,内部类的语法,继承相关的语法,异常的语法,线程的语...
    子非鱼_t_阅读 31,587评论 18 399
  • 国家电网公司企业标准(Q/GDW)- 面向对象的用电信息数据交换协议 - 报批稿:20170802 前言: 排版 ...
    庭说阅读 10,869评论 6 13
  • Spring Cloud为开发人员提供了快速构建分布式系统中一些常见模式的工具(例如配置管理,服务发现,断路器,智...
    卡卡罗2017阅读 134,600评论 18 139
  • 1、.java源文件: 一个以”.java“为后缀的源文件:只能有一个与文件名相同的类,可以包含其他类。 2、类方...
    Hughman阅读 1,445评论 1 9
  • 月上高悬,青灯初上,挥墨成影泪沾襟!这世界唯一的你,在哪里?
    可我不爱聪明阅读 126评论 0 0