Effective|Effective Java英文第三版读书笔记(4) -- 通用编程最佳实践

写在前面 《Effective Java》原书国内的翻译只出版到第二版,书籍的编写日期距今已有十年之久。这期间,Java已经更新换代好几次,有些实践经验已经不再适用。去年底,作者结合Java7、8、9的最新特性,编著了第三版(参考https://blog.csdn.net/u014717036/article/details/80588806)。当前只有英文版本,可以在互联网搜索到PDF原书。本读书笔记都是基于原书的理解。
以下是正文部分
通用编程(General Programming) 本章包括:

  • 实践57 最小化局部变量作用域(Minimize the scope of local variables)
  • 实践58 使用 foreach 循环替代传统循环(Prefer for-each loops to traditional for loops)
  • 实践59 知道并使用库(Know and use the libraries)
  • 实践60 不要在需要精确结果的场景使用浮点数(Avoid float and double if exact answers are required)
  • 实践61 使用基础类型替代封装基础类型(Prefer primitive types to boxed primitives)
  • 实践62 尽量用更恰当的类型替换 String (Avoid strings where other types are more appropriate)
  • 实践63 拼接 String 是低效的(Beware the performance of string concatenation)
  • 实践64 使用对象的接口来指示对象(Refer to objects by their interfaces)
  • 实践65 使用接口替代反射(Prefer interfaces to reflection)
  • 实践66 审慎地使用原生方法(Use native methods judiciously)
  • 实践67 审慎地优化代码(Optimize judiciously)
  • 实践68 坚持使用公认的命名规范(Adhere to generally accepted naming conventions)
实践57 最小化局部变量作用域(Minimize the scope of local variables) C语言的一个约定是将变量声明在函数最前面,实际上,这完全可以改变。Java 中,一个最小化局部变量作用域的好办法是,在使用变量时才去声明它。这样还能使得代码阅读起来更清晰。
  • 除了 try..catch... 中的变量外,其他变量都应该在声明时初始化。
  • 相比while循环,for循环能够帮助我们定义更加局部的变量,因此应多使用for循环。
  • 函数的功能应当尽量小且聚焦,这样避免定义过多的变量,作用域相互干扰、混淆。
实践58 使用 foreach 循环替代传统 for 循环(Prefer for-each loops to traditional for loops) 先看看传统 for 循环的示例:
// Not the best way to iterate over a collection! for (Iterator i = c.iterator(); i.hasNext(); ) { Element e = i.next(); ... // Do something with e } // Not the best way to iterate over an array! for (int i = 0; i < a.length; i++) { ... // Do something with a[i] }

for-each 方法,更加简洁,可以避免越界访问问题。
// The preferred idiom for iterating over collections and arrays for (Element e : elements) { ... // Do something with e }

在嵌套循环中,for-each 的优势更加明显。在传统循环中,为了维持第一层循环的某个变量值,我们需要单独定义一个变量;而 for-each 不需要这样做。
for (Iterator i = suits.iterator(); i.hasNext(); ) { Suit suit = i.next(); for (Iterator j = ranks.iterator(); j.hasNext(); ) deck.add(new Card(suit, j.next())); }for (Suit suit : suits) for (Rank rank : ranks) deck.add(new Card(suit, rank));

但是,要注意,有几个场景是不应该使用 for-each 的:
  • 破坏性过滤(Destructive filtering): 例如遍历时需要删除集合中的元素,则应当显式使用迭代器。
  • 元素变换(Transforming): 需要修改元素值时。
  • 并行遍历(Parallel iteration): 同时遍历多个容器。
for-each 支持遍历所有实现了 Iterable 接口的对象。
实践59 知道并使用库(Know and use the libraries) 在使用库生成随机数时,大多数人会这么写:
// Common but deeply flawed! static Random rnd = new Random(); static int random(int n) { return Math.abs(rnd.nextInt()) % n; }

  1. 如果n是2的平方切值较小,则不久就会形成序列循环
  2. 如果n不是2的平方,则返回某些数的概率会大于另一些数
  3. 极端情况下,返回的值甚至可能超出限定范围,这是处理正负值时可能引起的
实质上,完全可以使用库里面的 Random.nextInt(int) 来实现上述功能。库函数由专家编写,经历成千上万使用者的考验,其安全性、功能性都有保障。有 bug 也会及时修复。我们也可以从实现-测试-迭代中抽脱出来。另外,使用库函数也会让你的代码融入主流,更容易阅读、理解。
Java强大的社区支持,使得每个发布版本都会对库函数有很多的更新迭代。我们要尽量多得掌握、了解每个版本的关键库,包括以下几个:
  • java.lang
  • java.util
  • java.io
实践60 不要在需要精确结果的场景使用浮点数(Avoid float and double if exact answers are required) Java中浮点数的存在主要是为了科学计算与工程计算。在需要精确结果的场景,尤其是涉及到钱款的运算,不要使用浮点数,使用 int, long, BigDecimal
错误地使用浮点数示例代码:
public static void main(String[] args) { double funds = 1.00; int itemsBought = 0; for (double price = 0.10; funds >= price; price += 0.10) { funds -= price; itemsBought++; } System.out.println(itemsBought + " items bought."); System.out.println("Change: $" + funds); } //OUTPUT: //3 items bought. //Change: $0.3999999999999999

使用 BigDecimal 修正后的代码:
public static void main(String[] args) { final BigDecimal TEN_CENTS = new BigDecimal(".10"); int itemsBought = 0; BigDecimal funds = new BigDecimal("1.00"); for (BigDecimal price = TEN_CENTS; funds.compareTo(price) >= 0; price = price.add(TEN_CENTS)) { funds = funds.subtract(price); itemsBought++; } System.out.println(itemsBought + " items bought."); System.out.println("Money left over: $" + funds); } //OUTPUT: //4 items bought. //Money left over: $0.00

实践61 使用基础类型替代封装基础类型(Prefer primitive types to boxed primitives) 基础类型(例如 int)与封装基础类型(例如 Integer)有三个主要区别:
  • 基础类型只有值,封装基础类型还有对象信息
  • 封装基础类型的值可能为 null
  • 基础类型在时间效率和空间效率更优
主要的坑是,使用 == 去比较两个封装对象并不是比较其值,相同值得封装基础对象也会返回 false
在基础类型与封装基础类型混用的场景,封装基础类型会自动“解封”,如果此时对象为 null ,将抛出异常。
注意,有几种情况必须使用封装基础类型,除此之外,应尽量使用基础类型。
  1. 集合的对象不能使用基础类型
  2. 泛型参数不能使用基础类型
  3. 反射调用不能使用基础类型
实践62 尽量用更恰当的类型替换 String (Avoid strings where other types are more appropriate) String 类型不应当作为值类型、枚举类型、聚合对象类型、线程关键key来使用。
实践63 拼接 String 是低效的(Beware the performance of string concatenation) 使用 + 来拼接 String 非常方便,但是由于 String 对象的不可变性,这种拼接操作是耗时的,它需要拷贝两个对象到一个新的对象。如果在 for 循环中使用,效率会呈指数降低。
在拼接操作比较频繁是,我们就应该选用 StringBuilder 类来进行。注意,最好在一开始定义好 StringBuilder 的长度。示例代码如下:
public String statement() { StringBuilder b = new StringBuilder(numItems() * LINE_WIDTH); for (int i = 0; i < numItems(); i++) b.append(lineForItem(i)); return b.toString(); }

实践64 使用对象的接口来指示对象(Refer to objects by their interfaces) 在使用对象时,如果对象是某个适当接口的实现,那么我们应该使用接口来指示该对象。通常,确实有必要使用类信息的只有一处——使用构造函数创建对象时。其他地方包括代码的如下关键点,都最好使用接口。当然,如果对象要用到接口实现中的某些特定方法,那么还是该使用具体的对象类来标识对象。
  • 对象作为参数
  • 返回值类型
  • 变量
  • 类属性
// 推荐用法 Set sonSet = new LinkedHashSet<>(); // 不推荐用法 LinkedHashSet sonSet = new LinkedHashSet<>();

这样做可以使得程序扩展性更好。例如上述代码想改用 HashSet,则只需要换掉 LinkedHashSet 就可以了。
实践65 使用接口替代反射(Prefer interfaces to reflection) 使用 java.lang.reflect 可以基于类名,去访问到构造函数、方法、属性。这带来了一些便利性,但是同时,也引入了几个问题:
  • 由于编译时无法进行检测使用是否合法,反射出的东西是否可用,给程序增加了额外的风险。
  • 为了实现反射,程序写起来麻烦,可读性也低
  • 性能下降
很少很少有程序是必须使用反射来实现的,哪怕是目前主要的两中应用:依赖注入框架、代码分析工具,也在逐渐减少反射的使用。
public static void main(String[] args) { // Translate the class name into a Class object Class cl = null; try { cl = (Class) // Unchecked cast! Class.forName(args[0]); } catch (ClassNotFoundException e) { fatalError("Class not found."); } // Get the constructor Constructor cons = null; try { cons = cl.getDeclaredConstructor(); } catch (NoSuchMethodException e) { fatalError("No parameterless constructor"); } // Instantiate the set Set s = null; try { s = cons.newInstance(); } catch (IllegalAccessException e) { fatalError("Constructor not accessible"); } catch (InstantiationException e) { fatalError("Class not instantiable."); } catch (InvocationTargetException e) { fatalError("Constructor threw " + e.getCause()); } catch (ClassCastException e) { fatalError("Class doesn't implement Set"); } // Exercise the set s.addAll(Arrays.asList(args).subList(1, args.length)); System.out.println(s); }private static void fatalError(String msg) { System.err.println(msg); System.exit(1); }

实践66 审慎地使用原生方法(Use native methods judiciously) 在 Java 中,使用JNI可以引入原生方法,这些方法可以是使用 C,C++ 语言编写的。早期,原生方法有三个作用:
  • 能够访问寄存器等平台特有的模块
  • 能够访问原生方法的库和数据
  • 特殊部分的性能优化
但是,随着 Java 的发展,现在几乎用不上了。总之,能不用则不用。
实践67 审慎地优化代码(Optimize judiciously) 对于代码优化,有三条名言。
  • 在计算机领域以优化之名所犯的罪比其他所有原因加起来还要多(并且还不一定达到优化的目的)。[More computing sins are committed in the name of efficiency (without necessarily achieving it) than for any other single reason—including blind stupidity.]
  • 过早的优化是万恶的根源。[We should forget about small efficiencies, say about 97% of the time: premature optimization is the root of all evil.]
  • 关于优化的建议是:1、不要优化。2、在你成为专家,在你有了清晰完美的方案之前,不要优化。[We follow two rules in the matter of optimization: Rule 1. Don’t do it. Rule 2 (for experts only). Don’t do it yet—that is, not until you have a perfectly clear and unoptimized solution.]
我们应致力于编写能用的、好的程序,而不是快的的程序。好的程序都是解耦的,在内部完成逻辑,后续优化大有空间。另外,不优化的意思并不是说在写代码的时候就不考虑性能问题,我们应在初次尽量搭好架构,不要做大手术。
我们应避免引入明显降低性能的代码块。
实践68 坚持使用公认的命名规范(Adhere to generally accepted naming conventions) Effective|Effective Java英文第三版读书笔记(4) -- 通用编程最佳实践
文章图片
image.png 【Effective|Effective Java英文第三版读书笔记(4) -- 通用编程最佳实践】(完)

    推荐阅读