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