0%

Effective Java 第 2 章:创建和销毁对象

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

  • 静态工厂方法的优点一:它们有名称

    • 如果构造器的参数本身没有确切地描述正被返回的对象,那么具有适当名称的静态工厂会更容易使用,产生的客户端代码也更易于阅读
    • 重载的类的构造器实际上并不是一个好主意,面对这样的 API,用户永远也记不住该用哪个构造器,结构常常会调用错误的构造器。并且在读到使用了这些构造器的代码时,如果没有参考类的文档,往往不知所云
    • 当一个类需要多个带有相同签名的构造器时,就用静态工厂方法代替构造器,并且仔细选择名称以便突出静态工厂方法之间的区别
  • 静态工厂方法的优点二:不必在每次调用它们的时候都创建一个新对象

    • 这使得不可变类(详见第 17 条)可以使用预先构建好的实例,或者将构建好的实例缓存起来,进行重复利用,从而避免创建不必要的对象。类似享元(Flyweight)模式,如果程序经常请求创建相同的对象,并且创建对象的代价很高,那么该技术可以极大提升性能
    • 静态工厂方法能够为重复的调用返回相同的对象,这样有助于类总能严格控制在某个时刻哪些实例应该存在,这种类被称作实例受控的类(instance-controlled)。实例受控使得类可以确保它是一个 Singleton (详见第 3 条)或者是不可实例化的(详见第 4 条)
    • 它还使得不可变的值类可以确保不会存在两个相等的实例,即当且仅当 a == b 时,a.equals(b) 才为 true。这是享元模式的基础,枚举类型(详见第 34 条)保证了这一点
  • 静态工厂方法的优点三:它们可以返回原返回类型的任何子类型的对象

    • 这种灵活性的一种应用是,API 可以返回对象,同时又不会使对象的类变成公有的,以这种方式隐藏实现类会使 API 变得非常简洁。这项技术适用于基于接口的框架(interface-based framework)(详见第 20 条),因为在这种框架中,接口为静态工厂方法提供了自然返回类型
    • 在 Java 8 之前,接口不能有静态方法,因此按照惯例,接口 Type 的静态工厂方法被放在一个名为 Types 的不可实例化的伴生类(详见第 4 条)中
    • 使用这种静态工厂方法时,甚至要求客户端通过接口来引用被返回的对象,而不是通过它的实现类来引用被返回的对象,这是一种良好的习惯(详见第 64 条)
    • Java 8 要求接口的所有静态成员都必须是公有的;Java 9 允许接口有私有的静态方法,但是静态域和静态成员类仍然需要是公有的
  • 静态工厂方法的优点四:所返回的对象的类可以随着每次调用而发生变化,这取决于静态工厂方法的参数值

    • 只要是已声明的返回类型的子类型,都是允许的。返回对象的类也可能随着发行版本的不同而不同。比如,EnumSet (详见第 36 条)没有共有的构造器,只有静态工厂方法
    • 举例,EnumSet 没有公有的构造器,只有静态工厂方法。在 OpenJDK 实现中,它们返回两种子类之一的一个实例,具体则取决于底层枚举类型的大小
  • 静态工厂方法的优点五:方法返回的对象所属的类,在编写包含静态工厂方法的类时可以不存在

    • 这种灵活的静态工厂方法构成了服务提供者框架(Service Provider Framework)的基础。服务提供者框架是指这样一个系统:多个服务提供者实现一个服务,系统为服务提供者的客户端提供多个实现,并把它们从多个实现中解耦出来
    • 服务提供者框架中有三个重要的组件:服务接口(Service Interface),这是提供者实现的;提供者注册 API(Provider Registration API),这是提供者用来注册实现的;服务访问 API(Service Access API),这是客户端用来获取服务的实例
    • 服务提供者框架的第四个组件服务提供者接口(Service Provider Interface)是可选的,它表示产生服务接口之实例的工厂对象。如果没有服务提供者接口,实现就通过反射方式进行实例化(详见第 65 条)
    • 服务提供者框架模式有着无数种变体。例如,服务访问 API 可以返回比提供者需要的更丰富的服务接口,这就是桥接(Bridge)模式依赖注入框架(详见第 5 条)可以被看作是一个强大的服务提供者;从 Java 6 版本开始,Java 平台就提供了一个通用的服务提供者框架 java.util.ServiceLoader(详见第 59 条)
  • 静态工厂方法的缺点一:类如果不含有公有的或者受保护的构造器,就不能被子类化

    • 例如,要想将 Collection Framework 中的任何便利的实现类子类化,这是不可能的
    • 这样也因祸得福,因为它鼓励我们使用复合(composition)而不是继承(详见第 18 条),这正是不可变类型所需要的(详见第 17 条)
  • 静态工厂方法的缺点二:程序员很难发现它们

    • 在 API 文档中,它们没有像构造器那样在 API 文档中明确标识出来

    • 同时,通过在类或者接口注释中关注静态工厂,并遵守标准的命名习惯,也可以弥补这一劣势

      静态工厂方法惯用名称 含义 举例
      from 类型转换方法,它只有单个参数,返回该类型的一个相对应的实例 Date d = Data.from(instant);
      of 聚合方法,带有多个参数,返回该类型的一个实例,把它们合并起来 Set<Rank> faceCards = EnumSet.of(JACK, QUEEN, KING);
      valueOf fromof 更繁琐的一种替代方法 BigInteger prime = BigInteger.valueOf(Integer.MAX_VALUE);
      instance 或者 getInstance 返回的实例是通过方法的(如有)参数来描述的,但是不能说与参数具有同样的值 StackWalker luke = StackWalker.getInstance(options);
      create 或者 newInstance instance 或者 getInstance 一样,但 create 或者 newInstance 能够确保每次调用都返回一个新的实例 Object newArray = Array.newInstance(classObject, arrayLen);
      getType getInstance 一样,但是在工厂方法处于不同的类中的时候使用。Type 表示工厂方法所返回的对象类型 FileStore fs = Files.getFileStore(path);
      newType newInstance 一样,但是在工厂方法处于不同的类中的时候使用。Type 表示工厂方法所返回的对象类型 BufferedReader br = Files.newBufferedReader(path);
      type getTypenewType 的简版 List<Complaint> litany = Collections.list(legacyLitany);

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

  • 重叠构造器(telescoping constructor)

    • 含义

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

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      19
      20
      21
      22
      23
      24
      25
      26
      27
      28
      29
      30
      31
      32
      33
      34
      35
      36
      37
      38
      39
      40
      package effectivejava.chapter2.item2.telescopingconstructor;

      // Telescoping constructor pattern - does not scale well! (Pages 10-11)
      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;
      }

      public static void main(String[] args) {
      NutritionFacts cocaCola = new NutritionFacts(240, 8, 100, 0, 35, 27);
      }
      }
    • 特点

      • 重叠构造器模式可行,但当有许多参数的时候,客户端代码会很难编写,并且仍然较难以阅读
      • 比如,如果客户端不小心颠倒了其中两个相同类型参数的顺序,不会编译报错但往往会有运行时报错(详见第 51 条)
  • JavaBeans 模式

    • 含义

      • 先调用一个无参构造器来创建对象
      • 然后再调用 setter 方法来设置每个必要的参数,以及每个相关的可选参数
    • Demo

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      19
      20
      21
      22
      23
      24
      25
      26
      27
      28
      29
      30
      31
      32
      33
      34
      35
      36
      37
      38
      39
      40
      41
      42
      43
      44
      45
      46
      47
      48
      package effectivejava.chapter2.item2.javabeans;

      // JavaBeans Pattern - allows inconsistency, mandates mutability (pages 11-12)
      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;
      }

      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);
      }
      }
    • 缺点

      • 因为构造过程被分到了几个调用中,在构造过程中 JavaBean 可能处于不一致的状态,调试起来很困难
      • JavaBeans 模式使得把类做成不可变的可能性不复存在(详见第 17 条),这就需要付出额外的努力来确保它的线程安全
  • 建造者(Builder)模式

    • 含义

      • 它不直接生成想要的对象,而是让客户端利用所有必要的参数调用构造器(或者静态工厂),得到一个 builder 对象
      • 然后客户端在 builder 对象上调用类似于 setter 的方法,来设置每个相关的可选参数
      • 最后,客户端调用无参的 build 方法来生成通常是不可变的对象。这个 builder 通常是它构建的类的静态成员类(详见第 24 条)
    • Demo

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      19
      20
      21
      22
      23
      24
      25
      26
      27
      28
      29
      30
      31
      32
      33
      34
      35
      36
      37
      38
      39
      40
      41
      42
      43
      44
      45
      46
      47
      48
      49
      50
      51
      52
      53
      54
      55
      56
      57
      58
      59
      60
      61
      62
      63
      64
      65
      package effectivejava.chapter2.item2.builder;

      // Builder Pattern (Page 13)
      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;
      }

      public static void main(String[] args) {
      NutritionFacts cocaCola = new NutritionFacts.Builder(240, 8).calories(100).sodium(35).carbohydrate(27).build();
      }
      }
    • 优点

      • 这是一个流式 API,这样的代码容易编写,更为重要的是容易阅读
      • Builder 模式模拟了具名的可选参数,就像 Python 和 Scala 编程语言中的一样
      • 如果想尽快检测到无效的参数,可以在 builder 的构造器和方法中检查参数的有效性
        • 查看不可变量,包括 build 方法调用的构造器中的多个参数
        • 为了确保这些不变量免受攻击,从 builder 复制完参数之后,要检查对象域(详见第 50 条)
        • 如果检查失败,就抛出 IllegalArgumentException 异常(详见第 72 条),其中的详细信息会说明哪些参数是无效的(详见第 75 条)
  • 适用于类层次结构的 Builder 模式

    • 含义

      • 使用平行层次结构的 builder 时,各自嵌套在相应的类中
      • 当类具有层次结构时,抽象类有抽象类的 builder,具体类有具体类的 builder
    • Demo

      • 抽象类 Pizza:位于类层次根部,表示各式各样的披萨

        1
        2
        3
        4
        5
        6
        7
        8
        9
        10
        11
        12
        13
        14
        15
        16
        17
        18
        19
        20
        21
        22
        23
        24
        25
        26
        27
        28
        29
        package effectivejava.chapter2.item2.hierarchicalbuilcer;

        import java.util.*;

        // Builder pattern for class hierarchies (Page 14)

        // Note that the underlying "simulated self-type" idiom allows for arbitrary fluid hierarchies, not just builders

        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) {
        topping = builder.topping.clone(); // See Item 50
        }
        }
        • Pizza.Builder 的类型是泛型(generic type),带有一个递归类型参数(recursive type parameter)(详见第 30 条)
        • 它和抽象的 self 方法一样,允许在子类中适当地进行方法链接,不需要类型转换。这个针对 Java 缺乏 self 类型的解决方案,被称作模拟的 self 类型(simulated self-type)
      • 子类 NyPizza:表示经典纽约风味的披萨,需要一个尺寸

        1
        2
        3
        4
        5
        6
        7
        8
        9
        10
        11
        12
        13
        14
        15
        16
        17
        18
        19
        20
        21
        22
        23
        24
        25
        26
        27
        28
        29
        30
        31
        32
        33
        34
        35
        36
        37
        package effectivejava.chapter2.item2.hierarchicalbuilder;

        import java.util.Objects;

        // Subclass with hierarchical builder (Page 15)
        public class NyPizza extends Pizza {
        public enum Size { SMALL, MEDIUM, LARGE }
        private final Size size;

        public static class Builder extends Pizza.Builder<Builder> {
        private fianl 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;
        }

        @Override
        public String toString() {
        return "New York Pizza with " + toppings;
        }
        }
      • 子类 Calzone:表示馅料内置的半月型披萨,需要指定酱汁应该内置还是外置

        1
        2
        3
        4
        5
        6
        7
        8
        9
        10
        11
        12
        13
        14
        15
        16
        17
        18
        19
        20
        21
        22
        23
        24
        25
        26
        27
        28
        29
        30
        31
        32
        33
        34
        35
        package effectivejave.chapter2.item2.hierarchicalbuilder;

        // Subclass with hierarchical builder (Page 15)
        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;
        }

        @Override
        public String toString() {
        return String.format("Calzone with %s and sauce on the %s", toppings, sauceInside ? "inside" : "outside");
        }
        }
      • 测试类 PizzaTest

        1
        2
        3
        4
        5
        6
        7
        8
        9
        10
        11
        12
        13
        14
        15
        package effectivejava.chapter2.item2.hierarchicalbuilder;

        import static effectivejava.chapter2.item2.hierarchicalbuilder.Pizza.Topping.*;
        import static effectivejava.chapter2.item2.hierarchicalbuilder.NyPizza.Size.*;

        // Using the hierarchical builder (Page 16)
        public class PizzaTest {
        public static void main(String[] args) {
        NyPizza pizza = new NyPizza.Builder(SMALL).addTopping(SAUSAGE).addTopping(ONION).build();
        Calzone calzoce = new Calzone.Builder().addTopping(HAM).sauceInside().build();

        System.out.println(pizza);
        System.out.println(calzone);
        }
        }
    • 特点

      • 子类方法声明返回父类中声明的返回类型的子类型,这被称作协变返回类型(covariant return type),它允许客户端无需类型转换就能使用这些构造器
      • 与构造器相比,builder 的略微优势在于,它可以有多个可变(varargs)参数。因为 builder 是利用单独的方法来设置每一个参数
      • 此外,构造器还可以将多此调用某一个方法传入的参数集中到一个域中,例如上面代码中调用了两次 addTopping 方法
      • Builder 模式十分灵活,可以利用单个 builder 构建多个对象。builder 的参数可以在调用 build 方法创建对象期间进行调整,也可以随着不同的对象而改变。builder 可以自动填充某些域,例如每次创建对象时自动增加序列号
      • Builder 模式的确也有它自身的不足。为了创建对象,必须先创建它的构建器。虽然创建构建器的开销在实践中可能不那么明显,但在某些十分注重性能的情况下,可能就成问题了
      • Builder 模式还比重叠构造器更加冗长,因此它只有在有很多参数的时候才使用,比如 4 个或者更多个参数。但是考虑到扩展性,通常最好一开始就使用构建器
      • 如果类的构造器或者静态工厂中具有多个参数,设计这种类时,Builder 模式就是一种不错的选择,特别是当大多数参数都是可选或者类型相同的时候。
      • 与使用重叠构造器相比,使用 Builder 模式的客户端代码将更易于阅读和编写,构建器也比 JavaBeans 更加安全

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

  • 概念

    • Singleton 是指仅仅被实例化一次的类。Singleton 通常被认为用来代表一个无状态的对象,如函数(详见第 24 条),或者那些本质上唯一的系统组件
    • 使类成为 Singleton 会使它的客户端测试变得十分困难,因为不可能给 Singleton 替换模拟实现,除非实现一个充当其类型的接口
  • 方法一:公有静态成员是个 final

    • Demo

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      package effectivejava.chapter2.item3.field;

      // Singleton with public final field (Page 15)
      public class Elvis {
      public static final Elvis INSTANCE = new Elvis();

      private Elvis() { }

      public void leaveTheBuilding() {
      System.out.println("Whoa baby, I'm outta here!");
      }

      // This code would normally appear outside the class!
      public static void main(String[] args) {
      Elvis elvis = Elvis.INSTANCE;
      elvis.leaveTheBuilding();
      }
      }
      • 由于缺少公有的或者受保护的构造器,所以保证了 Elvis 的全局唯一性:一旦 Elvis 类被实例化,将只会存在一个 Elvis 实例,不多也不少
      • 享有特权的客户端可以借助 AccessibleObject.setAccessible 方法,通过反射机制(详见第 65 条)调用私有构造器。如果需要抵御这种攻击,可以修改构造器,让它在被要求创建第二个实例的时候抛出异常
    • 优点

      • API 很清楚地表明了这个类是一个 Singleton:公有的静态域是 final 的,所以该域总是包含相同的对象引用
      • 更简单,优先考虑
  • 方法二:公有的成员是个静态工厂方法

    • Demo

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      19
      20
      21
      22
      package effectivejava.chapter2.item3.staticfactory;

      // Singleton with static factory (Page 17)
      public class Elvis {
      private static final Elvis INSTANCE = new Elvis();

      private Elvis() { }

      public static Elvis getInstance() {
      return INSTANCE;
      }

      public void leaveTheBuilding() {
      System.out.println("Whoa baby, I'm outta here!");
      }

      // This code would normally appear outside the class!
      public static void main(String[] args) {
      Elvis elvis = Elvis.getInstance();
      elvis.leaveTheBuilding();
      }
      }
    • 优点

      • 它提供了灵活性:在不改变其 API 的前提下,我们可以改变该类是否应该为 Singleton 的想法。工厂方法返回该类的唯一实例,但是,它很容易被修改,比如改成为每个调用该方法的线程返回一个唯一的实例
      • 如果应用程序需要,可以编写一个 泛型 Singleton 工厂(generic singleton factory)(详见第 30 条)
      • 可以通过方法引用(method reference)作为提供者,比如 Elvis::instance 就是一个 Supplier<Elvis>
    • 注意

      • 将方法一和方法二实现的 Singleton 类变成是可序列化的(Serializable)(详见第 12 章),仅仅在声明中加上 implements Serializable 是不够的

      • 为了维护并保证 Singleton,必须声明所有实例域都是 transient 的,并提供一个 readResolve 方法(详见第 89 条)。否则,每次反序列化一个序列化的实例时,都会创建一个新的实例

        1
        2
        3
        4
        5
        6
        // 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;
        }
  • 方法三:声明一个包含单个元素的枚举类型

    • Demo

      1
      2
      3
      4
      5
      6
      // Enum singleton - the preferred approach
      public enum Elvis {
      INSTANCE;

      public void leaveTheBuilding() { ... }
      }
    • 特点

      • 枚举单例方式在功能上与公有域相似,但更加简洁,无偿地提供了序列化机制,绝对防止多次实例化,即使是在面对复杂的序列化或者反射攻击的时候
      • 虽然这种方法还没有广泛采用,但是单元素的枚举类型经常成为实现 Singleton 的最佳方法
      • 如果 Singleton 必须扩展一个超类,而不是扩展 Enum 的时候,则不宜使用这个方法(虽然可以声明枚举去实现接口)

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

  • 概要

    • 通常,工具类(utility class)不希望被实例化,因为实例化对它没有任何意义
    • 在缺少显示构造器的情况下,编译器会自动提供一个共有的、无参的缺省构造器(default constructor)
    • 企图通过将类做成抽象类来强制该类不可被实例化时行不通的
    • 由于只有当类不包含显式的构造器时,编译器才会生成缺省的构造器,因此只要让这个类包含一个私有构造器,它就不能被实例化
  • Demo

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    package effectivejava.chapter2.item4;

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

    // Remainder omitted
    }
    • AssertionError 不是必需的,但是它可以避免不小心在类的内部调用构造器
    • 这种习惯用法也有副作用,它使得一个类不能被子类化

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

  • 背景

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

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

      1
      2
      3
      4
      5
      6
      7
      // 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) { ... }
      }
      1
      2
      3
      4
      5
      6
      7
      8
      // 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) { ... }
      }
  • 依赖注入

    • 场景:能够支持类的多个实例(在本例中是指 SpellChecker),每一个实例都能使用客户端指定的资源(在本例中是指词典)

    • 最简单的模式:当创建一个新的实例时,就将该资源传到构造器中。这是依赖注入(dependency injection)的一种形式:词典(dictionary)是拼写检查器的一个依赖(dependency),在创建拼写检查器时就将词典注入(injected)其中

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      // 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) { ... }
      }
    • 优点

      • 虽然这个拼写检查器的范例中只有一个资源(词典),但是依赖注入却适用于任意数量的资源,以及任意的依赖形式
      • 依赖注入的对象资源具有不可变性(详见第 17 条),因此多个客户端可以共享依赖对象(假设客户端们想要的是同一个底层资源)
      • 依赖注入也同样适用于构造器静态工厂(详见第 1 条)和构建器(详见第 2 条)
    • 变体

      • 依赖注入模式的另一种有用的变体是:将资源工厂(factory)传给构造器。工厂是可以被重复调用来创建类型实例的一个对象。这类工厂具体表现为工厂方法(Factory Method)模式

      • 在 Java 8 中增加的接口 Supplier<T>,最适合用于表示工厂。带有 Supplier<T>的方法,通常应该限制输入工厂的类型参数使用有限通配符类型(bounded wildcard type)(详见第 31 条),以便客户端能够传入一个工厂,来创建指定类型的任意子类型

        1
        2
        // 这是一个生产马赛克的方法,它利用客户端提供的工厂来生产每一片马赛克
        Mosaic create(Supplier<? extends Tile> tileFactory) { ... }
    • 总结

      • 不要用 Singleton 或静态工具类来实现依赖一个或多个底层资源的类,且该资源的行为会影响到该类的行为。也不要直接用这个类来创建这些资源
      • 应该将这些资源或者工厂传给构造器(或者静态工厂,或者构建器),通过它们来创建类。这个实践被称作依赖注入,它极大地提升了类的灵活性、可重用性和可测试性
      • 虽然依赖注入极大地提升了灵活性和可测试性,但它会导致大型项目凌乱不堪,因为它通常包含上千个依赖。一般通过依赖注入框架(dependency injection framework)可以解决这个问题,比如 Dagger[Dagger]、Guice[Guice] 或者 Spring[Spring]

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

  • 一般来说,最好能重用单个对象,而不是在每次需要的时候就创建一个功能相同的新对象。重用方式既快速,又流行。如果对象是不可变的(immutable)(详见第 17 条),它就始终可以被重用

    • 极端的反面例子:String s = new String("bikini");

      • 该语句每次被执行的时候都创建一个新的 String 实例,但是这些创建对象的动作全都是不必要的
      • 传递给 String 构造器的参数 "bikini" 本身就是一个 String 实例,功能方面等同于构造器创建的所有对象
      • 如果这种用法是在一个循环中,或者是在一个被频繁调用的方法中,就会创建出成千上万不必要的 String 实例
    • 改进后的版本:String s = "bikini";

      • 这个版本只用了一个 String 实例,而不是每次执行的时候都创建一个新的实例
      • 同时可以保证,对于所有在同一台虚拟机中运行的代码,只要它们包含相同的字符串字面常量,该对象就会被重用
  • 对于同时提供了静态工厂方法(static factory method)(详见第 1 条)和构造器的不可变类,通常优先使用静态工厂方法而不是构造器,以避免创建不必要的对象

    • 静态工厂方法 Boolean.valueOf(String) 几乎总是优先于构造器 Boolean(String)注意构造器 Boolean(String) 在 Java 9 中已经被废弃了
    • 构造器在每次被调用的时候都会创建一个新的对象,而静态工厂方法则从来不要求在这样做,实际上也不会这么做
    • 除了重用不可变的对象外,也可以重用那些已知不会被修改的可变对象
  • 有些对象创建的成本比其他对象高很多,如果重复地需要这类“昂贵的对象”,最好是缓存下来重用

    • 判断一个字符串是否为一个有效的罗马数字

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      19
      20
      21
      22
      23
      24
      25
      26
      27
      28
      29
      30
      31
      32
      33
      34
      35
      36
      37
      package effectivejava.chapter2.item6;
      import java.util.regex.Pattern;

      // Reusing expensive object for improved performance
      public class RomanNumerals {
      // Performance can be greatly improved!
      static boolean isRomanNumeralSlow(String s) {
      return s.matches("^(?=.)M*(C[MD]|D?C{0,3})(X[CL]|L?X{0,3})(I[XV]|V?I{0,3})$");
      }

      // Reusing expensive object for improved performance
      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 isRomanNumeralFast(String s) {
      return ROMAN.matcher(s).matches();
      }

      public static void main(String[] args) {
      int numSets = Integer.parseInt(args[0]);
      int numReps = Integer.parseInt(args[1]);
      boolean b = false;

      for(int i = 0; i < numSets; i++) {
      long start = System.nanoTime();
      for(int j = 0; j < numReps; j++) {
      b ^= isRomanNumeralSlow("MCMLXXVI"); // Change Slow to Fast to see performance difference
      }
      long end = System.nanotime();
      System.out.println((end - start) / (1_000. * numReps)) + "μs.");
      }

      // Prevents VM from optimizing away everything.
      if(!b) {
      System.out.println();
      }
      }
      }
    • SLOW 方法实现的问题在于它依赖 String.matches 方法。虽然 String.matches 方法最易于查看一个字符串是否与正则表达式相匹配,但并不适合在注重性能的情形中重复使用

      • 问题在于,它在内部为正则表达式创建了一个 Pattern 实例,却只用了一次,之后就可以进行垃圾回收了
      • 创建 Pattern 实例的成本很高,因为需要将正则表达式编译成一个有限状态机(finite state machine)
    • 为了提升性能,应该显示地将正则表达式编译成一个 Pattern 实例(不可变),让它成为类初始化的一部分,并将它缓存起来,每当调用 isRomanNumeral 方法的时候就重用同一个实例

      • 改进后的 isRomanNumeral 方法如果被频繁调用,会显示出明显的性能优势
      • 除了提高性能之外,代码也更清晰了,将不可见的 Pattern 实例做成 final 静态域时,可以给它起个名字,这样会比正则表达式本身更有可读性
    • 如果包含改进后的 isRomanNumeral 方法的类被初始化了,但是该方法没有被调用,那就没必要初始化 ROMAN

      • 通过在 isRomanNumeral 方法第一次被调用的时候延迟初始化(lazily initializing)(详见第 83 条)这个域,有可能消除这个不必要的初始化工作,但是不建议这样做
      • 正如延迟初始化中常见的情况一样,这样做会使方法的实现更加复杂,从而无法将性能显著提高到超过已经达到的水平(详见第 67 条)
    • 如果一个对象是不变的,那么它显然能够被安全地重用,但其他有些情形则并不总是这么明显。考虑适配器(adapter)的情形,有时也叫作视图(view)

      • 适配器是指这样一个对象:它把功能委托给一个后备对象(backing object),从而为后备对象提供一个可以替代的接口
      • 由于适配器除了后备对象之外,没有其他的状态信息,所以针对某个给定对象的特定适配器而言,它不需要创建多个适配器实例
      • 例如,Map 接口的 keySet 方法返回该 Map 对象的 Set 视图,其中包含该 Map 中所有的键(key)。对于一个给定的 Map 对象,实际上每次调用 keySet 都返回同样的 Set 实例
  • 自动装箱(autoboxing)

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

第 8 条:避免使用终结方法和清除方法

第 9 条:try-with-resources 优先于 try-finally

-------------------- 本文结束感谢您的阅读 --------------------