Effevtive-Java3rd-Edition-第一章

51次阅读

共计 6478 个字符,预计需要花费 17 分钟才能阅读完成。

非完整翻译,仅是阅读英文原版时的笔记,如对你有所帮助,不甚荣幸,建议阅读英文原版,生僻词汇相对较少,相对阅读翻译版本,也能减少一些误读

1. 考虑用静态工厂方法而不是构造器

  1. 不同于设计模式的工厂方法
  2. 有命名,表达更清楚
  3. 可使用单例,类似享元模式,节省创建对象的开销,例:Boolean.valueOf(boolean)
  4. 可返回原类型的任意子类型

    • Java 8 前,用 Companion(同伴;朋友;指南;手册) Class,例:Collections
    • Java 8 后,用接口
  5. 可以根据调用时传入的不同参数而返回不同类的对象,例:EnumSet
  6. 编写该方法时,返回类型不需存在

    1. Service Provider Framework,e.g. JDBC

      1. Service Interface,e.g. Connection
      2. Provider Registration API,e.g. DriverManager.registerDriver
      3. Service Access API,e.g. DriverManager.getConnection
      4. Service Provider Interface(Optional),e.g. Driver
    2. 很多 Service Provider Framework 的变种,Service Access API 可以返回一个比 Service Provider 提供的更丰富的服务接口

      1. 桥接模式,Service Access API 可以 Client 返回一个比 Service Provider 提供的更丰富的服务接口
      2. IoC 框架,可看做强大的 Service Provider
      3. Java 6 开始,提供了一个通用的服务提供框架,ServiceLoader
  7. 限制

    1. 没有 public 或者 protected 构造方法的类不能子类化, 因此鼓励使用组合,而非继承
    2. 难以找到 static factory methods
  8. 常用方法

    1. from,通过传入单个参数,例:Date.from(instant);
    2. of,传入多个参数,例:EnumSet.of(JACK, QUEEN, KING);
    3. valueOf,from 和 of 的替代方案,BigInteger.valueOf(Integer.MAX_VALUE);
    4. getInstance,返回一个由参数描述的实例,StackWalker.getInstance(options);
    5. getType,类似于 getInstance,但一般在工厂方法包含在不同类的情况下使用,Files.getFileStore(path);
    6. newType,Files.newBufferedReader(path);
    7. type,Collections.list(legacyLitany);

2. 用 Builder 替代多可选参数构造器

  1. 可伸缩构造器(telescoping constructor),可行,但对于 client code 难编写,难阅读
  2. JavaBeans 模式,无参构造器创建,setter 方法设置参数,一个 JavaBean 在其构造过程中可能处于不一致的状态,类无法仅仅通过检查构造器参数的有效性来保证一致性
  3. Builder 模式模仿了Python 和 Scala 中的具名可选参数
  4. Builder 模式很好适配了继承类,抽象类和具体类都可以有自己的 Builder
  5. 要考虑创建一个对象的 Builder 带来的 性能损失

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

3. 使用私有构造器或者枚举类型来强化 Singleton 属性

  1. Singleton 通常用来表示一个无状态对象
  2. 以下方式两种好处:

    1. 清晰的意识到类为 Singleton,final 不可变,保持相同对象引用
    2. 简单

      // 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() { ...} 
      }
  3. 对于采用上述任一一种方式创建的 Singleton 类,若要使其可序列化

    1. 仅仅在类的声明里让其实现 Serializable 接口是不够的。
    2. 为了保证 Singleton,应当将所有的实例属性声明为瞬时的(transient),并提供一个 readResolve 方法(条目 89)。
    3. 否则,每次反序列化一个已被序列化的实例时,将产生一个新的实例,在刚刚的例子中,就会产生一个假冒的 Evlis 实例。
  4. Singleton 的方式是声明一个包含单个元素的枚举

    1. 类似公有属性模式,更简洁
    2. 免费提供了序列化机制
    3. 单元素枚举通常是实现 Singleton 的最佳方式
    4. 如果我们的 Singleton 必须扩展自一个超类而不是枚举时,这种方式就不能使用了

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

  1. 工具类被设计为不可实例化
  2. 通过做成抽象类以强制不可实例化是 行不通

    1. 因抽象类能被继承且子类可实例化
    2. 产生误导
  3. 因此用 private constructor

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

5. 优先使用依赖注入而不是硬连接资源

  1. 静态工具类和 Singleton 对于类行为需要被底层资源参数化的场景是不适用的。
  2. 使用方法

    1. 工厂方法模式
    2. Supplier<T> 有界通配符类型
  3. 提高灵活性和测试性,可重用性
  4. DI 带来的复杂性可以被使用 DI 框架来抵消

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

  1. 避免不停创建新的字符串
  2. 尽量用静态工厂方法替代构造函数,e.g Boolean.valueOf(String)
  3. 所需的正则表达式显式地编译进一个不可变的 Pattern 对象里
  4. 避免自动装箱,拆箱,尽量使用基本类型
  5. 由于建立数据库连接的代价是很高的,所以复用这些连接对象就显得很有意义了
  6. 线程池的使用
  7. 避免对轻量级的对象使用对象池,除非对象创建代价很高

7. 消除过时的对象引用

  1. 这段代码并没有明显的错误。无论你如何测试它,它都能通过每一次测试,但这里面潜伏着一个问题。
  2. 不严格地讲,这段程序里面有个“内存泄露”的问题,随着垃圾回收活动的增加或者内存占用的增多,将会逐渐显现出性能下降的现象。
  3. 在极端的情况下,这种内存泄露会引发磁盘分页 (disk paging),甚至还会导致程序失败并抛出OutOfMemoryError 错误,但这种错误并不多见
  4. 所以代码中内存泄露的部分在哪里?

    1. 如果一个栈先增长后收缩,那些被弹出栈的对象将不会被回收,即使使用栈的程序不再引用它们。
    2. 这是因为栈里面包含着这些对象的过期引用(obsolete reference)。过期引用是指永远不会被再次解除的引用。
    3. 在这个例子当中,任何在 elements 数组的活动区域(active portion)之外的引用都是过期的。活动部分由 elements 数组里面下标小于数据长度的元素组成。非完整翻译,仅是阅读英文原版时的笔记,如对你有所帮助,不甚荣幸,建议阅读英文原版,生僻词汇相对较少,相对阅读翻译版本,也能减少一些误读
// 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);
    }
}
  1. pop 的正确方法

    public Object pop() {if (size == 0)
            throw new EmptyStackException();
        Object result = elements[--size];
        elements[size] = null; // Eliminate obsolete reference
        return result;
    }
  2. 清空引用的一个额外好处是,假如它们后来又被误解除引用,程序将会立即抛出 NullPointerException 异常,而不是默默地错误下去。这么做通常也有利于快速地查出错误
  3. 清空对象应该是例外而不是规范行为。消除过期引用最佳的方式是让包含这个引用的变量离开作用域。如果我们将每个变量定义在最小的作用域里,那么这种机制就会自然生效
  4. 每当一个类自己管理它的内存时,程序员就要小心内存泄露的问题了
  5. 缓存是内存泄露的另一个常见来源 ,只有当所要的缓存项的生命周期是由该键的外部引用而不是值来决定时,WeakHashMap 才有用
  6. 第三种常见的缓存泄漏的来源是监听器和其它调用,一种可以保证回调被及时垃圾回收的方式是,只保留对它们的弱引用

8. 避免使用终结方法和清理方法

  1. 终结方法(Finalizer)是不可预知的,很多时候是危险的,而且一般情况下是不必要的,Java 9 中,终结方法已经被遗弃,替代的方法为 Cleanners,相对安全点,同样不可预知,运行慢的,一般情况不必要
  2. 终结方法和清理方法的一个缺点是无法保证它们及时地被执行 [JLS, 12.6]。一个对象从变得不可到达开始到它的终结方法和清理方法被执行,中间可能会经过任意长的时间。这意味着,我们 不应该在终结方法和清理方法中做对时间有严格要求的任务
  3. Java 语言规范不仅不保证终结方法或清理方法会被及时运行,而且不保证它们最终会运行
  4. 依赖于终结方法或清理方法来释放共享资源(比如数据库)的永久锁,将很容易使得整个分布式系统停止运行
  5. 不要被 System.gcSystem.runFinalization两个方法给诱惑了。

    • 这两个方法也许会增加终结方法和清理方法被执行的概率,但也并不能保证一定会执行。
    • 有两个方法可以保证一旦被调用就执行终结方法和清理方法:

      1. System.runFinalizersOnExit和它臭名昭著的孪生兄弟,Runtime.runFinalizersOnExit
      2. 这两个方法都有致命的缺陷而且很久前就已经废弃了
  6. 终结方法的另一个问题是在终结过程中若有未被捕获的异常抛出,则抛出的异常会被忽略,而且该对象的终结过程也会终止
  7. 使用终结方法和清理方法还会导致严重的性能损失
  8. Finalizer 还会导致严重的安全问题,可能会引来 finalizer 攻击
  9. 所以,对于封装了需要终止使用的资源(比如文件或者线程),我们应该怎么做才能不用编写终止方法或者清理方法呢?我们只需 让类继承 AutoCloseable 接口 即可,并要求使用这个类的客户端在每个类实例都不再需要时就调用 close 方法,一般都是运用 try-with-resources 来保证资源的终止使用
  10. Finallizer 的合理用法:

    1. 如果一个资源的持有者没有调用 close 方法,Finalizer 可以作为一个安全保障(safety-net)
    2. cleaner 关注有 native 内存对象 (native peers) 关联的对象,native peers 无法被 GC 回收

9. 优先使用 try-with-resources 而不是 try-finally

  1. 当 Java 7 引入 try-with-resources 语句时,所有问题突然一下子解决了。若要使用这个语句,一个资源必须实现 AutoCloseable 接口

    // 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); 
        }
    }
  2. 比起 try-finally,try-with-resources 语句不仅更简短和更可读,而且它们更容易排查问题。

    • 考虑 firstLineOfFile 方法的情况,如果从 readLine 方法和 close(不可见)方法都抛出异常,那么后者抛出的异常将被抑制而不是前者。
    • 事实上,为了保留我们实际想看的异常,多个异常都可能会被抑制。这些被抑制的异常并不仅仅是被忽略了,它们被打印在错误栈当中,并被标注为被抑制了。
  3. 结论很明显:面对必须要关闭的资源,我们总是应该优先使用 try-with-resources 而不是 try-finally。随之产生的代码更简短,更清晰,产生的异常对我们也更有用。try-with-resources 语句让我们更容易编写必须要关闭的资源的代码,若采用 try-finally 则几乎做不到这点。

正文完
 0