《Effective Java》阅读笔记-第八章

Effective Java 阅读笔记

第八章 方法

第 49 条 检查参数的有效性

基于“发生错误后应尽快检测出错误”这一通用原则,应对方法的参数进行检查。

Java 7 中增加了Objects.requireNonNull方法,可以很方便的对参数进行null检查并抛出异常:

public void someMethod(String args) {
    args = Objects.requireNonNull(args, "xxx 参数不可以为 null");
}

第 50 条 必要时进行保护性拷贝

在设计类时,应考虑到使用者会破坏类的约束条件,因此应设置保护性措施。

比如要设计一个 Period 的类:

public final class Period {
    private final Date start;
    private final Date end;

    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 getStart() {
        return start;
    }

    public Date getEnd() {
        return end;
    }
}

这个类看起来是不可变的,但是不要忘了Date类本身是可变的,因此很容易违反开始不能大于结束的约束。

解决方法就是使用 Java 8 加入的不可变类LocalDateTime,因为Date已经过时了。

或者在进行保护性拷贝:

public final class Period {
    private final Date start;
    private final Date end;

    public Period(Date start, Date end) {
        if (start.compareTo(end) > 0) {
            throw new IllegalArgumentException(start + " after " + end);
        }
        this.start = new Date(start.getTime());
        this.end = new Date(end.getTime());
    }

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

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

这样这个类就是真正不可变的了(这里不讨论反射)。

第 51 条 谨慎设计方法签名

  • 谨慎选择方法名称:方法名称首先要易于理解,并且风格一致。然后是优先大众认可的名称。
  • 不要过于追求提供便利的方法:方法太多会使学习、使用、文档化、测试、维护的难度增加,优先提供一个功能齐全的方法,只有当某一项操作经常用到的时候才考虑为它提供快捷方式,如果不确定,那就不要提供快捷方式。
  • 避免过长的参数列表:尽量在4个参数或者更少,参数过多不便使用。
  • 参数类型优先使用接口,而不是类:使用接口扩展性更好。
  • boolean 参数,可以使用两个元素的枚举:用枚举的话,不管是可读性还是后续扩展,都更好一些。

第 52 条 重载方法需谨慎

比如下面这个程序,根据一个集合是 Set 还是 List 还是其他类型,对其进行分类:

public class CollectionClassifier {
    public static String classify(Set<?> set) {
        return "Set";
    }
    public static String classify(List<?> list) {
        return "List";
    }
    public static String classify(Collection<?> coll) {
        return "Unknown Collection";
    }

    public static void main(String[] args) {
        List<Collection<?>> list = new ArrayList<>();
        list.add(new HashSet<String>());
        list.add(new ArrayList<Integer>());
        list.add(new HashMap<String, String>().values());

        for (Collection<?> collection : list) {
            System.out.println(CollectionClassifier.classify(collection));
        }
    }
}

本意是好的,但是实际程序打印了三次Unknown Collection,因为classify方法被重载(overloaded)了,要调用哪个重载方法是在编译时决定的,因此调用的全部是classify(Collection<?> coll)。正确的做法是使用instanceof进行运行时检测。

应避免胡乱使用重载,比较保守的策略是永远不要导出两个具有相同参数数量的方法,如果有需要可以给方法改名,而不是使用重载。

Java 5 之前,基本类型不同于所有引用类型,但是引入自动拆装箱之后,List接口中的方法重载就产生了混乱,比如下面这个例子:

Set<Integer> set = new TreeSet<>();
List<Integer> 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: " + set);
System.out.println("list: " + list);

上面的例子中,Set 和 List 进行了相同的操作,大多数人的直觉来看,最终都打印[-3, -2, -1]
但是结果并不是这样,最后打印结果是:

set: [-3, -2, -1]
list: [-2, 0, 2]

原因就是 Collection 中有一个方法remove(Object o),Collection 的子接口 List 有一个同名的重载方法remove(int index),因此就很容易使用出错。

想要上述代码产生一直的结果需要这么改:

for (int i = 0; i < 3; i++) {
    set.remove(i);
    list.remove((Integer) i);
}

上面就是加入自动拆装箱之后破坏了 List 接口。

Java 8 引入 Lambda 之后加剧了重载造成的混乱:

// 可编译
new Thread(System.out::println).start();

// 不可编译
ExecutorService exec = Executors.newCachedThreadPool();
exec.submit(System.out::println);

上面的代码中,第一行可以编译,后面两行不可以编译。因为submit方法有一个Callable<T>的重载,而 Thread 构造函数没有。

并且加入println方法没有重载,那么调用就是合法的。归根结底就是因为System.out::println是一个不精确调用,即使所有的重载方法都是void的。

因此在重载方法时,应该保证:传递同样的参数时,所有重载方法的行为应该保持一致。

第 53 条 慎用可变参数

如果要检查可变参数的数量,比如至少为1个,这种情况是就应该直接在方法签名上修改为一个参数+一个可变参数,而不是直接用可变参数。

例子

比如有时候写一个必须要一个或多个参数的方法,如果只使用可变参数是这种情况:

public static int sum(int... args) {
    if (args.length == 0) throw new IllegalArgumentException("args too few");

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

把问题从编译期推迟到了运行期,这种情况看就应该直接定义成下面这种方式:

public static int sum(int firstArg, int... remainingArgs) {
    int sum = firstArg;
    for (int arg : remainingArgs) {
        sum += arg;
    }
    return sum;
}

这样在编译时就可以发现问题。

在重视性能的时候,使用可变参数应该小心,因为每一次调用都会创建并初始化一个数组,如果没办法承受这个成本,那么就可以吧常用的参数数量的方法定义出来:

就像这样
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 条 返回 0 长度的数组或集合,而不是 null

返回 null 会使调用者需要额外的代码处理 null 值,而空集合不会。

返回空集合时不要使用 new 创建,可以使用Collections.emptyList()Collections.emptySet()Collections.emptyMap()等。

第 55 条 谨慎返回 Optional

在 Java 8 之前,在编写特定情况下没有值的时候,只能返回 null 或者抛出异常。

Java 8 引入了Optional,强制用户处理 null 情况。

  • 如果一个方法返回 Optional,那就不要直接返回 null,而是返回Optional.empty()
  • 容器类型包括集合、映射、Stream、数组和 Optional,都不应该被包装在 Optional 中,也就是不要返回Optional<List<T>>,而是返回一个空List<T>
  • 如果没办法返回结果,并且没有返回结果是调用者必须进行特殊处理时,就可以返回 Optional。
  • 不应该返回Optional<Integer>,使用OptionalIntOptionalLongOptionalDouble代替。
  • 基本任何时候都不适合用 Optional 作为键、值,或者集合和数组中的元素。

第 56 条 为所有导出的 API 编写文档

JavaDoc 是个好东西。

posted @ 2024-02-21 16:16  code-blog  阅读(3)  评论(0编辑  收藏  举报