写在前面
《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;
}
- 如果n是2的平方切值较小,则不久就会形成序列循环
- 如果n不是2的平方,则返回某些数的概率会大于另一些数
- 极端情况下,返回的值甚至可能超出限定范围,这是处理正负值时可能引起的
实质上,完全可以使用库里面的 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
,将抛出异常。
注意,有几种情况必须使用封装基础类型,除此之外,应尽量使用基础类型。
- 集合的对象不能使用基础类型
- 泛型参数不能使用基础类型
- 反射调用不能使用基础类型
实践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 extends Set> cl = null;
try {
cl =
(Class extends Set>)
// Unchecked cast!
Class.forName(args[0]);
} catch (ClassNotFoundException e) {
fatalError("Class not found.");
}
// Get the constructor
Constructor extends Set> 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)
(完)