《Effective Java》——学习笔记(创建和销毁对象)

版权声明:欢迎转载,请注明出处,谢谢! https://blog.csdn.net/benhuo931115/article/details/79364382

创建和销毁对象

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

静态工厂方法与公有的构造器相比,具有以下几大优势:

  • 优势一:静态工厂方法有名称

    具有适当名称的静态工厂更容易使用,产生的客户端代码也更容易阅读,当一个类需要多个带有相同签名的构造器时,就用静态工厂方法代替构造器,并且慎重地选择名称以便突出它们之间的区别

  • 优势二:不必在每次调用它们的时候都创建一个新对象

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

  • 优势三:可以返回原返回类型的任何子类型的对象

    公有的静态工厂方法所返回的对象的类不仅可以是非公有的,而且该类还可以随着每次调用而发生变化,这取决于静态工厂方法的参数值,只要是已声明的返回类型的子类型,都是允许的。为了提升软件的可维护性和性能,返回对象的类也可能随着发行版本的不同而不同

  • 优势四:在创建参数化类型实例的时候,它们使代码变得更加简洁

    public static <K, V> HashMap<K, V> newInstance() {
        return new HashMap<K, V>();
    }
    
    Map<String, List<String>> m = HashMap.newInstance();
    
    => 等价于
    
    Map<String, List<String>> m = new HashMap<String, List<String>>();
    

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

  • 缺点一:类如果不含公有的或者受保护的构造器,就不能被子类化
  • 缺点二:它们与其他的静态方法实际上没有任何区别

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

静态工厂和构造器有个共同的局限性:它们都不能很好地扩展到大量的可选参数

可以采用重叠构造器模式,在这种模式下,提供第一个只有必要参数的构造器,第二个构造器有一个可选参数,第三个有两个可选参数,以此类推,最后一个构造器包含所有可选参数

public class NutritionFacts {

    private final int servingSize; // required
    private final int servings; // required
    private final int calories; // optional
    private final int fat; // optional
    private final int sodium; // optional
    private final int carbohydrate; // 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模式,在这种模式下,调用一个无参构造器来创建对象,然后调用setter方法来设置每个必要的参数,以及每个相关的可选参数:

public class NutritionFacts {

    // Parameters initialized to default values(if any)
    private int servingSize = -1; // required
    private int servings = -1; // required
    private int calories = 0; // optional
    private int fat = 0; // optional
    private int sodium = 0; // optional
    private int carbohydrate = 0; // optional

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

遗憾的是,JavaBeans模式自身有着很严重的缺点,因为构造过程被分到了几个调用中,在构造过程中JavaBean可能处于不一致的状态,类无法仅仅通过检验构造器参数的有效性来保证一致性,试图使用处于不一致状态的对象,将会导致失败,这种失败调试起来十分困难,另外需要付出额外的努力来确保它的线程安全

第三种替代方法,既能保证像重叠构造器模式那样的安全性,也能保证像JavaBeans模式那么好的可读性,这就是Builder模式的一种形式。不直接生成想要的对象,而是让客户端利用所有必要的参数调用构造器(或者静态工厂),得到一个builder对象,然后客户端在builder对象上调用类似于setter的方法,来设置每个相关的可选参数,最后,客户端调用无参的build方法来生成不可变的对象,这个builder是它构建的类的静态成员类,示例如下:

// Builder Pattern
public class NutritionFacts {

    // Parameters initialized to default values(if any)
    private final int servingSize; // required
    private final int servings; // required
    private final int calories; // optional
    private final int fat; // optional
    private final int sodium; // optional
    private final int carbohydrate; // optional

    public static class Builder {
        // Required parameters
        private final int servingSize;
        private final int servings;

        // Optional parameters - initialized to default values
        private int calories = 0; // optional
        private int fat = 0; // optional
        private int sodium = 0; // optional
        private int carbohydrate = 0; // optional

        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 sodium(int sodium) {
            this.sodium = sodium;
            return this;
        }

        public Builder carbohydrate(int carbohydrate) {
            this.carbohydrate = carbohydrate;
            return this;
        }

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

    public NutritionFacts(Builder builder) {
        servingSize = builder.servingSize;
        servings = builder.servings;
        calories = builder.calories;
        fat = builder.fat;
        sodium = builder.sodium;
        carbohydrate = builder.carbohydrate;
    }
}

NutritionFacts是不可变的,所有的默认参数值都单独放在一个地方,builder的setter方法返回builder本身,以便可以把调用链接起来,客户端代码如下:

NutritionFacts cocaCola = new NutritionFacts.Builder(240, 8)
    .calories(100).sodium(35).carbohydrate(27).build();

builder像个构造器一样,可以对其参数强加约束条件,build方法可以检验这些约束条件,将参数从builder拷贝到对象中之后,并在对象域而不是builder域中对它们进行检验,如果违反了任何约束条件,build方法就应该抛出IllegalStateException

与构造器相比,builder优势在于,builder可以有多个可变(varargs)参数,构造器就像方法一样,只能有一个可变参数

Builder模式十分灵活,可以利用单个builder构建多个对象,builder的参数可以在创建对象期间进行调整,也可以随着不同的对象而改变。builder可以自动填充某些域,例如每次创建对象时自动增加序列号

Builder模式也有它自身的不足,为了创建对象,必须先创建它的构造器,这会有一定的开销。Builder模式还比重叠构造器模式更加冗长,因此它只在有很多参数的时候才使用,比如4个或更多个参数。如果将来需要添加参数,最好一开始就使用构建器

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

Singleton指仅仅被实例化一次的类,Singleton通常被用来代表那些本质上唯一的系统组件,比如窗口管理器或文件系统

实现Singleton,公有的成员是个静态工厂方法:

// Singleton with static factory
public class Elvis {
    private static final Elvis INSTANCE = new Elvis();
    private Elvis() { ... }
    public static Elvis getInstance() { return INSTANCE; }

    public void leaveTheBuilding() { ... }
}

公有域方法的主要好处在于,组成类的成员的声明很清楚地表明了这个类是一个Singleton,公有的静态域是final的,所以该域将总是包含相同的对象引用

如果要实现Singleton类是可序列化的(Serializable),仅仅在声明中加上“implements Serializable”是不够的,还必须提供一个readResolve方法,否则每次反序列化一个序列化的实例时,都会创建一个新的实例

// readResolve method to preserve singleton property
private Object readResolve() {
    // Return the one true Elvis and let the garbage collector
    // take care of the Elvis impersonator
    return INSTANCE;
}

从Java 1.5发行版本起,实现Singleton只需编写一个包含单个元素的枚举类型:

// Enum Singleton - the preferred approach
public enum Elvis {
    INSTANCE;

    public void leaveTheBuilding() { ... }
}

这种方法在功能上与公有域方法相近,但是它更简洁,无偿地提供了序列化机制,绝对防止多次实例化,即使是在面对复杂的序列化或者反射攻击的时候

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

让这个类包含私有构造器,却不允许被实例化

// Noninstantiable utility class
public class UtilityClass {
    // Suppress default constructor for noninstantiability
    private UtilityClass() {
        throw new AssertionError();
    }
}

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

对于同时提供了静态工厂方法和构造器的不可变类,通常可以使用静态工厂方法而不是构造器,以避免创建不必要的对象

除了可以重用不可变的对象之外,也可以重用那些已知不会被修改的可变对象

示例:

public class Person {

    private final Date birthDate;

    public boolean isBabyBoomer() {
        // Unnecessary allocation of expensive object
        Calendar gmtCal = Calendar.getInstance(TimeZone.getTimeZone("GMT"));
        gmtCal.set(1946, Calendar.JANUARY, 1, 0, 0, 0);
        Date boomStart = gmtCal.getTime();
        gmtCal.set(1965, Calendar.JANUARY, 1, 0, 0, 0);
        Date boomEnd = gmtCal.getTime();
        return birthDate.compareTo(boomStart) >= 0 &&
                birthDate.compareTo(boomEnd) < 0;
    }

}

isBabyBoomer每次调用的时候,都会新建一个Calendar、一个TimeZone和两个Date实例,这是不必要的,改进如下:

public class Person {

    private final Date birthDate;

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

    static {
        Calendar gmtCal = Calendar.getInstance(TimeZone.getTimeZone("GMT"));
        gmtCal.set(1946, Calendar.JANUARY, 1, 0, 0, 0);
        BOOM_START = gmtCal.getTime();
        gmtCal.set(1965, Calendar.JANUARY, 1, 0, 0, 0);
        BOOM_END = gmtCal.getTime();
    }

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

}

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

下例程序:

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

    private void ensureCapacity() {
        if (elements.length == size) {
            elements = Arrays.copyOf(elements, 2 * size + 1);
        }
    }

}

这段程序有一个“内存泄漏”,随着垃圾回收器活动的增加,或者由于内存占用的不断增加,程序性能的降低会逐渐表现出来

如果一个栈先是增长,然后再收缩,那么,从栈中弹出来的对象将不会被当作垃圾回收,即使使用栈的程序不再引用这些对象,它们也不会被回收,这是因为,栈内部维护着对这些对象的过期引用(obsolete reference)。所谓的过期引用,是指永远也不会再被解除的引用

这类问题的修复方法很简单:一旦对象引用已经过期,只需清空这些引用即可

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

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

清空对象引用应该是一种例外,而不是一种规范行为,消除过期引用最好的方法是让包含该引用的变量结束其生命周期

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

内存泄漏的另一个常见来源是缓存,如果在缓存之外存在对某个项的键的引用,该项就有意义,那么就可以用WeakHashMap代表缓存;当缓存中的项过期之后,它们就会自动被删除,只有当所有的缓存项的生命周期是由该键的外部引用而不是由值决定时,WeakHashMap才有用处

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

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

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

终结方法的缺点在于不能保证会被及时地执行。从一个对象变得不可到达开始,到它的终结方法被执行,所花费的这段时间是任意长的

Java语言规范不仅不保证终结方法会被及时执行,而且根本就不保证它们会被执行,所以不应该依赖终结方法来更新重要的持久状态,例如依赖终结方法来释放共享资源(比如数据库)上的永久锁

正常情况下,未被捕获的异常将会使线程终止,并打印出栈轨迹(Stack Trace),但是,如果异常发生在终结方法之中,则不会如此,甚至连警告都不会打印出来

使用终结方法还会有一个非常严重的(Severe)性能损失

如果类的对象中封装的资源(例如文件或者线程)确实需要终止,只需提供一个显式的终止方法,并要求该类的客户端在每个实例不再有用的时候调用这个方法。该实例必须记录下自己是否已经被终止了:显式的终止方法必须在一个私有域中记录下“该对象已经不再有效”,如果这些方法是在对象已经终止之后被调用,其他的方法就必须检查这个域,并抛出IllegalStateException异常

显式终止方法的典型例子是InputStream、OutputStream和java.sql.Connection上的close方法

显式的终止方法通常与try-finally结构结合起来使用,以确保及时终止。在finally子句内部调用显式的终止方法,可以保证即使在使用对象的时候有异常抛出,该终止方法也会执行:

Foo foo = new Foo(...);
try {
    ...
} finally {
    foo.terminate();
}
阅读更多

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