Effective Java英文第三版读书笔记(3) -- 函数最佳实践

写在前面

《Effective Java》原书国内的翻译只出版到第二版,书籍的编写日期距今已有十年之久。这期间,Java已经更新换代好几次,有些实践经验已经不再适用。去年底,作者结合Java7、8、9的最新特性,编著了第三版(参考https://blog.csdn.net/u014717036/article/details/80588806)。当前只有英文版本,可以在互联网搜索到PDF原书。本读书笔记都是基于原书的理解。


以下是正文部分

函数最佳实践(Methods)

本章包括:

  • 实践49 校验参数的有效性(Check parameters for validity)
  • 实践50 必要时,使用保护性拷贝(Make defensive copies when needed)
  • 实践51 仔细设计函数签名(Design method signatures carefully)
  • 实践52 审慎地使用重载(Use overloading judiciously)
  • 实践53 审慎地使用varargs(Use varargs judiciously)
  • 实践54 返回空集合而不是null(Return empty collections or arrays, not nulls)
  • 实践55 审慎地使用 Optional 作为返回(Return optionals judiciously)
  • 实践56 为所有外部暴露API编写注释文档(Write doc comments for all exposed APIelements)

实践49 校验参数的有效性(Check parameters for validity)

在编程时,对于函数入参可能会有一些限制,例如参数作为数组下标不能为负数;参数作为对象引用不能为null。最好的实践是注释说明参数限制并在函数入口进行参数检查,越早发现问题越好。
对于声明为private的函数,由于开发者有完全的控制权和访问与其,在内部使用assert来校验即可。注意,只有在启动java程序时,指定了-ea或者-enableassertions参数,assert语句才回发挥实际用途,在校验失败时抛出AssertionError。对于声明为publicprotected的方法使用Javadoc的@throws注释关键词来注明如果函数调用违背了规则,将抛出的异常。通常,异常的类型为IllegalArgumentException, IndexOutOfBoundsException,, NullPointerException。示例如下:

// Private helper function for a recursive sort
private static void sort(long a[], int offset, int length) {
  assert a != null;
  assert offset >= 0 && offset <= a.length;
  assert length >= 0 && length <= a.length - offset;
}

/**
 * * Returns a BigInteger whose value is (this mod m). This method * differs from the remainder
 * method in that it always returns a * non-negative BigInteger. *
 *
 * @param m the modulus, which must be positive
 * @return this mod m
 * @throws ArithmeticException if m is less than or equal to 0
 */
public BigInteger mod(BigInteger m) {
  if (m.signum() <= 0) throw new ArithmeticException("Modulus <= 0: " + m);
}

注意上述代码并没有注明NullPointerException,这是因为BigInteger类已经内置了这个问题,如果mnullm.signum()将抛出NullPointerException。从Java7开始,使用Objects.requireNonNull方法可以方便地进行空指针检查。Java9进一步在Object引入了下标检查checkFromIndexSizecheckFromToIndexcheckIndex

this.strategy = Objects.requireNonNull(strategy, "strategy");

如果检查特别耗时,或者实现上不可行又或者在相关点已经有了检查,这几种情况下可以省略参数校验。

实践50 必要时,使用保护性拷贝(Make defensive copies when needed)

// Broken "immutable" time period class
final class Period {
  private final Date start;
  private final Date end;
  /**
   * @param start the beginning of the period
   * @param end the end of the period; must not precede start
   * @throws IllegalArgumentException if start is after end
   * @throws NullPointerException if start or end is null
   */
  public Period(Date start, Date end) {
    if (start.compareTo(end) > 0) throw new IllegalArgumentException(start + " after " + end);
    this.start = start;
    this.end = end;
  }

  public Date start() {
    return start;
  }

  public Date end() {
    return end;
  }
}

构造一个合理的时间段,起始时间保证早于结束时间,初看该函数是没问题的。但是注意由于Java在传递对象时,使用的是引用拷贝,作为参数Date在外部的修改会影响类内的引用值。典型的攻击手法如下:

Date start = new Date(); 
Date end = new Date(); 
Period p = new Period(start, end); 
end.setYear(78); // Modifies internals of p!

对于这个问题,在Java8中可以使用Instant或者ZonedDateTime类替换Date。推而广之,我们可能在某些API确实需要传入可变对象,但在类内部希望其值不可再修改。这是,就需要用到保护性性拷贝(defensive copy)技术。上面的缺陷类可以这么修改,注意,这两句拷贝必须放在函数最开始。为了防止操作时对象在其他线程被修改,拷贝完,再做参数校验等其他操作。

this.start = new Date(start.getTime()); 
this.end = new Date(end.getTime());

代码中用的是最原始的构造函数来建立新的对象而不是clone方法。这是因为clone方法不一定返回java.util.Date原始对象,它如果在子类中被重写,可能在拷贝过程中进行某些污染操作,并返回java.util.Date的子类对象。
解决了第一种修改"不变量"的攻击,还有第二种等着我们...

// Second attack on the internals of a Period instance
Date start = new Date(); 
Date end = new Date(); 
Period p = new Period(start, end); 
p.end().setYear(78); // Modifies internals of p!

针对这种操作,我们需要对函数返回的不希望被修改的内部可变对象作保护性拷贝

public Date start() { return new Date(start.getTime()); }
public Date end() { return new Date(end.getTime()); }

实践51 仔细设计函数签名(Design method signatures carefully)

首先,函数名要符合编程规范,并且在一个包内部保持风格一致。不要使用过长的函数名,但同时也要保持它的可阅读性。
不要为了使用方便而抽象出过多的函数。函数太多会增加学习、使用、文档、测试及维护的负担。
避免过长的函数参数列表,尤其是很多参数的类型都相同的时候,容易误用。实践中,建议参数个数尽量少于5个。下面列出三种解决参数列表过长问题的方法

  • 把函数重构成多个函数,每个函数只需要传入部分参数进行处理
  • 将多个参数视为一个对象,创建辅助静态成员类
  • 使用Builder模式进行对象创建及函数调用,例如在类的Builder模式最后是build(),在函数的Builder模式最后是execute()。这种方法在有可选参数的场景尤其适用。

对于参数类型

  1. 如果对象是某个接口的实现,则最好用接口类型而不是类类型。例如用 Map 而不是 HashMap
  2. 除非意义明确,使用Enum代替Boolean,增加代码可读性。

实践52 审慎地使用重载(Use overloading judiciously)

下面的程序对classify函数进行了重载,期望能判断集合的类型。从功能角度,程序设计者的一个预期输入应当是 "Set List Unknown Collection" 。可实际上,运行这段程序的输出是 "Unknown Collection Unknown Collection Unknown Collection" 。

// Broken! - What does this program print?
class CollectionClassifier {
  public static String classify(Set s) { return "Set"; }

  public static String classify(List lst) { return "List";  }

  public static String classify(Collection c) {
    return "Unknown Collection";
  }

  public static void main(String[] args) {
    Collection[] collections = {
      new HashSet(), 
      new ArrayList(),
      new HashMap().values()
    };
    for (Collection c : collections) 
      System.out.println(classify(c));
  }
}

重载函数的调用是在编译时(compile time)决定的,也就是说对于上述程序,运行时(runtime)调用classify只有一个选择——classify(Collection)

  • 重载函数的选择是静态的,编译时确定
  • 重写函数的选择是动态的,运行时确定

classify可以修改如下:

public static String classify(Collection c) {
  return c instanceof Set ? "Set" : c instanceof List ? "List" : "Unknown Collection";
}

对于重载,一个安全、保守的建议是:重载函数的参数个数不能相同(如果函数有varargs,那么久不要进行重载)。Java标准库的ObjectOutputStream给出了很好的示例,对于写操作,它没有去重载write,而是针对不同的参数类型,给出了writeBoolean(boolean),writeInt(int),writeLong(long)等。稍微放宽一点,至少保证参数类型完全不同且无法相互转换。例如ArrayList的两种构造方法,一个是参数为int,一个是参数为Collection。这种情况下,使用时基本不会搞混。

另一个误用的示例如下:

class SetList {
  public static void main(String[] args) {
    Set set = new TreeSet<>();
    List list = new ArrayList<>();
    for (int i = -3; i < 3; i++) {
      set.add(i);
      list.add(i);
    }
    for (int i = 0; i < 3; i++) {
      set.remove(i);
      list.remove(i);
    }
    System.out.println(set + " " + list);
  }
}

执行main函数将输出[-3, -2, -1] [-2, 0, 2]

实践53 审慎地使用varargs(Use varargs judiciously)

从原理上讲,varargs 会根据入参个数,创建一个相同大小的数组,把所有参数放入数组中,然后再传递到方法里面。因此 varargs 可以直接当数组使用,比较方便,但是同时数组的空间分配和初始化又会引入额外的系统开销,不适用与高性能场景。

static int sum(int... args) {
  int sum = 0;
  for (int arg : args) sum += arg;
  return sum;
}

关于性能问题,一个可能的替代方式是,对于常用场景不使用 varargs。例如 95% 的情况下,参数不超过3个,那么你应该使用重载。

public void foo() {}
public void foo(int a1) {}
public void foo(int a1, int a2) {}
public void foo(int a1, int a2, int a3) {}
public void foo(int a1, int a2, int a3, int... rest) {}

实践54 返回空集合而不是null(Return empty collections or arrays, not nulls)

我们常常看到这样的代码:

/**
 * * @return a list containing all of the cheeses in the shop, * or null if no cheeses are
 * available for purchase.
 */
public List getCheeses() {
  return cheesesInStock.isEmpty() ? null : new ArrayList<>(cheesesInStock);
}

这种返回 null 的做法,是的“空”这个场景被特殊化。所有外部调用点,都需要针对这种空场景进行处理。一旦外部处理不当,可能会在运行时导致程序出错。因此,此时应当返回一个空的集合,而不是 null。返回空集合并不增加多少实际的系统开销。

List cheeses = shop.getCheeses(); 
if (cheeses != null && cheeses.contains(Cheese.STILTON))

推荐的方式如下:

//The right way to return a possibly empty collection public 
List getCheeses() {
   return new ArrayList<>(cheesesInStock);
}

//The right way to return a possibly empty array public 
Cheese[] getCheeses() {
    return cheesesInStock.toArray(new Cheese[0]); 
}

如果实在担心性能问题,可以用 static 空集合。Java 标准库中已经集成了 Collections.emptyList, Collections.emptySet, Collections.emptyMap 几种。

// Optimization - avoids allocating empty collections
public List getCheeses() { 
    return cheesesInStock.isEmpty() ? Collections.emptyList() : new ArrayList<>(cheesesInStock); 
}

// Optimization - avoids allocating empty arrays 
private static final Cheese[] EMPTY_CHEESE_ARRAY = new Cheese[0];
public Cheese[] getCheeses() { 
    return cheesesInStock.toArray(EMPTY_CHEESE_ARRAY); 
}

实践55 审慎地 Optional 作为返回(Return optionals judiciously)

在Java8中,Optional 是一种不可变的容器类,它包含单个的非nullT 对象,或者什么也不包含(nothing at all)。有了 Optional ,我们处理空返回不用像抛出异常那样重型化,也不用专门返回 null 特殊处理。

// Returns maximum value in collection as an Optional
public static > Optional max(Collection c) {
  if (c.isEmpty()) return Optional.empty();
  E result = null;
  for (E e : c) 
    if (result == null || e.compareTo(result) > 0) 
      result = Objects.requireNonNull(e);
  return Optional.of(result);
}

针对返回 Optional 的函数。调用者可以做的操作包括:

// 收到空值时指定其他返回值
String lastWordInLexicon = max(words).orElse("No words...");
// 收到空值时抛出异常,注意此处使用函数式编程方法,避免了非空时浪费资源创建异常
Toy myToy = max(toys).orElseThrow(TemperTantrumException::new);
// 直接取内容,可能抛出 NoSuchElementException
Element lastNobleGas = max(Elements.NOBLE_GASES).get();

另外,Optional 提供 isPresent() 方法来验证是否非空。但是,通常上面几种用法可以避免使用 isPresent(),并且更加简洁。

虽然 Optional 有上述优点。但是并非所有数据类型都适用于它。还有,相比于返回 nullOptional 的开销更大。

  • 所有集合类都不适用。例如,返回空的List优于返回空的 Optional>
  • 也不要用它来返回一个封装基础类型,例如 Optional
  • 不要再数组、集合的key,value上用它

实践56 为所有外部暴露API编写注释文档(Write doc comments for all exposed APIelements)

(略)

你可能感兴趣的:(Effective Java英文第三版读书笔记(3) -- 函数最佳实践)