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

/ dev

Item 57 : Minimize the scope of local variables

最小化局部变量的作用域

  1. 在使用的地方声明局部变量,过早的声明会导致代码块过早开始过晚结束;
  2. 让方法保持精简,集中于某一些逻辑,如果方法太大分成两个方法;
  3. 使用for循环而不是使用while循环;
    声明局部变量时便初始化。 同时偏向于使用for循环而不是while循环,使用while循环会造成一些复制粘贴错误,比如:
1
2
3
4
5
6
7
8
9
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循环:

1
2
3
4
5
6
7
8
9
10
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. 基本类型在时间和空间上比包装类型更好的性能;
1
2
3
4
// 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。

1
2
3
4
5
6
7
8
// 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是基本类型,循环中反复拆装箱导致性能问题。
但也有必须使用包装类的时候:

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

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

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

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

而不是:

1
2
// 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使用到后面的代码优化都提供了很多的建议,还是可以帮助我们规范不少平时的编码习惯。