Effective Java (一)

一、创建和销毁对象

1. 考虑用静态工厂方法代替构造方法创建对象

  1)构造方法创建:

public Boolean(String s) {
    this(toBoolean(s));
}
Boolean bTrue = new Boolean("true");

  2)静态工厂创建:

public static Boolean valueOf(String s) {
    return toBoolean(s) ? TRUE : FALSE;
}
Boolean bTrue = Boolean.valueOf("true");

2. 遇到多个构造器参数时要考虑使用Builder模式

三种常用的构造模式

  1)重叠构造器模式

public class NutritionFacts {
    private final int servingSize;  //(ml)require
    private final int servings;  //(per container)require
    private final int calories;  //optional
    private final int fat;  //(g)optional
    private final int sodium;  //(mg)optional
    private final int carbohydrate;  //(g)optional

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

   

NutritionFacts nutritionFacts = new NutritionFacts(240, 8, 0, 0, 0, 0);

    缺点:当参数过多时,代码编写很麻烦;并且难以阅读,必须仔细数着参数来探个究竟。

  2)JavaBean模式

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

    public NutritionFacts() {}

    //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;
    }
}
NutritionFacts nutritionFacts = new NutritionFacts();
nutritionFacts.setServingSize(240);
nutritionFacts.setServings(240);
nutritionFacts.setFat(240);
nutritionFacts.setSodium(240);
nutritionFacts.setCarbohydrate(240);

    缺点:无法通过检查构造器参数有效性来保证一致性,调试起来较困难;需要付出额外的努力来保证线程安全。

  3)Builder模式

public class NutritionFacts {
    private final int servingSize;  //(ml)require
    private final int servings;  //(per container)require
    private final int calories;  //optional
    private final int fat;  //(g)optional
    private final int sodium;  //(mg)optional
    private final int carbohydrate;  //(g)optional

    public static class Builder {
        //Require 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 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 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;
    }
}

    优点:既能保证像重叠构造器那样的安全性,也能保证像JavaBeans模式那么好的可读性。

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

  不建议:

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

  建议:

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

  简洁版:

public enum Elvis {
    INSTANCE;
}

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

        企图通过将类做成抽象类来强制该类不可被实例化,这是行不通的。因为该类可以被子类化,并且该子类也可以被实例化。这样做甚至会误导用户,以为这种类是专门为继承而设计的。

    正确:

public class UtilityClass {
    private UtilityClass() {}
    ...
}

5. 避免创建不必要的对象

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

    错误:

String s = new String("stringette");

    正确:

String s = "stringette";

    错误:

public class Person {
    private final Date birthday;

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

    public boolean isBabyBoomer() {
        Calendar c = Calendar.getInstance(TimeZone.getTimeZone("GMT"));
        c.set(1946, Calendar.JANUARY, 1, 0, 0, 0);  //将时间设置为1946年1月1日0时0分0秒
        Date boomStart = c.getTime();
        c.set(1965, Calendar.JANUARY, 1, 0, 0, 0);
        Date boomEnd = c.getTime();
        return birthday.compareTo(boomStart) >= 0 && 
               birthday.compareTo(boomEnd) < 0;
    }
}

        isBabyBoomer在每次调用的时候都会创建一个Calendar 一个TimeZone和两个Date实例,这些都是不必要的。可以用一个静态的初始化器来改进,从而避免这种效率低下的问题。

public class Person {
    private final Date birthday;

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

    static {
        Calendar c = Calendar.getInstance(TimeZone.getTimeZone("GMT"));
        c.set(1946, Calendar.JANUARY, 1, 0, 0, 0);  //将时间设置为1946年1月1日0时0分0秒
        BOOM_START = c.getTime();
        c.set(1965, Calendar.JANUARY, 1, 0, 0, 0);
        BOOM_END = c.getTime();
    }

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

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

        当静态初始化器所在类第一次被使用时,静态初始化器首先被调用,而且仅此一次而已。以后不管这个类被使用多少次,静态初始化器都不会再被调用。

       要优先使用基本数据类型而不是装箱基本类型。会额外创建一个Integer实例,同时频繁的自动装箱会造成性能消耗。

Integer value = 0;

6. 消除过期的对象引用

    Java的垃圾回收机制并不代表我们不需要考虑内存管理的问题。

public class Stack {
    pprivate Object[] elements;
    private int size = 0;
    private static final int DEFAULT_INITAL_CAPACITY = 16;
    
    public Stack() {
        elements = new Object[DEFAULT_INITAL_CAPACITY];
    }
    
    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 = Arrays.copyOf(elements, 2 * size + 1);
    }
}

       这段程序没有任何明显的错误,但这个程序中隐藏着一个问题,内存泄漏

       如果一个栈先是增长,然后收缩,那么从栈中弹出来的对象不会被当作垃圾回收,这是因为栈内部仍然维护着这些过期对象的引用。所谓 过期引用 是指elements中下标大于等于size的那些元素(由于栈会增长收缩,所以这是完全有可能的),如果一个对象被无意识地(即我们不希望地)保留下来,那么这个引用所引用的其他对象也不会被垃圾回收机制回收,因为仍然存在着引用,GC认为这些对象仍然是会被使用的。随着程序的运行时间增长,过期的对象引用占用的内存会越来越大,导致程序性能下降。

      这类问题的解决方法是:一旦对象引用已经过期,只需清空这些引用即可。

public Object pop() {
    if(size == 0) {
        throw new EmptyStackException();
    }
    Object result = elements[--size];
    elements[size] = null;//显式地清空引用
    return result;
}

       当然,清空对象引用应该是一种例外,而不是一种规范行为。不要求也不建议程序员对于每个对象引用,一旦程序不再用,就把它清空,这通常会把代码弄的很混乱。消除过期引用的最好办法是让包含该引用的变量结束其生命周期,在最紧凑的作用域范围内定义每一个变量,当作用域被执行完,GC会自动把过期作用域的变量回收掉。

       一般而言,只要类是自己管理内存,就应该警惕内存泄露问题。一旦元素被释放掉,则该元素中包含的任何对象引用都应该被清空。

       内存泄露的另一个常见来源是缓存。一旦你把对象引用放到缓存中,它就很容易被遗忘掉,从而使得它不再有用之后的很长一段时间仍然留在缓存中。

       解决方案:如果你正好要实现这样的缓存:只要缓存之外存在对某个项的键的引用,该项就有意义,那么就可以用WeakHashMap代替缓存;当缓存中的项过期之后,它们就会自动被删除。记住只有当所要的缓存项的生命周期是由该键的外部引用而不是由值决定时,WeakHashMap才有用处。

       更为常见的情形则是,“缓存项的生命周期是否有意义”,并不是很容易确定,随着时间的推移,其中的项会变得越来越没有价值。在这种情况下,缓存应该不时地清除掉没用项。这项清除工作可以给一个后台线程(可能是Timer或者SchduledThreadPoolExecutor)来完成,或者也可以在给缓存添加新条目的时候顺便进行清理。LinkedHashMap类利用它的removeEldestEntry方法可以很容易地实现后一种方案。对于更复杂的缓存,必须直接使用java.lang.ref。

       内存泄露的第三个常见的来源是监听器和其它回调。如果你实现了一个API,客户端在这个API注册回调,却没有显式地取消注册,那么除非你采取某些动作,否则他们就会积聚。确保回调立即被当做垃圾回收的最佳方法是只保存它们的弱引用(weak reference),例如只将它们保存成WeakHashMap中的键。

7. 避免使用终结方法

    尽量不要在你的类中覆盖finalize方法,然后在在里面写一些释放你的类中资源的语句。

    原因如下:

   (1)finalize方法不能保证它能被及时的执行。
   (2)finalize方法甚至都不会被执行。
   (3)System.gc和System.runFinalization这两个方法只是能增加finalize方法被调用的几率。
   (4)唯一能保证finalize方法被执行的方法有两个,System.runFinalizersOnExit和Runtime.runFinalizersOnExit但是这两个方法已经被弃用。
   (5)覆盖并使用终结方法会造成严重的性能损失。

   正确的方法:

       提供一个public修饰的终止方法,用来释放资源,并要求这类的使用者在不再使用这个类的时候调用这个方法,并且在类中添加一个标志,来标记资源是否已经释放,如果已经被释放了,那这个类中的方法如果在被调用的话就抛出IllegalStateException异常。

       此外,在调用我们自己定义的public修饰的终止方法的时候最好和 try-finally 一起使用。

       例如:

class MyObject{
    private boolean isClosed = false;
    //public修饰的终止方法
    public void close(){
        //资源释放操作
        ...
        isClosed = true;
    }
}
public static void main(String[] args) {
    MyObject object = new MyObject();
    try{
        //在这里面使用object;
        ...
    }  finally {
        //在这里面关闭object;
        object.close();
    }
}

      什么时候使用终结方法是合理的?

     (1)用终结方法充当“安全网”
              “安全网”的作用是当我们提供的public修饰的终结方法被在外部忘记调用的时候提供一种安全保障,如下:

class MyObject{
    private boolean isClosed = false;
    //public修饰的终止方法
    public void close(){
        //资源释放操作
        ...
        isClosed = true;
    }

    //安全网
    @Overried
    protected void finalize() throws Throwable {
        try{
            close();
        }  finally  {
            super.finalize();
        }
    }
}

  (2)第二种合理用法和本地对等体相关,在涉及到JNI编程的时候,我们的普通java对象有的时候需要委托给本地对象,在资源回收的时候只能回收到普通java对象而回收不了被本地代码(C或者C++)托管的对象,所以需要我们在终结方法方法中通过调用本地方法来释放掉这个被本地代码托管的对象。注:这种方式是面对不包含关键资源的对象,如果包含关键资源同样需要提供一个public修饰的终结方法。

     注意:“终结方法链”并不会被自动执行。如果类(不是Object)有终结方法,并且子类覆盖了终结方法,子类的终结方法必须手动调用超类的终结方法。因此关键就是确保 super.finalize()方法一定会被执行
        确保它一定会被执行的方式有两种:
      (1)使用try-finally(像上面的安全网一样);
      (2)使用“终结方法守卫者”

        例如:

public class Foo{
    //终结守卫者
    private final Object finalizerGuardian = new Object{
        @Overried
        protected void finalize() throws Throwable {
            //释放Foo中的资源,这里不需要调用super.finalize()
            ...
        }
    }
}
发布了35 篇原创文章 · 获赞 37 · 访问量 1万+

猜你喜欢

转载自blog.csdn.net/qq_34519487/article/details/103850770