美文网首页EffectiveJava笔记
EffectiveJava第2章-创建和销毁对象

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

作者: wangcanfeng | 来源:发表于2017-06-12 22:38 被阅读0次

    第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.终止非关键的本地资源。如果是关键资源,必须调用终止方法。

    相关文章

      网友评论

        本文标题:EffectiveJava第2章-创建和销毁对象

        本文链接:https://www.haomeiwen.com/subject/xmscqxtx.html