Effective Java读书笔记 -- 第二章:创建和销毁对象

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

    静态工厂方法优点:

        1、静态工厂方法有名称

            具有适当名称的静态工厂更易于阅读和被使用。

        2、不必在每次调用时都要创建一个新的对象

            静态工厂方法能够为重复的调用返回相同对象,这样有助于类总能严格控制在某个时刻哪些实例应该存在。

            静态工厂方法也可使不可变类可以使用预先构建好的实例,或者将构建好的实例缓存起来以便重复利用,从而避免创建不必要的重复对象。

        3、可以返回原返回类型的任何子类型的对象

            API可以返回对象,同时又不会是对象的类变成公有的,以这种方式隐藏实现类会使API变得非常简洁。

        4、在创建参数化类型实例的时候,使代码更加简洁。

    静态工厂方法的主要缺点:

        1、类如果不含有公有的或受保护的构造器,就不能被子类化

        2、很难实例化一个提供了静态工厂方法而不是构造器的类


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

    静态工厂和构造器有个共同的局限性:它们都不能很好地扩展到大量的可选参数。这时候,就需要使用构建器(模式):

package JavaDay5_26;

/**
 * @author [email protected]
 * @date 18-5-26  下午8:19
 */

public class Cat {
    private final String name;
    private final int age;
    private final String bodyColor;
    private final String mood;

    public static class Builder {
        //必须参数
        private final String name;
        private final int age;

        //可选参数
        private String bodyColor = "black";
        private String mood = "happy";

        public Builder(String name, int age) {
            this.name = name;
            this.age = age;
        }

        public Builder buildBodyColor(String bodyColor) {
            this.bodyColor = bodyColor;
            return this;
        }

        public Builder buildMood(String mood) {
            this.mood = mood;
            return this;
        }

        public Cat build() {
            return new Cat(this);
        }
    }

    private Cat(Builder builder) {
        name = builder.name;
        age = builder.age;
        bodyColor = builder.bodyColor;
        mood = builder.mood;
    }

    @Override
    public String toString() {
        return "Cat{" +
                "name='" + name + '\'' +
                ", age=" + age +
                ", bodyColor='" + bodyColor + '\'' +
                ", mood='" + mood + '\'' +
                '}';
    }

    public static void main(String[] args) {
        Cat cat = new Builder("vina", 3).buildBodyColor("white").buildMood("so happy").build();
        System.out.println(cat);
    }
}

    builder模式模拟了具名的可选参数。它也像个构造器一样,可以对其参数强加约束条件;与构造器器相比,builder的优势在于,builder可以有多个可变参数。

    Builder模式也有它自身的不足,为了创建对象,必须先创建它的构建器。

    简而言之,如果类的构造器或者静态工厂中具有多个参数,设计这种类时,Builder墨水就是中不错的选择,特别是当大多数参数都是可选的时候。


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

    Singleton指仅仅被实例化一次的类(即单例类),在之前的单例模式讲解中,讲解了两种方法:饿汉式和懒汉式,其实还有另外一种:只编写一个包含单个元素的枚举类型(大概是实现Singleton的最佳方法)。前两种方法可能通过Java的反射机制调用私有构造器创建其他实例(可以通过修改构造器,使其在要求被创建第二个实例的时候,抛出异常来防止)。

    单例模式可以使用私有构造器,私有静态常量,公共静态方法初始化的方法构造,也可以使用公共静态常量,私有构造器直接返回单例.但前一种方法我们可以自己定义初始化过程,可以根据需要改变方式(单例,多例,或者为每个线程构造一个(此时需要持有一个含有线程信息与实例一一对应的 map,每次获取实例时从该 map 中获取当前线程对应的实例,没有再初始化(Thread.currentThread()))).注意所有的私有构造器都有可能被使用反射构造多个实例,此时可以在构造方法中增加校验,抛出异常来避免这种情况. 

    单例模式还需要注意对于序列化,为了保证单例必须写方法readResolve(),才能保证单例。

    反序列化与反射方法对单例带来的副作用均可以使用单个元素的枚举类型来避免这些现象。


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

    有些工具类不希望被用户实例化,可以为该工具类创建一个显示的私有的构造器而避免被实例化。但也使得该类不能被子类化,因为所有的构造器都必须显式或隐式调用超类的构造器。


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

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

    String str1 = new String("Don't do this.");
    String str2 = "This way is better.";

    第一个语句在每次被执行的时候都创建一个新的String的实例;第二个语句可以保证在同一台虚拟机中运行的代码,只要他们包含相同的字符创字面常量,该String对象就会被重用。

    对于同时提供了静态工厂方法和构造器的不可变类,通常可以使用静态工厂方法而不是构造器,以避免创建不必要的对象,因为构造器每次被调用的时候都会创建一个新的对象,而静态工厂方法不会这样做。

    另外一个需要注意的是:要有限使用基本类型而不是装箱基本类型,要当心无意识的自动装箱,如下面的栗子:    

    Long sum = 0L;
    for(long i = 0; i < Integer.MAX_VALUE; i++) {
        sum += i;
    }
    System.out.println(sum);

    运行时间为:
    

    改进一下,如下:

    long sum = 0L;//此处将Long对象类型改为long基本类型
    for(long i = 0; i < Integer.MAX_VALUE; i++) {
        sum += i;
    }
    System.out.println(sum);

    运行时间显著下降:

    

    通常创建对象的代价非常昂贵,应该要尽可能地避免创建对象。但是,现在小对象的创建和回收动作在现代虚拟机上是非常廉价的,而通过创建附加的对象,有时候可以更好地替身程序的清晰性、简洁性和功能性。

        (通过以下代码计算程序运行时间,单位为ms)

    long startTime = System.currentTimeMillis();   //获取开始时间
    long endTime = System.currentTimeMillis(); //获取结束时间
    System.out.println("程序运行时间: "+(endTime - startTime) + "ms");


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

    举个简单的栈实现的栗子:

package JavaDay5_26;

import java.util.Arrays;
import java.util.EmptyStackException;

/**
 * @author [email protected]
 * @date 18-5-26  下午9:18
 */

public class Stack {
    private Object[] items;
    private int size = 0;
    private static final int DEFAULT_INITAL_CAPACITY = 16;
    
    public Stack() {
        items = new Object[DEFAULT_INITAL_CAPACITY];
    }
    
    public void push(Object o) {
        ensureCapacity();
        items[size++] = o;
    }
    
    public Object pop() {
        if(size == 0) {
            throw new EmptyStackException();
        }
        return items[--size];
    }

    /**
     * 确保容量足够且自动扩增
     */
    private void ensureCapacity() {
        if(items.length == size) {
            items = Arrays.copyOf(items, 2 * size + 1);
        }
    }
}

    这段代码没有很明显的错误。但严格来讲这段程序有一个“内存泄露”的问题。因为从栈中pop的对象不会被当做垃圾回收,即时用户不再引用这些对象,他们也不会被回收,因为栈内部维护着对这些对象的过期引用(obsolete reference)。所谓过期引用,是指永远不会再被接触的引用。

    这个问题的修改方法很简单,只需在对象引用过期时,清空这些引用即可:

    public Object pop() {
        if(size == 0) {
            throw new EmptyStackException();
        }
        Object result = items[--size];
        items[size] = null;//消除过期引用
        return result;
    }

    一般而言,只要类是自己管理内存,就应该警惕内存泄露问题。内存泄露的另一个常见来源是缓存。内存泄露的第三个来源是监听器和其他回调。


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

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

    终结方法的缺点在于不能保证会被及时地执行。从一个对象变得不可达开始到它的终结方法被执行,所花费的时间是任意长的。这意味着,注重时间的任务不应该有终结方法来完成。

    及时地执行终结方法正是垃圾回收算法的一个主要功能,这种算法在不同的JVM实现会大相径庭。

    如果未被捕获的异常在终结过程中被抛出来,那么这种异常可以被忽略,并且该对象终结过程也会终止。

    还有一点:使用终结方法有一个非常严重的性能损失。

    终结方法有两种合法用途:

        1、当对象所有者忘记调用前面段落中建议的显式终止方法时,终结方法可以充当“安全网”。

        2、与对象的本地对等体有关。本地对等体是一个本地对象,普通对象通过本地方法委托给一个本地对象,因为本地对象不是一个普通对象,所以垃圾回收器不会知道它,当它的Java对等体被回收的时候,它不会被回收。在本地对等体拥有必须被及时终止的资源的前提下,终结方法正是执行这项任务最合适的工具。

        总之,除非是作为安全网,或者是为了终止非关键的本地资源,否则请不要使用终结方法。还有,既然使用了终结方法,就要记得调用super.finalize()。如果用终结方法作为安全网,要记得记录终结方法的非法用法。


总结:以上就是读完本书第二章之后,记录下一些重要的点,希望自己能多多注意。共勉。

猜你喜欢

转载自blog.csdn.net/weixin_41704428/article/details/80464522