Effective-Java学习笔记(一)

第二章 创建和销毁对象

1. 考虑用静态工厂方法代替构造函数

静态工厂方法是一个返回该类实例的 public static 方法。例如,Boolean类的valueOf方法

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

要注意静态工厂方法与设计模式中的工厂方法不同。

静态工厂方法优点:

  1. 静态工厂方法有确切名字,客户端实例化对象时代码更易懂。例如,BigInteger类中返回可能为素数的BigInteger对象静态工厂方法叫BigInteger.probablePrime。此外,每个类的构造函数签名是唯一的,但是程序员可以通过调整参数类型、个数或顺序修改构造函数的签名,这样会给客户端实例化对象带来困惑,因为静态工厂方法有确切名称所以不会出现这个问题
  2. 静态工厂方法不需要在每次调用时创建新对象。例如Boolean.valueOf(boolean),true和false会返回预先创建好的对应Boolean对象。这种能力允许类在任何时候都能严格控制存在的实例,常用来实现单例
  3. 静态工厂方法可以获取任何子类的对象。这种能力的一个应用是API可以不公开子类或者实现类的情况下返回对象。例如Collections类提供静态工厂生成不是public的子类对象,不可修改集合和同步集合等
  4. 静态工厂方法返回对象的类型可以根据输入参数变换。例如,EnumSet类的noneOf方法,当enum的元素个数小于等于64,noneOf方法返回RegularEnumSet类型的对象,否则返回JumboEnumSet类型的对象
  5. 静态工厂方法的返回对象的类不需要存在。这种灵活的静态工厂方法构成了服务提供者框架的基础。service provider框架有三个必要的组件:代表实现的service interface;provider registration API,提供者用来注册实现;service access API,客户端使用它来获取服务的实例,服务访问API允许客户端选择不同实现,是一个灵活的静态工厂方法。service provider第四个可选的组件是service provider interface,它描述了产生service interface实例的工厂对象。在JDBC中,Connection扮演service interface角色,DriverManager.registerDriver是provider registration API,DriverManager.getConnection是service access API,Driver是service provider interface

静态工厂方法的缺点:

  1. 只提供静态工厂方法而没有public或者protected的构造方法就不能被继承,例如Collections类就不能被继承

  2. 静态工厂方法没有构造函数那么显眼,常见的静态工厂方法名字如下:
    from,一种类型转换方法,该方法接受单个参数并返回该类型的相应实例,例如:

    Date d = Date.from(instant);
    

    of,一个聚合方法,它接受多个参数并返回一个包含这些参数的实例,例如:

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

    valueOf,一种替代 from 和 of 但更冗长的方法

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

    instance 或 getInstance,返回一个实例,该实例由其参数(如果有的话)描述,但不具有相同的值,例如:

    StackWalker luke = StackWalker.getInstance(options);
    

    create 或 newInstance,与 instance 或 getInstance 类似,只是该方法保证每个调用都返回一个新实例,例如:

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

    getType,类似于 getInstance,但如果工厂方法位于不同的类中,则使用此方法。其类型是工厂方法返回的对象类型,例如:

    FileStore fs = Files.getFileStore(path);
    

    newType,与 newInstance 类似,但是如果工厂方法在不同的类中使用。类型是工厂方法返回的对象类型,例如:

    BufferedReader br = Files.newBufferedReader(path);`
    

    type,一个用来替代 getType 和 newType 的比较简单的方式,例如:

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

2. 当构造函数有多个参数时,考虑改用Builder

静态工厂和构造函数都有一个局限:不能对大量可选参数做很好扩展。例如,一个表示食品营养标签的类,必选字段有净含量、热量,另外有超过20个可选字段,比如反式脂肪、钠等。

为这种类编写构造函数,通常是使用可伸缩构造函数,这里展示四个可选字段的情况

// Telescoping constructor pattern - does not scale well!
public class NutritionFacts {
    private final int servingSize; // (mL) required
    private final int servings; // (per container) required
    private final int calories; // (per serving) optional
    private final int fat; // (g/serving) optional
    private final int sodium; // (mg/serving) optional
    private final int carbohydrate; // (g/serving) 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;
    }
}

当你想创建指定carbohydrate的实例,就必须调用最后一个构造函数,这样就必须给出calories、fat、sodium的值。当有很多可选参数时,这种模式可读性很差。

当遇到许多可选参数时,另一种选择是使用JavaBean模式,在这种模式中,调用一个无参数的构造函数来创建对象,然后调用setter方法来设置参数值

// JavaBeans Pattern - allows inconsistency, mandates mutability
public class NutritionFacts {
    // Parameters initialized to default values (if any)
    private int servingSize = -1; // Required; no default value
    private int servings = -1; // Required; no default value
    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; }
}

这种模式比可伸缩构造函数模式更易读,但有严重的缺点。因为对象的构建要调用多个set方法,所以对象可能会在构建过程中处于不一致状态(多线程下,其它线程使用了未构建完成的对象)

第三种选择是建造者模式,它结合了可伸缩构造函数模式的安全性和JavaBean模式的可读性。客户端不直接生成所需的对象,而是使用所有必需的参数调用构造函数或静态工厂方法生成一个builder对象。然后,客户端在builder对象上调用像set一样的方法来设置可选参数,最后调用build方法生成所需对象。Builder通常是它构建的类的静态成员类

// Builder Pattern
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 sodium = 0;
        private int carbohydrate = 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 sodium(int val) {
            sodium = val;
            return this;
        }

        public Builder carbohydrate(int val) {
            carbohydrate = 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类是不可变的。Builder的set方法返回builder对象本身,这样就可以链式调用。下面是客户端代码的样子:

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

为了简介,这里省略的参数校验。参数校验在Builder的构造函数和方法中。多参数校验在build方法中

建造者模式也适用于抽象类,抽象类有抽象Builder

import java.util.EnumSet;
import java.util.Objects;
import java.util.Set;

// Builder pattern for class hierarchies
public abstract class Pizza {
    public enum Topping {HAM, MUSHROOM, ONION, PEPPER, SAUSAGE}

    final Set<Topping> toppings;

    abstract static class Builder<T extends Builder<T>> {
        EnumSet<Topping> toppings = EnumSet.noneOf(Topping.class);

        public T addTopping(Topping topping) {
            toppings.add(Objects.requireNonNull(topping));
            return self();
        }

        abstract Pizza build();

        // Subclasses must override this method to return "this"
        protected abstract T self();
    }

    Pizza(Builder<?> builder) {
        toppings = builder.toppings.clone(); // See Item 50
    }
}
import java.util.Objects;

public class NyPizza extends Pizza {
    public enum Size {SMALL, MEDIUM, LARGE}

    private final Size size;

    public static class Builder extends Pizza.Builder<Builder> {
        private final Size size;

        public Builder(Size size) {
            this.size = Objects.requireNonNull(size);
        }

        @Override
        public NyPizza build() {
            return new NyPizza(this);
        }

        @Override
        protected Builder self() {
            return this;
        }
    }

    private NyPizza(Builder builder) {
        super(builder);
        size = builder.size;
    }
}

public class Calzone extends Pizza {
    private final boolean sauceInside;

    public static class Builder extends Pizza.Builder<Builder> {
        private boolean sauceInside = false; // Default

        public Builder sauceInside() {
            sauceInside = true;
            return this;
        }

        @Override
        public Calzone build() {
            return new Calzone(this);
        }

        @Override
        protected Builder self() {
            return this;
        }
    }

    private Calzone(Builder builder) {
        super(builder);
        sauceInside = builder.sauceInside;
    }
}

客户端实例化对象代码如下:

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

建造者模式非常灵活,一个builder对象可以反复构建多个对象,可以通过builder中的方法调用生成不同的对象。建造者模式的缺点就是生成一个对象前要先创建它的builder对象,性能会稍差一些,但为了未来字段更好扩展,建议还是用建造者模式

3. 使用私有构造函数或枚举类型创建单例

单例是只实例化一次的类。当类不保存状态或状态都一致,那么它的对象本质上都是一样的,可以用单例模式创建

实现单例有两种方法。两者都基于私有化构造函数和对外提供 public static 成员,在第一个方法中,该成员是个用final修饰的字段

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

私有构造函数只调用一次,用于初始化public static final 修饰的Elvis类型字段INSTANCE。一旦初始化Elvis类,就只会存在一个Elvis实例。客户端不能再创建别的实例,但是要注意的是拥有特殊权限的客户端可以利用反射调用私有构造函数生成实例

Constructor<?>[] constructors = Elvis.class.getDeclaredConstructors();
AccessibleObject.setAccessible(constructors, true);

Arrays.stream(constructors).forEach(name -> {
    if (name.toString().contains("Elvis")) {
        Elvis instance = (Elvis) name.newInstance();
        instance.leaveTheBuilding();
    }
});

第二种方法,对外提供的 public static 成员是个静态工厂方法

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

所有对getInsance()方法的调用都返回相同的对象,但是同样可以通过反射调用私有构造函数创建对象。
这两种方法要实现可序列化,仅仅在声明中添加implements Serializable是不够的。还要声明所有实例字段未transient,并提供readResolve方法,否则,每次反序列化都会创建一个新实例。JVM在反序列化时会自动调用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() { ... }
}

这种方法类似于 public 字段方法,但是它更简洁,默认提供了序列化机制,提供了对多个实例化的严格保证,即使面对复杂的序列化或反射攻击也是如此。这种方法可能有点不自然,但是单元素枚举类型通常是实现单例的最佳方法。但是,如果你的单例要继承父类,那么就不能用这种方法

4. 用私有构造函数实施不可实例化

工具类不需要实例化,它里面的方法都是public static的,可以通过私有化构造函数使类不可实例化

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

AssertionError不是必须有的,但可以防止构造函数被意外调用(反射)

这种用法也防止了类被继承。因为所有子类构造函数都必须显示或者隐式调用父类构造函数,但父类构造函数私有化后就无法调用

5. 依赖注入优于硬连接资源

有些类依赖于一个或多个资源。例如拼写检查程序依赖于字典。错误实现如下:

// Inappropriate use of static utility - inflexible & untestable!
public class SpellChecker {
    private static final Lexicon dictionary = ...;
    private SpellChecker() {} // Noninstantiable
    public static boolean isValid(String word) { ... }
    public static List<String> suggestions(String typo) { ... }
}
// Inappropriate use of singleton - inflexible & untestable!
public class SpellChecker {
    private final Lexicon dictionary = ...;
    private SpellChecker(...) {}
    public static INSTANCE = new SpellChecker(...);
    public boolean isValid(String word) { ... }
    public List<String> suggestions(String typo) { ... }
}

这两种写法分别是工具类和单例,都假定使用同一个字典,实际情况是不同拼写检查程序依赖不同的字典。

你可能会想取消dictionary的final修饰,并让SpellChecker类添加更改dictionary的方法。但这种方法在并发环境下会出错。工具类和单例不适用于需要参数化依赖对象

参数化依赖对象的一种简单模式是,创建对象时将被依赖的对象传递给构造函数。这是依赖注入的一种形式。

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

依赖注入适用于构造函数、静态工厂方法、建造者模式。

依赖注入的一个有用变体是将工厂传递给构造函数,这样可以反复创建被依赖的对象。Java8中引入的Supplier<T>非常适合作为工厂。下面是一个生产瓷砖的方法

Mosaic create(Supplier<? extends Tile> tileFactory) { ... }

尽管依赖注入极大提高了灵活性和可测试性,但它可能会使大型项目变得混乱。通过使用依赖注入框架(如Spring、Dagger、Guice)可以消除这种混乱

6. 避免创建不必要的对象

复用对象可以加快程序运行速度,如果对象是不可变的,那么它总是可以被复用的。

一个不复用对象的极端例子如下

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

该语句每次执行都会创建一个新的String实例。"bikini"本身就是一个String实例,改进的代码如下

String s = "bikini";

这个版本使用单个String实例,而不是每次执行时都会创建一个新的实例。此外,可以保证在同一虚拟机中运行的其它代码都可以复用该对象,只要它们都包含相同的字符串字面量

通常可以使用静态工厂方法来避免创建不必要的对象。例如,Boolean.valueOf(String) 比构造函数Boolean(String) 更可取,后者在Java9中被废弃了。

有些对象的创建代价很高,如果要重复使用,最好做缓存。例如,使用正则表达式确定字符串是否为有效的罗马数字

// 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实例代价很大,因为它需要将正则表达式编译成有限的状态机

为了提高性能,将正则表达式显示编译成Pattern实例,作为类初始化的一部分,每次调用匹配的方法时都使用这个实例

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

当对象是不可变的时候,我们很容易会想到复用。但是有些情况不那么容易想到复用。例如适配器模式中的适配器,因为适配器中没有它适配对象的状态,所以不需要创建多个适配器。

一个具体的例子是,KeySet是Map中的适配器,使得Map不仅能提供返回键值对的方法,也能返回所有键的Set,KeySet不需要重复创建,对Map的修改会同步到KeySet实例

7. 排除过时的对象引用

Java具体垃圾回收机制,会让程序员的工作轻松很多,但是并不意味着需要考虑内存管理,考虑以下简单的堆栈实现

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

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

这段代码有一个潜在的内存泄露问题。当栈的长度增加,再收缩时,从栈中pop的对象不会被回收,因为引用仍然还在elements中。解决方法很简单,一旦pop就置空

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

用null处理过时引用的另一个好处是,如果被意外引用的话会立刻抛NullPointerException

另外一个常见的内存泄漏是缓存。HashMap的key是对实际对象的强引用,不会被GC回收。WeakHashMap的key如果只有WeakHashMap本身使用,外部没有使用,那么会被GC回收

内存泄露第三种常见来源是监听器和其它回调。如果客户端注册了回调但是没有显式地取消它们,它们就会一直在内存中。确保回调被及时回收的一种方法是仅存储它们的弱引用,例如,将他它们作为键存储在WeakHashMap中

8. 避免使用终结器和清除器

如标题所说

9. 使用 try-with-resources 优于 try-finally

Java库中有许多必须通过调用close方法手动关闭的资源,比如InputStream、OutputStream和java.sql.Connection。

从历史上看,try-finally语句是确保正确关闭资源的最佳方法,即便出现异常或返回

// try-finally - No longer the best way to close resources!
static String firstLineOfFile(String path) throws IOException {
    BufferedReader br = new BufferedReader(new FileReader(path));
    try {
        return br.readLine();
    } finally {
        br.close();
    }
}

这种方式在资源变多时就很糟糕

// try-finally is ugly when used with more than one resource!
static void copy(String src, String dst) throws IOException {
    InputStream in = new FileInputStream(src);
    try {
        OutputStream out = new FileOutputStream(dst);
    try {
        byte[] buf = new byte[BUFFER_SIZE];
        int n;
        while ((n = in.read(buf)) >= 0)
            out.write(buf, 0, n);
    } finally {
        out.close();
        }
    }
    finally {
        in.close();
    }
}

使用 try-finally 语句关闭资源的正确代码(如前两个代码示例所示)也有一个细微的缺陷。try 块和 finally 块中的代码都能够抛出异常。例如,在 firstLineOfFile 方法中,由于底层物理设备发生故障,对 readLine 的调用可能会抛出异常,而关闭的调用也可能出于同样的原因而失败。在这种情况下,第二个异常将完全覆盖第一个异常。异常堆栈跟踪中没有第一个异常的记录,这可能会使实际系统中的调试变得非常复杂(而这可能是希望出现的第一个异常,以便诊断问题)

Java7 引入 try-with-resources语句解决了这个问题。要使用这个结构,资源必须实现AutoCloseable接口,这个接口只有一个void close方法,下面是前两个例子的try-with-resources形式

// try-with-resources - the the best way to close resources!
static String firstLineOfFile(String path) throws IOException {
    try (BufferedReader br = new BufferedReader(new FileReader(path))) {
        return br.readLine();
    }
}
// try-with-resources on multiple resources - short and sweet
static void copy(String src, String dst) throws IOException {
    try (InputStream in = new FileInputStream(src);OutputStream out = new FileOutputStream(dst)) {
        byte[] buf = new byte[BUFFER_SIZE];
        int n;
        while ((n = in.read(buf)) >= 0)
            out.write(buf, 0, n);
    }
}

try-with-resources为开发者提供了更好的异常排查方式。考虑firstLineOfFile方法,如果异常由readLine和不可见close抛出,那么后者异常会被抑制。被抑制的异常不会被抛弃,它们会被打印在堆栈中并标记被抑制。可以通过getSuppressed方法访问它们,该方法是Java7中添加到Throwable中的

像try-catch-finally一样,try-with-resources也可以写catch语句。下面是firstLineOfFile方法的一个版本,它不抛出异常,但如果无法打开文件或从中读取文件,会返回一个默认值

// try-with-resources with a catch clause
static String firstLineOfFile(String path, String defaultVal) {
    try (BufferedReader br = new BufferedReader(new FileReader(path))) {
        return br.readLine();
    } catch (IOException e) {
        return defaultVal;
    }
}