《Effective Java》-----创建和销毁对象

版权声明:本文为博主原创文章,转载请注明出处。 https://blog.csdn.net/hanxueyu666/article/details/78588926

何时以及如何创建对象?何时以及如何避免创建对象?如何确保他们能够适时的销毁,以及如何管理对象销毁之前必须进行的各种清理动作?带着问题进入今天的学习

一、考虑用静态工厂方法代替构造器

此处所提到的静态工厂,并不是设计模式中的静态工厂,其实就是一个静态方法。用来返回类的实例。因此类可以通过静态工厂方法来提供它的客户端,而不是公有的构造器。当然做当然会有很多的优势,下面是Boolean的API源码,方法将boolean基本类型转成了一个Bollean对象的引用就是利用了静态工厂方法

/**
     * Returns a {@code Boolean} instance representing the specified
     * {@code boolean} value.  If the specified {@code boolean} value
     * is {@code true}, this method returns {@code Boolean.TRUE};
     * if it is {@code false}, this method returns {@code Boolean.FALSE}.
     * If a new {@code Boolean} instance is not required, this method
     * should generally be used in preference to the constructor
     * {@link #Boolean(boolean)}, as this method is likely to yield
     * significantly better space and time performance.
     *
     * @param  b a boolean value.
     * @return a {@code Boolean} instance representing {@code b}.
     * @since  1.4
     */
    public static Boolean valueOf(boolean b) {
        return (b ? TRUE : FALSE);
    }


1.1、静态工厂方法与构造器不同的第一个优势在于,他们有名字

一个类只能有一个带有指定签名的构造器。编程人员通常通过提供两个构造器,他们的参数列表只在参数类型的顺序上有所不同。实践上这并不是很好,面对API,用户永远了记不住该用哪个构造器,通常会调用错误的构造器。由于静态工厂方法有名字,所以当一个类需要多个带有相同签名的构造器时,就用静态工厂方法代替构造器,并且慎重的选择名称以便突出他们的区别。

1.2、静态工厂方法与构造器不同的第二个优势在于,不必在每次调用他们的时候都创建一个新的对象

正如上面的valueOF静态方法,从来不创建对象,只是把对象的引用返回,这种方法类似于享元模式。如果程序经常请求创建相同的对象,并且创建对象的代价比较高,这种方法可以大大提升程序的性能
静态工厂能够为重复的调用返回相同对象,这样有助于类总能严格控制在某个时刻那些实例应该存在。这种类被称作实例受控的类。受控的类使得类可以确保它是一个Singleton或者是不可实例化。它还使得不可变的类可以确保不会存在两个相等的实例,即当且仅当a==b的时候才有a.equals(b)为true。如果类保证了这一点,它的客户端就可以使用==操作符来代替equals(Object)方法,这样可以提升性能。

1.3、静态工厂方法与构造器不同的第三大优势在于,它们可以返回原返回类型的任何子类型的对象

这样我们在选择返回对象的类时就有了更大的灵活性。这项技术适用于基于接口的框架。在这种框架中,接口为静态工厂方法提供了自然返回类型。接口不能有静态方法,因此按照惯例,接口Type的静态工厂方法被放在一个名为Types的不可实例化的类中。(所谓不可实例化,可以理解为让构造器为私有的。)
例如Java collections Framework的集合接口有32个便利实现,分别提供了不可修改的集合、同步集合等等。几乎所有这些实现都通过静态工厂方法在一个不可实例化的类(java.util.Collections)中导出。所有返回对象的类都是非公有的。

静态工厂返回对象所属的类,在编写包含该静态工厂的类第可以不必存在。这种灵活的静态工厂方法构成服务提供者架构的基础,例如JDBC API。服务提供者框架是指这样一个系统:多个服务提供者实现一个服务,系统为服务者的客户端提供多个实现,并把他们从多个实现中解耦出来,下面是对服务提供者架构的一个简单的实现
//服务提供者接口,用来提供服务
//这个接口时可选的,如果没有,下面的Map集合就可以直接存放Service对象
public interface Provider {
    Service newService();
}

//服务接口, 用来写具体的服务内容
public interface Service {
    // Service-specific methods go here
}
public class Services {
    private Services() {
    } // Prevents instantiation (Item 4)

    // Maps service names to services
    //服务的注册和访问
    //用一个Map来存放提供者
    private static final Map<String, Provider> providers = new ConcurrentHashMap<String, Provider>();
    public static final String DEFAULT_PROVIDER_NAME = "<def>";

    // Provider registration API
    //注册默认的提供者
    public static void registerDefaultProvider(Provider p) {
        registerProvider(DEFAULT_PROVIDER_NAME, p);
    }
    //注册非默认的提供者,把提供者加入Map
    public static void registerProvider(String name, Provider p) {
        providers.put(name, p);
    }

    // Service access API
    //使用这个方法创建实例
    public static Service newInstance() {
        return newInstance(DEFAULT_PROVIDER_NAME);
    }

    //创建的时候看看是不一样已经注册了provider
    public static Service newInstance(String name) {
        Provider p = providers.get(name);
        if (p == null)
            throw new IllegalArgumentException(
                    "No provider registered with name: " + name);
        return p.newService();
    }
}


1.4、静态工厂方法的主要缺点在于,类如果不包含公有法人或者受保护的构造器,就不能被子类化。第二个缺点,他们与其他的静态方法没有什么区别。


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

前面提到的静态工厂和构造器有一个局限性:它们都不能很好地扩展到大量的可选参数。
在遇到这种情况下,大多数程序员都会习惯用重叠构造器,也就是写多个构造器,每个构造器的参数不同。在这种模式下,你提供一个只有必要参数的构造器,第二个构造器有一个可选参数,第三个有两个可选参数。一次类推,最后一个构造器包含所有的可选参数,如下面的例子
public class NutritionFacts {
    private final int servingSize; // (mL) required
    private final int servings; // (per container) required
    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;
    }

    
}

重叠的构造器可行,但是当许多参数的时候,客户端代码很难编写,并且较难阅读。

遇到许多构造器参数的时候,还可以考虑第二种代替方法,即JavaBeans模式。如下

public class NutritionFacts {
    // Parameters initialized to default values (if any)
    private int servingSize = -1; // Required; no default value
    private int servings = -1; // "     " "      "
    private int calories = 0;
    private int fat = 0;
    private int sodium = 0;
    private int carbohydrate = 0;

    public NutritionFacts() {
    }

    // Setters
    public void setServingSize(int val) {
        servingSize = val;
    }

    public void setServings(int val) {
        servings = val;
    }

    public void setCalories(int val) {
        calories = val;
    }

    public void setFat(int val) {
        fat = val;
    }

    public void setSodium(int val) {
        sodium = val;
    }

    public void setCarbohydrate(int val) {
        carbohydrate = val;
    }

    public static void main(String[] args) {
        NutritionFacts cocaCola = new NutritionFacts();
        cocaCola.setServingSize(240);
        cocaCola.setServings(8);
        cocaCola.setCalories(100);
        cocaCola.setSodium(35);
        cocaCola.setCarbohydrate(27);
    }
}



javaBeans模式的缺点,因为构造过程被分到了几个调用中,在构造过程中JavaBean可能处于不一致的状态。另一个缺点就是JavaBeans模式阻止了把类做成不可变得可能。这就需要程序员来确保它的线程安全

最后放大招了,第三种代替方法Builder模式,这种模式,既能保证像重叠构造器那样的安全性,也能保证像JavaBeans模式那么好的可读性。代码如下


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

   
}


简而言之,如果类的构造器或者静态工厂中具有多个参数,设计这种类,Builder模式就是一种不错的选择,特别是当大多数参数都是可选的时候。与传统的重叠构造器模式相比,使用Builder模式的客户端将更易于阅读和编写,构建器也比JavaBeans更加安全



三、用私有构造器或者枚举类型强化Sigleton属性

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

企图通过将类做成抽象类来强制该类不可实例化时行不通的。该类可以被子类化

五、避免创建不必要的对象

一般来说,最好能重用对象而不是在每一次需要的时候创建一个新的对象。如果对象时不可变的,那么他就始终可以被重用。比如我们常用的String类型,就是不可变的,所以我们使用字符串的时候可以考虑String s = “stringette”,而不是String s = new String("stringette");
同样,可以用第一条中说到的静态工厂方法创建对象,来避免创建不必要的对象。例如静态工厂方法Boolen.valueOf(String)几乎总是优先于构造器Boolean(String)。构造器在每次调用的时候都会创造一个人新的对象,而静态工厂就不要求必须产生一个新的对象。

除了重用不可变的对象之外,也可以重用那些已知不会被修改的对象。比如有一些特殊的对象,如新中国的成立的日期,创建出来就不能在变,人的出生日期一般也不会改变(除非登记错了)

需要注意的一点是,从java1.5发型版本中,有一种创建多余对象的新方法,成为自动装箱,它允许程序员将基本类型和装箱类型混用,按需求自动装箱或拆箱,这样就加大了开销,所以我们要尽量避免自动的多次拆装箱,考虑下面的程序,计算所有int正值的总和

// Hideously slow program . Can you spot the object creation?

  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而不是long,意味着程序构造了大约2的31个多余的Long实例(大约每次往Long sum中增加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)  
            throw new EmptyStackException();  
        return elements[--size];  
    }  
  
    /** 
     * Ensure space for at least one more element, roughly 
     * doubling the capacity each time the array needs to grow. 
     */  
    private void ensureCapacity() {  
        if (elements.length == size)  
            elements = Arrays.copyOf(elements, 2 * size + 1);  
    }  
}  


这段程序表面上没有什么问题,但是这段程序隐藏着一个问题-----内存泄漏
那么程序中哪里发生了内存泄漏呢?如果一个栈先是增长,然后收缩,那么,从栈中弹出的对象将不会被当做垃圾回收,即使使用栈的程序不再引用这些对象,它们也不会被回收。这是因为栈内部维护者对这些对象的过期引用(所谓过期引用,就是永远也不会解除的引用)
这类问题的修复方法很简单:一旦对象引用已经过期,只需要清空这些引用即可。对于上面问题pop方法可以修改为
public Object pop() {  
        if (size == 0)  
            throw new EmptyStackException();  
        Object result = elements[--size];  
	element[size] = null;
	return result;
    } 


清空过期引用的另一个好处是,如果它们以后又被错误地解除引用,程序就会立即抛出NullPoinerException异常,而不是悄悄地错误的运行下去。

  

既然存在上面所说的问题,那么是不是要对每一个对象引用都在程序不再用到它的时候手到将引用清空呢?这样其实没什么必要,因为它会把代码弄的很乱,“清空对象引用应该是一种例外,而不是一种规范行为”。消除过期引用的最好方法是让包含该引用的变量结束其生命周期。那么,应该在何时清空引用呢?在如上的例子中,由于stack自己维持着存储池(数组),所以垃圾回收器不会将过期的引用当做垃圾,因为它认为数组中的所有对象引用都同等有效,那么在这种情况下,只有程序员知道哪些是有效的,哪些是过期的,就需要手工清空这些数组元素。一般来言,只要类是自己管理内存的,程序员就需要警惕内存泄漏的问题。一旦元素被释放掉,则该元素中包含的任何对象引用都应该被清空。
        内存泄漏的另一个常见的来源是缓存,一旦把对象引用放入缓存中,它就很容易被遗忘。对于这种情况有几种可能的解决方案,如果你正好要实现一个只要在缓存之外存在对某个项的键的引用,该项就有意义这样的缓存的话,就可以使用WeakHashMap代表缓存,因为当缓存中的项过期的时候,它们就会自动被删除掉。但是只有当所要的缓存项的生命周期是由key的外部引用而不是由value决定时WeakHashMap才有用处。
        更常见的情况是“缓存项的生命周期是否有意义”,并不是很容易确定,随着时间的推移,其中的项会变得越来越没有价值,在这种情况下,缓存应该时不时地清除掉没用的项,这种工作可以交给一个后台线程(可能是Timer或者ScheduledThreadPoolExecutor)来完成,或者也可以在给缓存添加新条目的时候顺便进行。LinkedHashMap类利用它的removeEldestEntry方法可以很容易的实现后一种方案,对于更加复杂的缓存,就只能直接使用java.lang.ref了。
        内存泄漏的第三种常见来源是监听器和其他的回调函数。如果客户在你实现的API中注册回调,但是却没有显示取消注册。那么除非你采取些手段,否则它们就会积聚。确保回调立即被当作垃圾的最佳方法是只保存它们的弱引用,例如,只将他们保存为WeakHashMap中的键。

七、避免使用终结方法

终结方法通常是不可预测的,也是危险的,一般情况下是不必要的。使用终结方法会导致行为不稳定、降低性能,以及可移植问题

终结方法的缺点:
1、不能保证会被及时的执行,例如:用终结方法来关闭已经打开的文件,这是严重错误,因为打开文件的描述符是一种很有限的资源
2、终结方法有严重的性能损失
3.Java语言规范不保证终结方法会被执行,当一个程序终止,某些无法访问的对象的终结方法没有被执行是完全有可能的,比如,依赖终结方法来释放数据库上的永久锁,可能会导致锁永远不会被释放。

如果类的对象中封装的资源确实需要终止,应该怎么做:

提供一个显式的终结方法。意思是,我们不去覆盖finalize方法,而是自己编写一个终止资源的方法。比如,java.io.FileInputStream的close方法。当使用完这个FileInputStream对象时,显式调用close() 来回收资源。

终结方法的好处:


1.当对象的所有者忘记调用显式终结方法时,终结方法可以充当"安全网"。


来看看FileInputStream是怎么做的:


public void close() throws IOException {
        synchronized (closeLock) {
            if (closed) {
                return;
            }
            closed = true;
        }
        if (channel != null) {
           channel.close();
        }


        fd.closeAll(new Closeable() {
            public void close() throws IOException {
               close0();
           }
        });
}


protected void finalize() throws IOException {
        if ((fd != null) &&  (fd != FileDescriptor.in)) {
            /* if fd is shared, the references in FileDescriptor
             * will ensure that finalizer is only called when
             * safe to do so. All references using the fd have
             * become unreachable. We can call close()
             */
            close();
        }
}


可以看到FileInputStream还是有覆盖finalize方法的,而里面做的就是调用close方法,这是为了当对象持有者忘记调用close方法,在finalize方法中为它做调用close的事,这就是“安全网”的意思。


 


2.终结方法的守卫者。把终结方法放在一个匿名的类,该匿名类的唯一用途就是终结它的外围实例。外围实例持有对终结方法守卫者的唯一实例,这意味着当外围实例是不可达时,这个终结方法守卫者也是不可达的了,垃圾回收器回收外围实例的同时也会回收终结方法守卫者的实例,而终结方法守卫者的finalize方法就把外围实例的资源释放掉,就好像是终结方法是外围实例的一个方法一样。


来看看java.util.Timer的终结方法守卫者:


public void cancel() {
        synchronized(queue) {
            thread.newTasksMayBeScheduled = false;
            queue.clear();
            queue.notify();  // In case queue was already empty.
        }
}




private final Object threadReaper = new Object() {
        protected void finalize() throws Throwable {
            synchronized(queue) {
                thread.newTasksMayBeScheduled = false;
                queue.notify(); // In case queue is empty.
            }
        }
};



cancel方法是Timer提供的显式终止方法,threadReaper是一个私有变量,保证除了实例本身外,没有对它的引用,它覆盖finalize方法,实现与cancel方法一样的功能。



阅读更多

没有更多推荐了,返回首页