EFFECTIVE JAVA 3RD EDITION — 第九章 日常编程

Item 57 : Minimize the scope of local variables

最小化局部变量的作用域

  1. 在使用的地方声明局部变量,过早的声明会导致代码块过早开始过晚结束;
  2. 让方法保持精简,集中于某一些逻辑,如果方法太大分成两个方法;
  3. 使用for循环而不是使用while循环;
    声明局部变量时便初始化。 同时偏向于使用for循环而不是while循环,使用while循环会造成一些复制粘贴错误,比如:
Iterator<Element> i = c.iterator();
while (i.hasNext()) {
    doSomething(i.next());
}
...
Iterator<Element> i2 = c2.iterator();
while (i.hasNext()) { // BUG!
    doSomethingElse(i2.next());
}

上面的代码编译运行都不会有任何问题,就是因为变量i的作用域太广了,导致后面的while循环也可以使用,如果使用for循环:

for (Iterator<Element> i = c.iterator(); i.hasNext(); ) {
Element e = i.next();
    ... // Do something with e and i
}
...
// Compile-time error - cannot find symbol i
for (Iterator<Element> i2 = c2.iterator(); i.hasNext(); ) {
    Element e2 = i2.next();
    ... // Do something with e2 and i2
}

代码在编译时就会出现问题,同时for循环代码也更简洁一点。

Item 58 : Prefer for-each loops to traditional for loops

偏向使用for-each循环而不是传统for循环

由于传统的for循环和迭代器会涉及到数组下标或者是是否还有元素的问题,在更关注元素本身的情况下使用for-each会让代码更加简洁,同时避免bug出现的几率。下面三种情况不适合使用foreach:

  1. 操作集合本身的一些过滤,比如添加或者删除元素,这些都需要数组下标来支持,或者使用迭代器;
  2. 转换,替换集合或者数组的元素等操作;
  3. 并行迭代,并行操作多个集合的时候由于需要控制循环的次数和数组下标来避免数组越界;
    foreach支持循环所有实现Iterable接口的对象,总的来说就是如果更加关注循环元素本身并且不需要操作集合元素(删除等等)的话使用foreach。

Item 59 : Know and use the libraries

了解并使用库

使用标准库可以充分利用开发者和其他人的经验,避免踩坑。java7之后的随机数生成器使用ThreadLocalRandom,性能会提升很多。而对fork join和并行流则可以使用SplittableRandom。
使用标准库的好处:

  1. 不需要自己去实现,标准库的发布都是经过了严格的测试和性能测试,就算有bug或者需要功能更新会有新的版本来发布;
  2. 不用花时间去造轮子,专心于业务本身节约时间;
  3. 标准库的性能会随着版本更新而提高,毕竟是工业级的产品,不需要担心他的性能和更新问题;
  4. 功能性的更新会随着版本更新到来,如果没有需要的功能可以在开发社区进行反馈;
  5. 使用标准库可以让你的代码更为‘主流’,可以学习标准库的实现方式和代码风格;
    由于开发人员并不知道每次更新加入的新功能和特性,尽管有上面那么多的好处,但是却很少开发人员这样做。所以需要对版本的更迭和新特性保持持续关注,同时也需要对java标准类库的熟悉:java.utl java.lang java.io及其子包。

通常开发中如果Java标准类库中没有可用功能可以寻找第三方的类库,如果还是没有则自己实现。不要重复造轮子!!!

Item 60 : Avoid float and double if exact answers are required

避免在需要返回具体值的地方使用float和double

通常float和double用来做科学和工程计算的,都用于浮点计算,并且在货币计算上面很难使用。正确的货币计算应该使用int long或者BigDecimal,但BigDecimal使用上比基本类型麻烦,或者在货币计算中统一使用最小的货币单位来进行计算。

总的来说就是在需要具体值的时候不使用double和float,根据实际的数据长度来选择使用int long或者BigDecimal。

Item 61 : Prefer primitive types to boxed primitives

偏向于使用基本类型而不是其包装类

基本类型和其包装类型有几点不同:

  1. 基本类型只有值,就算有相同值的包装类型也会有不同的特征(内部属性等);
  2. 基本类型只有一个值,而包装类型可以有null这么一个值;
  3. 基本类型在时间和空间上比包装类型更好的性能;
// Broken comparator - can you spot the flaw?
Comparator<Integer> naturalOrder =
(i, j) -> (i < j) ? -1 : (i == j ? 0 : 1);
naturalOrder.compare(new Integer(42), new Integer(42));

上面的代码会返回1,因为第一次比较出现自动拆箱,42==42,第二次比较==但是却比较的是两个包装类返回false,所以包装类使用==比较永远是错误的。

在混用包装类和基本类型的时候总是会出现包装类的自动拆箱,所以要非常小心包装类是否为null。

// Hideously slow program! Can you spot the object creation?
public static void main(String[] args) {
    Long sum = 0L;
    for (long i = 0; i < Integer.MAX_VALUE; i++) {
        sum += i;
    }
    System.out.println(sum);
}

上面代码结果输出没什么问题,但是性能上却很差,因为sum是一个包装类,而循环中的i是基本类型,循环中反复拆装箱导致性能问题。
但也有必须使用包装类的时候:

  • 集合框架中必须使用包装类;
  • 作为参数类型也必须使用包装类,比如不能这样:List
  • 在执行反射方法中必须使用包装类;
    虽然自动拆装箱减少了很多麻烦,但是并没有代表其是安全的,正确的选择基本类型和包装类型来避免空指针和性能问题。

Item 62 : Avoid strings where other types are more appropriate

在可选的情况下避免使用String

String本身的表达是不够强的,本身是设计用来表达字符本身的,对于其他类型的值就难以表达。枚举和其他聚合类都很难用String来代替,并且缺乏功能性。

总的来说避免使用String来代替其他类型,特别是误用在枚举、基本类型和聚合类中,这样使用不够灵活,慢,同时更容易出现问题。

Item 63 : Beware the performance of string concatenation

留意字符连接的性能

+拼接固定长度的字符尚可,但是如果出现在循环中就会造成严重的性能问题,这种情况可以使用StringBuilder,并且初始化长度。

Item 64 : Refer to objects by their interfaces

引用接口来代替引用实现类

如果有合适的接口存在,最好把参数、返回、属性、变量都声明成接口类型。比如:

// Good - uses interface as type
Set<Son> sonSet = new LinkedHashSet<>();

而不是:

// Bad - uses class as type!
LinkedHashSet<Son> sonSet = new LinkedHashSet<>();

习惯上面的会让代码更加灵活,同时可以快速的切换不同的实现。 但如果没有合适的接口或者要使用实现类中特有的功能那么还是引用原类型,比如String,Integer这种值对象是没有一个公共的接口的。

总的来说如果存在这么一个接口,同时又不依赖于某个实现特有的功能那么引用接口是更为合适的。

Item 65 : Prefer interfaces to reflection

多使用接口而不是反射

反射的不足:

  1. 丢失了编译期的类型检查,包括异常检查,比如反射中执行一个不存在的方法只有在运行时才会抛出异常;
  2. 反射的代码笨拙难以阅读;
  3. 性能受到影响;

适当的使用反射能够起到很好的效果,比如需要执行在编译期无法知道的类,就可以使用反射来创建对象,并且通过他的接口或者父类方法来操作。

在反射的代码中有很多检查异常,可以使用ReflectiveOperationException来捕获,它是反射异常的一个公共父类。

总的来说反射是个很强大的工具,但同时也有很多缺点,如果需要使用反射,最好只使用反射来初始化对象,对象的操作使用它的接口或者父类来处理。

Item 66 : Use native methods judiciously

正确的使用本地方法

Java本地方法(JNI)主要有3个用途:

  1. 提供平台功能的访问;
  2. 提供本地方法的访问;
  3. 编写高性能的代码;
    通常不建议使用本地方法来提高程序性能,使用本地方法是不安全的,因为使用本地方法的代码不在对内存问题免疫,本地方法比java本身更加依赖于平台;同时本地方法很难调试,如果不小心很可能降低性能,因为gc无法去处理本地方法的内存。

总的来说谨慎使用本地方法,本地方法中一个小的bug可能导致程序的崩溃。

Item 67 : Optimize judiciously

正确的优化代码

不要牺牲合理的设计和结构来交换性能,专注于编写好的程序而不是快的程序,但这并不是说就不需要考虑性能,实现可以修改和优化,但是架构本身的缺陷和漏洞除了重构无法优化。

努力避免限制性能的设计和代码,特别是API等等底层代码,这些代码后期的优化和修改十分困难,并且影响很大。设计时多考虑性能上的问题,比如使用接口而不是实现类,不然就会把自己绑定到实现类上,从而忽略了性能更好的实现。

不要为了性能而去封装性能差的API,API性能差可以随着版本迭代而修复,但是你封装的代码却永远是这样。

优化代码的前后都需要测试性能,往往你想要优化的代码不会有什么性能提升甚至性能会更差,所以每一个优化都需要进行前后对比。如果你的代码用了不同的实现或者需要跑在不同的硬件平台上,你需要更多的时间去优化程序性能。

总的来说就是专注于编写好的代码,同时注意性能问题,但是编写API或者其他底层公共代码则需要考虑性能问题;测试性能,如果代码性能不够好,定位耗时的代码并且优化它,每一个优化都一定要经过测试。

Item 68 : Adhere to generally accepted naming conventions

命名规范

JAVA有两类命名规范:排版规范和语法规范, 打破命名规范会让代码变得难以阅读和掌握,同时可能会引起误解。

排版规范:

  • 包和模块命名用.分隔,同时要是小写字母,名称不要太长一个单词最好,能够体现出这个包的作用,如果是公开的API需要用企业的域名倒置,比如:com.google,同时应该避免以java或者javax开头的包命名;
  • 类和接口的名字,包括枚举和注解都应该由一个或多个单词组成,每个单词首字母大写,也可以是缩略词;
  • 方法和属性命名和类类似,不同就是首字母不用大写,如果缩略词是第一个单词应该小写;
  • 常量则应该是用下划线连接的大写单词;
  • 局部变量命名则相对独立和宽松。方法入参则是一种特殊的局部变量,它的命名需要格外注意,因为他们会出现在文档中;
  • 类型参数则只用一个单独字母表示,大部分都是下面几种:
    T 代表任意类型,E代表集合的元素,K,V则代表Map的key和value,X代表异常,R代表返回值,一组类型通常用T,U,V或者T1,T2,T3。
    语法规范:
  • 可实例化的类包括枚举命名都是由一个名词或者名词短语构成,比如 Thread,PriorityQueue,而不可实例化的类通常是由复数形式的名词构成,比如Collectors,Collections;
  • 接口命名则和类相同,或者有一个形容词结尾,比如Runnable,Iterable;
  • 方法命名则通常都是由动词或者动词短语构成,比如append,drawImage,返回布尔值的方法通常以is或者has开头,紧跟一个名词,名词短语,或者是其他任何作为形容词的单词或者短语,比isDigit,isProbablePrime
    非布尔返回值的方法则通常用名词,名词短语或者是动词短语,并且用get开头,比如size,hashCode,getTime
    另外一些特别的方法命名需要注意,比如类型的转换toString,toArray等等,或者返回了一个和入参不相同的类型,比如asList等等,返回基本类型的通常以typeValue命名,比如intValue,而针对静态工厂方法则通常用from,of,valueOf,instance,getInstance,newInstance等等命名;
    总的来说,学习标准的命名并深入了解和使用,排版规范更直接和清楚,语法规范则更为复杂和宽松。

这一章很贴近平时编程从代码命名规范,接口、方法设计,API使用到后面的代码优化都提供了很多的建议,还是可以帮助我们规范不少平时的编码习惯。

版权所有丨转载请注明出处:https://minei.me/archives/502.html