EffectiveJava(7)之方法

注:本文是《Effective Java》学习的笔记。

本片叙述如何处理参数和返回值,如何设计方法签名,如何为方法编写文档。

49.检查参数的有效性

大多数方法和构造器对于传递给它们的参数值都会有某些限制。例如,索引值必须是非负数,对象引用不能为null,等等。

应该在文档中清楚的指明这些限制,并且在方法体的开头处检查参数,以强制施加这些限制。

如上是发生错误之后尽快检测出错误的原则。

在Java7中新增了Objects.requireNonNull方法比较灵活且方便,因此不必因手工进行null检查。也可以用它的重载方法,增加了异常抛出说明。

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

50.必要时进行保护性拷贝

假设类的客户端会尽其所能来破坏这个类的约束条件,因此你必须保护性的设计程序。

以日期类为例。date 是一个可变的类。 simpleDateFormat 这个类也是一个多线程存在不安全问题的类。

所以以上这一套日期操作,在java8完全可以被替换掉。使用LocalDateTime(当前时间,没划分时区)

Instant(有时区划分,默认格林尼治时间)  ZonedDateTime(有时区划分) 来替换date

使用 DateTimeFormatter 来代替 SimpleDateFormat 下面是一个小demo

Date已经过时了,不应该在新代码中使用。

下面是一个Date通过构造方法保护实例,防止被修改的Demo

@ToString
@Getter
@AllArgsConstructor
public final class DateTest {                    //111111
    private final Date start;
    private final Date end;
    public static void main(String[] args) {
       Date end = new Date(1990,1,1);
       DateTest dateTest = new DateTest(new Date(1970,1,1),end);
       end.setYear(1000);
       System.out.println(dateTest); 
      //DateTest(start=Tue Feb 01 00:00:00 CST 3870, end=Mon Feb 01 00:00:00 CST 2900)
    }
}
@Getter
public final class DateTest {                       //2222222
    private final Date start;
    private final Date end;
    public DateTest(Date start,Date end){
        this.start = new Date(start.getTime());    //内层重新创建实例。
        this.end = new Date(end.getTime());
    }
}

11111是没修改之前的类,可见end.setYear();就可以轻松破坏掉类的实例。 

2222是修改之后的类,内层重新创建实例可以保证外层修改end 当前实例不会被破坏

对于构造器的每个可变参数进行保护性拷贝是必要的。

!!注:保护性拷贝是在检查参数的有效性之前进行的,并且有效性检查是针对拷贝之后的对象,而不是针对原始的对象。

上述没有使用clone进行对象的克隆是因为date不是final的  使用克隆有被Date子类破坏掉的风险。

51.谨慎设计方法签名

谨慎的选择方法的名称,要符合命名规范。每个公司都有命名规范,默认遵循阿里的命名规范了。

不要过于追求提供便利的方法。

避免过长的参数列表。 相同类型的长参数序列格外有害。

对于参数类型,要优先使用接口而不是类。

对于boolean参数,要优先使用两个元素的枚举类型。这样方便后续修改。比如语义是大为true 小为false 

那么可以设计一个枚举   BIG,SMALL 这样后续需要修改时再在枚举中添加就好了。亲测好用。

52.慎用重载

要调用哪个重载方法是在编译时做出决定的。

对于重载方法的选择是静态的,而对于被重写的方法的选择则是动态的。

public  static  String  test(Set<?> s);

public  static  String  test(List<?> s);

public  static  String  test(Collection<?> s);  这三个重载的方法若是调用时泛型时Collection 则只会调用第三个而不会调用前两个,这算是一个小坑吧。

上述的重载可以用  instanceof代替。 

public static String test(Colletion<?> c){ return  c instanceof List ? "list" ...... }

安全而保守的策略是,永远不要导出两个具有相同参数数目的重载方法。

始终可以给方法起不同的名称,而不是使用重载机制。

综上,尽量不用、也没有太大好处。就是方法名一样而已。

53.慎用可变参数

这条建议,我几乎没有使用过可变参数。没觉得带来很多好处。参数不固定时确实是一个可行的使用场景。

54.返回零长度的数组或者集合,而不是null

有的人任务null返回值 比零长度集合或者数组更好,因为避免额分配零长度的容器所带来的开销。我之前就是这个想法。

这个观点是站不住脚的。原因有两点。第一:在这个级别上担心性能问题是不明智的,除非分析表明这个方法正是造成性能问题的真正源头。第二:不需要分配零长度的集合或者数组,也可以返回它们。

public List<String> get(){ return new ArrayList<>(someList); }    也可以像下面这儿样的返回空list  map啥的、也可以像数组那样,事先定义好一个空集合供返回。

public static List<String> get(){
    return Collections.emptyList();
}

关于数组也是这么玩的。返回一个零长度的数组而不是null

永远都不要返回null,而不返回一个零长度的数组或者集合。如果返回null,那样会使API更难以使用,也更容易出错,而且没有任何性能优势。

55.谨慎返回Optinal 

Java8之前,要编写一个在特定环境下无法返回任何值的方法是,有两种方法:要么抛出异常,要么返回null.

上述两种异常的代价很高,而返回null就像埋下了一个地雷。客户端编码缺少校验很容易NPE

第三种编写不能返回值的方法是 使用  Optional 

理论上能返回T的方法,实践中也可能无法返回,因此在某些特定的条件下,可以改为声明返回Optional

private static Optional<?> getArr(String arr){
    return arr == null? Optional.empty():Optional.of(arr);
}
System.out.println(getArr("asd").isPresent());    //true
System.out.println(getArr("asd").get());          //asd

永远不要通过返回Optional的方法返回null   Optional本质上与受检异常类似。

Stream有很多终止操作就是返回的Optional

如果方法返回Optional ,客户端必须做出选择:如果该方法不能返回值时应该采取什么动作。可以指定一个缺省值。

Stream.of("a","b","c").max((o1,o2)->o2.length()-o1.length()).orElse("other  words");

max().get()就获取到了这个Stream的结果。 

Optional有一个 isPresent 方法可以判断是否存在值。Boolean类型返回结果。

当使用Stream编程时,经常会遇到Stream<Optional<T>> ,为了推动进程还需要一个包含了非空optional中所有元素的Stream<T>. 可以通过过滤器 Optional::isPresent 来解决。

容器类型包括集合、映射、Stream、数组和Optional,都不应该被包装在Optional中。

Optional<T>的使用场景如下。

如果无法返回结果并且当没有返回结果时客户端必须执行特殊的处理,那么就应该声明该方法返回Optional<T>

optional不适应于注重性能的情况。   永远不应该返回基本类型的optional (int long double)

几乎永远都不适合用optional作为键、值,或者集合或数组中的元素。而是只作为一个返回结果。

总之:如果发现自己在编写的方法始终无法返回值,并且相信该方法的用户每次在调用它时都要考虑到这种可能性,那么或许就应该返回一个optional 。但是,应当注意到与返回optional相关的真实的性能影响;对于注重性能的方法,最好是返回一个null,或者抛出异常。最后,不要讲optional用作返回值以外的其他用途。

56.为所有导出的API元素编写文档注释

为了正确的编写API文档,必须在每个被导出的类、接口、构造器、方法和域声明之间增加一个文档注释。

方法的文档注释应该简洁的描述出它和客户端之间的约定。

当为泛型或者方法编写文档时,确保要在文档中说明所有的类型参数。

当为枚举类型编写文档时,要确保在文档中说明常量。

为注解类型编写文档时,要确保在文档中说明说要成员。

类或者静态方法是否线程安全,应该在文档中对它的线程安全级别进行说明。

 


57.将局部变量得作用域最小化

要使局部变量的作用域最小化,最有力的方法就是在第一次要使用它的地方进行声明。
对于局部变量的初始化操作,必须在下面马上使用,否则会让程序变得更难以理解。这对内存等资源来说也是提前占用的。是一个不好的编程习惯。
需要注意一下for和while的循环条件的局部变量有效范围。for循环的循环变量是在循环条件时声明的。而while是可以在外部声明循环变量的。
使方法小而集中。

58.foreach循环优先于传统的for循环。

foreach形式的循环是要比for以及迭代器iterator更易读懂的。而性能是差不多的。前期是你正确迭代使用。

59.了解和使用类库。

使用Random. nextInt来代替  random. nextInt  前者是静态方法,后者是实例方法。
从java7开始就不应该再使用Random了。现在选择随机数生成器时,大多使用ThreadLocalRandom 它会产生更高质量的随机数。
对于并发使用,fork   并行流则使用splittableRandom
对于新特性的学习是很重要的。让我们更少的重复造轮子。lang  util  io包中的特性尤其重要。
对于一些常规操作,工具类中已经有了实现。如果上述java包中没有,那么可以去牛逼点的第三方类库寻求方法,比如Guava

60. 如果需要精确的答案,请避免使用float和double

float  double尤其不适合用于货币计算。
其表示的数值会出现0.7999999999998这种而不是0.8
对于需要精确表示的数值请使用BigDecimal 或者根据数值的长度使用int  long  
BigDecimal肯定是相对来说有点麻烦的。但是更加精确。BigDecimal也会使性能有一些下降。

61.基本类型优先于装箱基本类型。

如果你只是想表达一个数,而不使用null及包装类型才有的操作,那就使用基本类型。因为基本类型要比装箱类型更快。
要明确装箱类型与基本类型的区别。比==和equals对于这两者时的不同。
当在一项操作中混合使用基本类型和装箱基本类型时,装箱基本类型都会自动拆箱。
而需要注意一下null的自动拆箱会触发npe
注意!!!一定要避免程序进行多次没有必要的反复拆箱装箱操作。

62.如果其他类型更合适,则尽量避免使用字符串。

能用其他类型表示就使用其他类型表示,字符串表示一些东西会造成程序的复杂混乱。尤其注意不要用字符串来存储时间!!!非常恶心。

63.了解字符串连接的性能。

字符串连接string  大家都知道每个+都意味着新对象的生成。超级占内存。非常恶心。禁止大量字符串拼接+这么玩。
字符串string类是final的,保证string的不可变。
此处需要了解string  stringbuffer  stringbuilder的区别。
如果大量字符串拼接请使用stringbuilder,涉及到资源竞争的字符串拼接使用stringbuffer,其他不需要字符串拼接的场景再使用string

64.通过接口引用对象。

如果有合适的接口类型存在,那么对于参数,返回值,变量和域来说,就都应该使用接口类型进行声明。
接口是一种多态的表现形式。这个事情还是要看应用场景。编程过程中就自然而然得写好了。

65.接口优先于反射机制

反射有一些缺点
损失了编译时类型检查的优势。
执行反射访问所需要的代码非常笨拙和冗长。
性能损失。
对于编译时不能确定的可以使用反射来实现。

66.谨慎的使用本地方法。

本地方法是使用java以外得语言来编写的。不见得就比java自身的实现性能高,安全。所以除非必要,否则不要使用本地方法。本地方法也有被java重写过的比如BigInteger在java3之前就是c写的。后来java重写性能更好了。
所以本地方法可能会导致预期之外的错误。要小心使用。

67.谨慎的进行优化。

不要因为性能优化而破坏点程序结构。
避免设计有局限的程序。局限是要可控的而不是被动被局限住的。

68.遵守普遍接受的命名惯例。

这个在有些场景是被配置的,你就必须叫这个名称才会被注入。
约定高于配置。
对于命名我觉得遵从阿里巴巴的代码规范就挺不错的。idea也有对于代码的format

69.只针对异常的情况才使用异常。

异常应该只用于异常的情况下;他们永远不应该用于正常的控制流。
考虑使用optional返回值,或者返回一个可识别的值,比如null

70.对可恢复的情况使用受检异常,对编程错误使用运行时异常。

如果期望调用者能够适当的恢复,对于这种情况就应该使用受检异常。比如密码错误你想捕获到这个异常并且想让程序更加清晰,使用受检异常,编辑器会时刻提醒你来处理这个异常。
如果程序抛出未受检异常或者错误,往往就属于不可恢复的情形,继续执行下去有害无益。
除非不得已,否则不要试图通过异常来实现逻辑处理。
你实现的所有未受检的抛出结构都应该是RuntimeException的子类。不仅不应该定义Error的子类,甚至也不应该抛出AssertionError异常。
不要抛出非受检异常,这只会困扰API的用户。

71.避免不必要的使用受检异常

受检异常如果使用得当,它们可以改善API和程序。但是大量使用受检异常会使API使用起来非常不方便。
抛出受检异常的方法不能直接在Stream流中使用。
一种好的代替抛出受检异常的方式是使用optional 
尤其在stream流中更有效。
此时需要考虑的是optional 返回没有什么说明,而异常会附有一些信息。但是异常需要我们每个catch到的时候做出响应。
总而言之,在谨慎使用得前提之下,受检异常可以提升程序的可读性,如果过度使用,将会使API使用起来非常痛苦。如果调用者无法恢复失败,就应该抛出未受检异常。如果可以恢复,并且想要迫使调用者处理异常的条件,首选应该返回一个optional值。当且仅当万一失败时,这些无法提供足够的信息,才应该抛出受检异常。

72.优先使用标准的异常。

使用java标准类库提供的受检异常来实现代码复用。这使程序更加轻便也更加易读。
比如IllegalArgumentException不合法的参数异常。IllegalStateException如果因为接收到的对象状态而使调用非法。比如调用未经初始化的对象。
还有IndexOutOfBoundsException, ConcurrentModificationException,UnsupportedOperationException等等
不要直接重用Exception  RuntimeException  Throwable  Error这些顶级父类。因为可能会导致一些预期之外的问题。因为类库中的异常也是继承于上述顶级父类的。
注意异常类是可以序列化的,因此这也是除非不得已,否则不要自己编写异常类的原因之一。

73.抛出与抽象对应的异常。

更高层的实现应该捕获低层的异常,同时抛出可以按照高层抽象进行解释的异常。这种做法称作异常转译。一种特殊的异常转译形式称为异常链。
总而言之,如果不能阻止或者处理来自更低层的异常,一般的做法是使用异常转译。只有在低层方法的规范碰巧可以保证它所抛出非所有异常对于更高层也是合适的情况下。才可以将异常从低层传播到高层。异常链对高层和低层异常都提供了最佳的性能。他允许抛出适当的高层异常,同时又能捕获低层的原因进行失败分析。

74.每个方法抛出非所有异常都要建立文档。

文档对于别人理解你的程序很关键。写文档是一个好的习惯。

75.在细节信息中包含失败!捕获信息。

在msg中注明异常参数对于调试起来非常方便。
为了捕获异常,异常的细节信息应该包含对该异常有贡献的所有参数和域的值。
当然在细节信息中不要包含密码,密钥以及类似的信息。
一种使异常类很好的打印细节信息的方式是,异常类的构造方法入参可以设定需要传入的细节信息值,然后再使用super将值format到父类异常。来实现补充异常信息。当然再throw的时候做这种操作也是可以的,只不过没有构造方法的方式好。要时刻关注代码的复用来降低程序的复杂性。

76.努力使失败保持原子性。

一般而言,失败的方法调用应该使对象保持在被调用之前的状态。

77.不要忽略异常。

空的catch块会使异常达不到应有的目的。
如果选择忽略异常,catch块中应该包含一条注释,说明为什么可以这么做,并且变量应该命名为ignored  






 

 

 

 

 

 

 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章