Effective.Java第45-55条(规范相关)

45.  明智谨慎地使用Stream

 

46.  优先考虑流中无副作用的函数

 

47.  优先使用Collection而不是Stream作为方法的返回类型

 

48.  谨慎使用流并行

 

49.  检查参数有效性

  大多数方法和构造方法对可以将哪些值传到其对应参数有一些限制。例如:索引必须是非负数、对象引用必须是非null。你应该清楚地在文档中记载所有这些限制,并在方法主体的开头用检查来强制执行。

  每次编写方法或构造方法时,都应该考虑对其参数存在哪些限制。应该记住这些限制,并在方法体的开头使用显示检查来强制执行这些限制。养成这样的习惯很重要。在第一次有效性检查失败时,它所需要的工作量将会得到对应的回报。

 

阿里规约有两条:

 【参考】 下列情形,需要进行参数校验:
1) 调用频次低的方法。
2) 执行时间开销很大的方法。 此情形中, 参数校验时间几乎可以忽略不计,但如果因为参
数错误导致中间执行回退,或者错误,那得不偿失。
3) 需要极高稳定性和可用性的方法。
4) 对外提供的开放接口,不管是 RPC/API/HTTP 接口。
5) 敏感权限入口。
【参考】 下列情形, 不需要进行参数校验:
1) 极有可能被循环调用的方法。但在方法说明里必须注明外部参数检查要求。
2) 底层调用频度比较高的方法。毕竟是像纯净水过滤的最后一道,参数错误不太可能到底
层才会暴露问题。一般 DAO 层与 Service 层都在同一个应用中,部署在同一台服务器中,所
以 DAO 的参数校验,可以省略。
3) 被声明成 private 只会被自己代码所调用的方法,如果能够确定调用方法的代码传入参
数已经做过检查或者肯定不会有问题,此时可以不校验参数。

 

比如JDK7带的Objects的验证null的方法,一般用此方法显示检查是否为空指针

    public static <T> T requireNonNull(T obj, String message) {
        if (obj == null)
            throw new NullPointerException(message);
        return obj;
    }

比如DateTimeFormatterBuilder类的如下方法:

    public DateTimeFormatterBuilder appendPattern(String pattern) {
        Objects.requireNonNull(pattern, "pattern");
        parsePattern(pattern);
        return this;
    }

 

例子如下:

    public static void main(String[] args) {
        String string = null;
        Objects.requireNonNull(string, "string is null");
    }

结果:

Exception in thread "main" java.lang.NullPointerException: string is null
at java.util.Objects.requireNonNull(Objects.java:228)
at Client.main(Client.java:9)

 

补充:有时候我们修改元素的时候通过get方法传入ID进行修改,为了防止传入不存在的ID,我们可以在Action验证查到的bean

    public String update() {
        employeecharge = employeeChargeService.findById(id);

        // 检查是否存在
        Objects.requireNonNull(employeecharge, "尝试访问不存在的数据" + id);

        return "update";
    }

 

50.  必要时进行防御性拷贝

  必须防御性地编写程序,假定类的客户端尽力摧毁类的不变量。

  比如我们编写表示一个不可变的时间期间类,如下:

import java.util.Date;

public class Period {

    private final Date start;
    private final Date end;

    public Period(Date start, Date end) {
        // >0表示在其后面
        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设置为final,但是只是引用不可变,由于Date是可变类,所以可以利用这一特性进行改变,如下:

        Date start = new Date();
        Date end = new Date();
        Period period = new Period(start, end);
        start.setDate(start.getDate() + 1);

 

  从Java8开始,解决这种问题可以使用Instant(或LocalDateTime或ZonedDateTime)代替Date,因为这些类是不可变类。Date已经过时,在新代码中不应再使用。

        // 类似于构造者模式
        DateTimeFormatter dateTimeFormatter = new DateTimeFormatterBuilder().appendPattern("yyyy/MM=dd HH:mm:ss")
                .toFormatter();
        LocalDateTime localDate = LocalDateTime.parse("2019/06=24 20:01:01", dateTimeFormatter);
        System.out.println(localDate);

        LocalDate now = LocalDate.now();
        System.out.println(now);

        LocalDateTime localDateTime = LocalDateTime.now();
        System.out.println(localDateTime);

 

JDK7中解决方法:

import java.util.Date;

public class Period {

    private final Date start;
    private final Date end;

    public Period(Date start, Date end) {
        this.start = new Date(start.getTime());
        this.end = new Date(end.getTime());

        // >0表示在其后面
        if (start.compareTo(end) > 0) {
            throw new IllegalArgumentException(start + "  after " + end);
        }
    }

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

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

}

  新的构造方法以及get方法将不会改变其值。注意:防御性拷贝是在参数有效性之前进行的,有效性检查是在拷贝上而不是在原始实例上进行的。

 

  在可能的情况下,应该使用不可变对象作为对象的组件,这样就不必担心防御性拷贝。

 

51.  仔细设计方法签名

   仔细设计方法名名称。主要目标是选择与同一包名中的其他名称一致且易于理解的名称,其次是选择与广泛的共识一致的名称。避免使用较长的方法名。

  不要过分地提供方便的方法。

  避免过长的参数列表。目标是四个或者更少。相同类型参数的长序列尤其有害,程序员记不住参数的意义且编译期间如果顺序错误也能正常编译。三种方法可以缩短过长的参数列表:将方法分解为多个方法;创建辅助类(静态内部类)来保存数组;从对象构造方法调用采用Build构造模式。

  对于参数类型,优先选择接口而不是具体的类。如果有合适的接口来定义一个参数用接口定义。

  与布尔型参数相比,优先选择两个元素枚举类型。

  

52.  明智审慎地使用重载

  重载方法之间的选择是静态的,而重写方法的选择是动态的。

  如下重载代码:

package zd.dms.test;

/**
 * 静态分配例子
 * 
 * @author Administrator
 *
 */
public class StaticDispatch {
    static abstract class Human {

    }

    static class Man extends Human {

    }

    static class Woman extends Human {

    }

    public static void sayHello(Human human) {
        System.out.println("human");
    }

    public static void sayHello(Man man) {
        System.out.println("man");
    }

    public static void sayHello(Woman woman) {
        System.out.println("woman");
    }

    public static void main(String[] args) {
        Human man = new Man();
        Human woman = new Woman();
        sayHello(man);
        sayHello(woman);
    }
}

结果:

human
human

 

解释:从结果看出执行的参数类型是Human,为什么会选择Human的重载?

Human man = new Man();

 

  我们把上面代码的Human称为变量的静态类型(Static Type),或者叫做外观类型(Apparent Type),后面的Man类称为变量的实际类型(Actual Type),静态类型和实际类型在程序中都可以发生一些变化,区别是静态类型的变化仅仅在使用时发生,变量本身的静态类型不会改变,并且最终的静态类型是在编译期可知的;而实际类型变化的结果在运行期才可确定,编译器在编译程序的时候并不知道一个对象的实际类型是什么。

  解释了这两个概念,main()方法的两次sayHello()方法调用,使用哪个重载版本完全取决于传入参数的数量和数据类型。代码中定义了 两个静态类型相同但实际类型不同的变量,但虚拟机(准确地说是编译器)在重载时是通过参数的静态类型而不是实际类型来作为判定依据的。并且静态类型是编译器可知的,因此,在编译阶段,Javac编译器会根据参数的静态类型决定使用哪个重载版本,所以选择了sayHello(Human)作为调用目标。  通过这个例子,我们明白了永远不要重写一个方法,参数个数完全相同,类型不同但是类型具有父子关系。

  所有依赖静态类型来定位方法执行版本的分派动作称为静态分派。静态分派的典型应用是方法重载。静态分派发生在编译阶段,因此确定静态分派的动作实际上不是由虚拟机来执行的。另外,编译器虽然能确定出方法的重载版本,但在很多情况下这个重载版本并不是唯一的,往往只能确定一个更加合适的版本。

  因为重写是规范,而重载是例外,应该避免混淆使用重载。一个最好的办法是永远不要写具有两个相同参数数量的重载。

 

53.  明智审慎地使用可变参数

  可变参数方法正式名称为可变的参数数量方法,接受零个或多个指定类型的参数。可变参数机制首先创建一个数组,其大小是在调用位置传递的参数数量,然后将参数放入数组中,最后将数组传递给方法。

  当需要使用可变数量的参数定义方法时,可变参数非常有用。在使用可变参数前加上必需参数,并注意可变参数带来的性能后果。  

 

54.  返回空的数组或集合,不要返回null

   在几乎使用null代替空数组或集合时,有可能造成一定的影响,因为调用方法的客户端可能不知道来处理返回null的情况。此外,用null代替空容器会使得返回容器的方法的实现变得复杂。

  有时可能会觉得返回空集合或者数组会有分配空间的开销。这里有两个误区,首先,除非测量结果表明所讨论的分配是性能问题的真正原因,否则不宜担心此级别的性能。第二,可以在不分配空集合和数组的情况下返回它们。如果有数据证明分配集合会损害性能,可以通过重复返回相同的不可变空集合来避免分配,因为不可变对象可以自由共享。Collections.emptyList(),Collections.emptySet(),Collections.emptyMap()。

List<Object> emptyList = Collections.emptyList();

 

源码如下:

    @SuppressWarnings("rawtypes")
    public static final List EMPTY_LIST = new EmptyList<>();

    @SuppressWarnings("unchecked")
    public static final <T> List<T> emptyList() {
        return (List<T>) EMPTY_LIST;
    }

 

  数组的情况与集合的情况相同,永远不要返回null,而是返回长度为零的数组。如果认为零长度数组会损害性能,则可以重复返回相同的零长度数组,如下:

    private static final Object[] OBJECTS = new Object[0];

 

  总之,永远不要返回null来代替空数组或集合。它使你的API更难以使用,更容易出错,并且没有性能优势。

 

55.  明智审慎地返回Optional

 

posted @ 2019-06-24 10:50  QiaoZhi  阅读(519)  评论(0编辑  收藏  举报