【Effective java 学习】第二章:创建和销毁对象

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

优点:

  1. 使用静态工厂方法,有名称,事例如下

    class Person1{
       private String sex ;
       //使用构造方法传递参数来区分性别
       public Person1(String sex){
           this.sex = sex;
       }
    }
    
    class Person2{
       private String sex;
       //使用静态工厂方法来返回实例,同时根据方法名称可以判断出性别
       public static Person2 male(){
           Person2 male = new Person2();
           male.sex = "male";
           return male;
       }
       public static Person2 female(){
           Person2 female = new Person2();
           female.sex = "female";
           return female;
       }
    }
  2. 使用静态工厂方法,不必在每次调用它们的时候都创建一个新对象

    这一点,在单例模式中有着较为明显的体现

    class SingletonDemo{
       private static SingletonDemo instance = null;
       private SingletonDemo() {}
       public static SingletonDemo getInstance() {
           instance = new SingletonDemo();
           return instance;
       }
    }
  3. 使用静态工厂方法,可以返回原返回类型的任何子类型

    这一点,是java中多态特性的体现。如果返回类型是A,那么方法运行过程中,返回B或C都是可以的。

    class A {}
    class B extends A {}
    class C extends A {}
  4. 在创建参数化类型实例的时候,使代码变得更加简洁

    书中代码意思如下,但这点已经在后续版本的java中简化掉

    Map<String, Integer> map = new HashMap<String, Integer>();
    //使用下面的方法
    public static <K, V> HashMap<K, V> newInstance(){
           return new HashMap<K, V>();
    }
    //然后就可以写成下面的样式
    Map<String, Integer> map = HashMap.newInstance();

缺点:

  1. 使用静态工厂方法,类如果不含公有的或者受保护的构造器,就不能被子类化

    在我们使用静态工厂方法后,很可能会将构造方法设置为private属性,这就使得该类无法被继承,但同时,也鼓励了程序员使用复合而不是继承,降低了代码的耦合性。

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

    静态工厂方法不像构造器在API文档中有明确的标识,如果一个类提供的是静态工厂方法而不是构造方法来实例化一个类,那么通过查文档,就比较麻烦了。下面是静态工厂方法一些惯用名称:

    //不太严格地讲,该方法返回的实例与它的参数具有相同的值,实际上是类型转换方法
    valueOf
    //valueOf一种更为简洁的替代,在EnumSet中使用并流行起来
    of
    //返回的实例是通过方法的参数来描述,但是不能够说与参数具有同样的值
    getInstance
    //像getInstance一样,但newInstance能够确保返回的每个实例斗鱼所有其他实例不同
    newInstance
    //想getInstance一样,但是在工厂方法中处于不同的类中的时候使用。Type表示工厂方法所返回的对象类型
    getType
    //想newInstance一样,但是在工厂方法中处于不同的类中的时候使用。Type表示工厂方法所返回的对象类型
    newType

总结:静态工厂方法和公有构造器都各有用处,不要酌情使用。静态工厂方法通常更加合适,因此切忌第一反应就是提供公有构造器,而不先考虑静态工厂。

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

当类中有大量可选参数时,无论是使用静态工厂方法还是使用构造器方法都会使得程序变得冗长和难以阅读。第二种方式就是使用JavaBeans模式,类型如下

A a = new A();
a.setFactor_1(100);
a.setFactor_2(50);
a.setFactor_3(320);

这种模式弥补了重叠构造器模式的不足,代码也容易阅读。但缺点就是在构造过程中JavaBean可能处于不一致的状态,这就需要程序员付出额外的努力确保其线程安全。

第三种就是Builder模式,书中代码如下:

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{
        // 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.servings    = servings;
            this.servingSize = servingSize;
        }

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

//当需要生成一个对象时,就可以使用下面语句
NutritionFacts cocaCola = new NutritionFacts.Builder(240, 8).
                calories(100).sodium(35).carbohydrate(27).build();

同时,可以在将参数从builder拷贝到对象中之后,在对象域而不是builder域中对它们进行检查。builder可以自动填充某些区域,例如每次创建对象时自动增加序列号。

注意:可以声明NutritionFacts.Builder类来实现Builder< NutritionFacts >(不懂)

java中传统的抽象工厂实现是Class对象,用newInstance方法充当build一部分(newInstance在java反射中有用到)。但其有一定的问题:

  1. newInstance总是企图调用无参构造器,这个构造器甚至可能不存在
  2. 如果没有类可以访问的无参构造器,也不会收到编译时错误
  3. 客户端代码必须在运行时处理InstantiationException或者IllegalAccessException
  4. newInstance方法还会传播由无参构造器抛出的异常,即使newInstance缺乏相应的throws子句

Builder模式也有缺点

  1. 需要先创建构建器,这可能会影响性能
  2. 比重叠构造器更冗长,因此只在很多参数时才使用

总结:如果类有多个可选参数,Builder模式好用。比之传统的构建器模式,客户端的代码更易于阅读编写;比之JavaBeans更安全。

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

使用私有构造器的单例模式在第一条中已经提及,但是其有一个缺点就是,可以使用java中的反射技术破坏访问到构造器,进而破坏这种模式。

还有另一种实现单例模式更为安全的方法就是使用枚举类型,更加简洁,无偿的提供了序列化机制,是实现单例模式的最佳方法。

enum Singleton{
    INSTANCE;
}

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

class Utility{
    // Suppress default constructor for noninstantiability
    private Utility(){
        throw new AssertionError();
    }

    //other codes
    ...
}

AssertionError不是必须的,但可以避免不小心在类内部调用构造器,保证该类在任何情况下都不会被实例化。

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

一般来说,重用要好于创建。不可变对象与不会被修改的可变对象都是可以始终重用的。

适配器:将功能委托给一个后备对象(适配器后面完成功能的对象),为后备对象提供一个可以替代的借口。由于适配器除了后备对象之外没有其它状态信息,因此对于某个给定对象的特定适配器来说,不需要创建多个适配器实例。

要优先使用基本类型而不是装箱基本类型,要当心无意识的自动装箱。自动装箱对性能有着一定的影响。

小对象的创建回收动作是非常廉价的,还可以提升程序的清晰性、简洁性和功能性。不要单一的认为要尽可能避免创建对象。

除非对象池中的对象是非常重量级的,比如数据库连接对象,否则通过维护自己的对象池来避免创建对象并不是好的做法。

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

所谓过期引用是指永远也不会再被解除的引用。会导致“内存泄漏”的情况出现。

书中使用的例子是栈中元素在被出栈之后,看起来是栈顶位置被清空了,但其实只是指针的位置向下移动了,栈顶中的元素仍然留在栈中,由栈负责维护着这些对象的过期引用。这些对象不会被当作垃圾回收,即使使用栈的程序不再引用这些对象。如果一个对象的引用被无意识的保留了起来,那么GC机制不仅不会处理这个对象,而且也不会处理被这个对象所引用的所有其他对象。

修复方法:一旦对象引用已经过期,只需清空这些引用即可。清空过期引用的另一个好处是,如果以后又被错误的引用,程序就会立即抛出NullpointException,而不是悄悄的运行下去。

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

消除过期引用最好的方法是让包含该引用的变量结束其生命周期,在最紧凑的作用于范围定义每一个变量。

对于Stack类而言,栈顶以下是数组活动区域,栈顶以上是非活动区域,但对于GC来说,是不分的。因此,一旦数组元素变成了非活动的一部分,程序员就该手工清空这些数组元素。一般来说,只要是类自己管理内存,程序员就该警惕内存泄漏问题。

内存泄漏另一个常见来源是缓存。 当缓存项的生命周期是由该键外部引用而不是由值决定时,WeakHashMap有用处。在更一般的情况下,缓存应该时不时的清除掉没用的项。

内存泄漏第三个常见来源是监听器和其它回调。 可以使用弱引用保存它们。

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

终结方法(finalizer)通常是不可预测的,也是很危险的,一般情况下是不必要的。

finalizer不能保证会被及时的执行。及时的执行终结方法正式GC算法的一个主要功能,但在不同的JVM中实现不同。java语言规范不仅不保证finalizer及时的执行,而且根本不保证它们会被执行,因此不能依赖finalizer来更新重要的持久状态,比如释放共享资源。

如果未被捕获的异常在终结过程中被抛出来,那么这种异常可以被忽略,并且该对象的终结过程也会终止。如果异常发生在finalizer中,不会使线程终止,甚至连警告都不会打印。

finalizer会严重的影响性能。

如需释放资源,只需提供一个显式的终止方法即可,通常与try-finally合用,该方法必须在一个私有域中记录下“该对象不再有效”。

finalizer的好处:

  1. 当对象的所有者忘记调用显式的终止方法时,可充当“安全网”
  2. 第二种合理用途与对象的本地对等体有关

终结方法链:如果类(不是Object)有finalizer,且子类覆盖i了finalizer,子类的finalizer就必须手工调用超类的finalizer。

或者为每一个将被终结的对象创建一个附加的对象,把fin放在一个匿名的类中。该匿名类的唯一用途就是终结它的外围实例。

总之,除非是作为安全网,或者是为了终止非关键的本地资源,否则请不要使用终结方法。

猜你喜欢

转载自blog.csdn.net/baidu_37378518/article/details/80964331