[Java读书笔记] Effective Java(Third Edition) 第2章 创建和销毁对象

 

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

对于类而言,获取一个实例的方法,传统是提供一个共有的构造器。

  类可以提供一个公有静态工厂方法(static factory method), 它只是一个返回类的实例的静态方法。

  示例:Boolean的装箱类,将boolean基本类型值转换成一个Boolean对象引用

public static Boolean valueOf(boolean b) {  
    return b ? Boolean.TRUE : Boolean.FALSE;  
}  

静态工厂方法与构造器不同的优点

  第一大优势:有名称。更容易使用,更容易阅读理解。

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

  第三大优势:它们可以返回类型的任何子类型的对象。

  例如Java Collections Framework的集合接口45个工具实现,提供了不可修改的集合、同步集合等等。几乎所有都是通过静态工厂方法在一个不可实例化的类(java.util.Collections)中导出。

  第四大优势:所有返回对象的类可以随着每次调用而发生变化,这取决于静态工厂方法的参数值。

  第五大优势: 方法返回的对象所属的类,在编写包含该静态工厂方法的类时可以不存在。

静态工厂方法构成了服务提供者框架(Service Provider Framework)的基础。例如JDBC(Java数据库连接) API.

静态工厂方法缺点

  第一: 类如果不含共有的或受保护的构造器,就不能被子类化。

  第二:程序员很难发现他们。在API文档中没有明确标识出来。

静态工厂方法一些惯用名称:

 from:

Date d = Date.from(instant);  

of:

Set<Rank> faceCards = EnumSet.of(JACK, QUEEN, KING);  

valueOf:

BigInteger prime = BigInteger.valueOf(Integer.MAX_VALUE);  

instance或getInstance:

StackWaler luke = StackWaler.getInstance(options);  

create或newInstance:

Object newArray = Array.newInstance(classObject, arrayLen);  

getType:

FileStore fs = Files.getFileStore(path);  

newType:

BufferReader br = Files.newBufferReader(path);  

type(getType和newType的简化版):

List<Complaint> litany = Collections.list(legacyLitany);  

总结,静态工厂方法和共有构造器各有用处,需要理解各自长处。静态工厂方法经常更加合适,因此切忌第一反应是提供公有构造器,而不是考虑静态工厂方法。

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

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

  第一种方案(一般解决方法):重叠构造器(telescoping constructor)模式:

    提供第一个构造器只有必要的参数,第二个构造器有一个可选参数,第三个构造器有二个可选参数,以此类推,最后一个构造器包含所有可选参数。

    缺点:重叠构造器模式可行,但是有很多参数时,代码很难编写,难以阅读。

  第二种方案:JavaBeans模式:

    先调用一个无参数构造器来创建对象,然后再调用setter方法来设置每个必要参数和每个可选参数。

   缺点:在构造过程中,JavaBeans可能处于不一致的状态。(线程不安全) 另外一点是这种模式就不能把类做成不可变的。

  第三种方案:建造者(builder)模式:

   例子:

   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 {
        // 必须属性
        private final int servingSize;
        private final int servings;
        // 可选属性
        private int calories = 0;
        private int fat = 0;
        private int sodium = 0;
        private int carbohydrate = 0;

        public Builder(int servingSize, int servings) {
            this.servingSize = servingSize;
            this.servings = servings;
        }

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

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

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

        public Builder setCarbohydrate(int carbohydrate) {
            this.carbohydrate = carbohydrate;
            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模式也适用于类层次结构。例子:抽象类Pizza, 两个子类,一个NyPizza表示经典纽约风味披萨,一个Calzone表示馅料内置半月形披萨。(代码略)

 使用代码:

NyPizza pizza = new NyPizza.Builder(SMALL).addTopping(SAUSAGE).addToping(ONION).build();
Calzone calzone = new Calzone.Builder().addTopping(HAM).sauceInside().build();

总结: 如果类的构造器或者静态工厂中具有多个参数,Builder模式是一种很好的选择,特别是当大多数参数是可选参数。

第 3 条:使用私有构造器或枚举类型来强制实现 singleton 属性

单例(singleton)就是一个只实例化一次的类。通常用于代表一个无状态的对象,比如函数,或者本质上是唯一的系统组件。

使类成为Singleton会使它的客户端测试变得十分困难。

  两种常见实现方式:

    1. Public final field: 公有静态成员是一个final域:

  

// Singleton with public final field
public class Elvis {
    public static final Elvis INSTANCE = new Elvis();
    private Elvis() { ... }
    public void leaveTheBuilding() { ... }
}

   该方法的优点:

  • API 明确表示这个类就是一个单例。公共静态属性是 final 的,所以它总是包含相同的对象引用。
  • 这个方法很简单。

    2. Singleton with static factory 公有成员是一个静态工厂方法:

// 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() { ... }
}

该方法的优点:

  • 可以方便地将类的实现改为非单例,并且用户代码不需要改变。
  • 可以编写一个泛型单例工厂。
  • 方法引用可以被用作 supplier,例如 Elvis::instance 等同于 Supplier<Elvis>。

为了实现Singleton类变成可序列化的(Serializable), 需要2点:

1. 声明中加implements Serializable。

2. 必须声明所有实例域都是瞬时的(transient),并提供一个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;
}

第三种实现方法:声明一个包含单个元素的枚举类型:

// Enum singleton - the preferred approach
public enum Elvis {
    INSTANCE;
    public void leaveTheBuilding() { ... }
}

 这种方法在功能上与公有域方法相似,更加简洁。无偿地提供序列化机制,绝对防止多次实例化。

   总结:单元素的枚举类型经常成为实现Singleton的最佳方法。

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

  有时候可能需要编写只包含静态方法和静态域的类。比如java.lang.Math或者java.util.Arrays。

  还有java.util.Collections的方式,吧实现特定接口的对象上的静态方法,包括工厂方法组织起来。

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

  该类可以被子类化,并且该子类也可以被实例化。

  只有让这个类包含一个私有构造器,他才不能被实例化。

  副作用:使得一个类不能被子类化(不能被继承)。所有的构造器都必须显式或隐式地调用超类(superclass)构造器,

  而子类就没有可以访问的超类的构造器可调用了。

第 5 条:优先考虑依赖注入来引入资源

  有许多类会依赖一个或者多个底层的资源。例如,拼写检查器需要依赖一个或多个词典。

  静态工具类和Singleton类不适合于需要引用底层资源的类。

  这里需要能够支持类的多个实例,每一个实例都使用客户端指定的资源(本例中的词典)。

  满足该条件的模式是:当创建一个新的实例时,就将该资源传到构造器中。(依赖注入的一种形式)

// Dependency injection provides flexibility and testability
public class SpellChecker {
    private final Lexicon dictionary;
    public SpellChecker(Lexicon dictionary)     {
        this.dictionary = Objects.requireNonNull(dictionary);
    }
    public boolean isValid(String word) { ... }
    public List<String> suggestions(String typo) { ... }
}

  总之,不要用Singleton和静态工厂类来实现依赖一个或者多个底层资源的类,且该资源的行为会影响到该类的行为,也不要直接用这个类来创建这些资源。

  而应该讲这些资源或者工厂传给构造器(或者静态工厂,构建器),通过它们来创建类。这就是依赖注入

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

  通常来讲,重用一个对象比创建一个功能相同的对象更加合适。

  重用速度更快,并且更接近现代的代码风格。如果对象是不可变的(immutable)(条款 17),它总是可以被重用。

  第一种多余创建对象的场景:String字符串:

String s = new String("bikini"); // DON'T DO THIS!

  这个语句每次执行时都会创建一个新的 String 实例,而这些实例的创建都是不必要的。

  如果这种用法发生在循环或者频繁调用的方法中,就会创建数百万个毫无必要的 String 实例。

  改进后:

String s = "bikini";

  第二种多余创建对象的场景:正则表达式:

  写一个方法来确定一个字符串是否是一个合法的罗马数字:

// Performance can be greatly improved!

static boolean isRomanNumeral(String s) {

    return s.matches("^(?=.)M*(C[MD]|D?C{0,3})"

            + "(X[CL]|L?X{0,3})(I[XV]|V?I{0,3})$");

}

  这个实现的问题在于它依赖于 String.matches 方法。

  虽然 String.matches 是检查字符串是否与正则表达式匹配的最简单方法,但它不适合在性能临界的情况下重复使用。

  因为它在内部为正则表达式创建一个 Pattern 实例,并且只使用一次,之后这个 Pattern 实例就会被 JVM 进行垃圾回收。

  创建 Pattern 实例是昂贵的,因为它需要将正则表达式编译成有限状态机(finite state machine)。

  为了提高性能,将正则表达式显式编译为一个 Pattern 实例(不可变)并且缓存它,在 isRomanNumeral 方法的每个调用中重复使用相同的实例:

// Reusing expensive object for improved performance
public class RomanNumerals {
    private static final Pattern ROMAN = Pattern.compile(
            "^(?=.)M*(C[MD]|D?C{0,3})"
            + "(X[CL]|L?X{0,3})(I[XV]|V?I{0,3})$");

    static boolean isRomanNumeral(String s) {
        return ROMAN.matcher(s).matches();
    }
}

  第三种多余创建对象的场景:自动装箱(autoboxing)

  它允许程序员混用基本类型和包装的基本类型,根据需要自动装箱和拆箱。 自动装箱模糊不清,但不会消除基本类型和装箱基本类型之间的区别。

// Hideously slow! Can you spot the object creation?

private static long sum() {
    Long sum = 0L;
    for (long i = 0; i <= Integer.MAX_VALUE; i++)
        sum += i;
    return sum;
}

  这个程序的结果是正确的,但由于写错了一个字符,运行的结果要比实际慢很多。

  变量 sum 被声明成了 Long 而不是 long ,这意味着程序构造了大约 2^31 个不必要的Long实例(每次往 Long 类型的 sum 变量中增加一个 long 类型的 i)。

  把 sum 变量的类型由 Long 改为 long 会使性能得到很大提升。这个教训很明显:优先使用基本类型而不是包装的基本类型,也要注意无意识的自动装箱。

  这个条目不应该被误解为暗示对象创建是昂贵的,应该避免创建对象。

  相反,创建和回收小的对象非常廉价,构造器只会做很少的工作,尤其在现代 JVM 实现上。 创建额外的对象以增强程序的清晰性,简单性或功能性通常是件好事。

  反之,通过维护自己的对象池来避免创建对象并不是一个好的做法,除非池中的对象是非常重量级的。正确使用对象池的典型对象示例是数据库连接池。

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

  Java语言,当你用完对象之后,他们会被自动回收。它很容易给你留下这样的印象,认为自己不再需要考虑内存管理的事情了,其实不然。

// Can you spot the "memory leak"?
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);
    }
}

  这个程序没有什么明显的错误,但有一个潜在的问题——“内存泄漏”。

  由于垃圾回收器的活动的增加,或内存占用的增加,程序的性能会下降。极端情况下导致程序失败(OutOfMemoryError)错误。

  程序中哪里发生了内存泄漏?

  如果一个栈先增长,后收缩,那么从栈弹出的对象不会被当作垃圾回收掉,即使使用栈的程序不再引用这些对象。

  这是因为栈内部维护了对这些对象的过期引用( obsolete references)。 过期引用简单来说就是永远不会再一次被解引用的引用。

  在上面这段代码中,数组“活动部分(active portion)”之外的任何引用都是过期的。活动部分是由索引下标小于 size 的那些元素组成的。

  这类问题的解决方法很简单:一旦对象引用过期,只需清空这些引用(将它们设置为 null)。

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

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

  常见的内存泄漏场景:

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

  2. 内存泄漏的另一个常见来源是缓存。一旦将对象引用放入缓存中,很容易忘记它的存在,并且在它变得无关紧要之后,仍然保留在缓存中。

    常见解决方案之一,用WeakHashMap代表缓存,当缓存中的项过期之后,它们就会被自动删除。

    常见解决方案之二,缓存应该时不时的清除掉没用的项。清除工作可以由后台线程(ScheduledThreadPoolExecutor)来完成。

  3. 监听器和其他回调。如果你实现了一个API——其客户端注册回调(callbacks),但是没有显式地撤销他们的注册。

    除非采取一些操作来处理,否则这些回调会积累。确保回调被垃圾回收的一种方法是只存储弱引用(weak references)。

    例如,仅将它们保存在 WeakHashMap 的键(key)中。

猜你喜欢

转载自www.cnblogs.com/fyql/p/11369712.html