目录
- 基本数据类型优于包装类
- 当使用其他类型更合适时应避免使用字符串
- 当心字符串连接引起的性能问题
- 通过接口引用对象
- 接口优于反射
- 明智审慎地本地方法
- 明智审慎地进行优化
- 遵守被广泛认可的命名约定
- 只针对异常的情况下才使用异常
- 编程错误使用运行时异常
61. 基本数据类型优于包装类
需要注意自动拆箱、装箱产生的bug
// 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,原因是,i<j自动拆箱,显示不等于,然后再进行两个包装类的地址比较,也不等,所以返回1。
改成如下方法则能修复此bug
Comparator<Integer> naturalOrder = (iBoxed, jBoxed) -> {
int i = iBoxed, j = jBoxed; // Auto-unboxing
return i < j ? -1 : (i == j ? 0 : 1);
};
public class Unbelievable {
static Integer i;
public static void main(String[] args) {
if (i == 42)
System.out.println("Unbelievable");
}
}
这段代码会返回空指针异常,因为Integer i是包装类,没赋值情况下是值为null。
那么,什么时候应该使用包装类型呢?
第一个是作为集合中的元素、键和值
第二是泛型参数
总结
首选基本类型,因为更快,减少自动拆装箱bug的风险。
62. 当使用其他类型更合适时应避免使用字符串
字符串是其他值类型的糟糕替代品
因为转换为需要的类型才能做处理,例如数值的相加。
字符串是枚举类型的糟糕替代品
正如条目 34 中所讨论的,枚举类型常量比字符串更适合于枚举类型常量
字符串是聚合类型的糟糕替代品
// Inappropriate use of string as aggregate type
String compoundKey = className + "#" + i.next()
这个key如果遇到i.next()得到的恰好是“#”,而通过"#"分割解析文本将会出错
字符串不能很好地替代
// Broken - inappropriate use of string as capability!
public class ThreadLocal {
private ThreadLocal() { } // Noninstantiable
// Sets the current thread's value for the named variable.
public static void set(String key, Object value);
// Returns the current thread's value for the named variable.
public static Object get(String key);
}
这段代码的问题是,如果客户端用同一个字符串,那么将会相互覆盖。
再进行优化,可以通过新建一个类来作为键
public class ThreadLocal {
private ThreadLocal() { } // Noninstantiable
public static class Key { // (Capability)
Key() { }
}
// Generates a unique, unforgeable key
public static Key getKey() {
return new Key();
}
public static void set(Key key, Object value);
public static Object get(Key key);
}
还可以进一步改进
public final class ThreadLocal {
public ThreadLocal();
public void set(Object value);
public Object get();
}
但这个不是类型安全的,改为泛型
public final class ThreadLocal<T> {
public ThreadLocal();
public void set(T value);
public T get();
}
这就是最终java.lang.ThreadLocal类
63. 当心字符串连接引起的性能问题
**字符串连接操作符 (+) 是将几个字符串组合成一个字符串的简便方法。对于生成单行输出或构造一个小的、固定大小的对象的字符串表示形式,它是可以的,但是它不能伸缩。使用 字符串串联运算符重复串联 n 个字符串需要n 的平方级时间。**使用StringBuilder则是线性级的,随着数量越来越大,连接操作符 (+) 的性能会越来越差。
64. 通过接口引用对象
如果存在合适的接口类型,那么应该使用接口类型声明参数、返回值、变量和字段。
// Good - uses interface as type
Set<Son> sonSet = new LinkedHashSet<>();
// Bad - uses class as type!
LinkedHashSet<Son> sonSet = new LinkedHashSet<>();
如果你养成了使用接口作为类型的习惯,那么你的程序将更加灵活。
但是要注意替换的类是否能满足要求,例如LinkedHashSet替换成HashSet将失去顺序排序的特性。
那么,为什么要更改实现类型呢?因为第二个实现比原来的实现提供了更好的性能,或者因为它提供了原来的实现所缺乏的理想功能。
**如果没有合适的接口存在,那么用类引用对象是完全合适的。**如 String 和 BigInteger是final类的,不会有修改,所以不需要接口引用。
没有合适接口类型的第二种情况是属于框架的对象,框架的基本类型是类而不是接口,在 java.io 类中许多诸如 OutputStream 之类的就属于这种情况。
如果没有合适的接口,就使用类层次结构中提供所需功能的最底层的类
65. 接口优于反射
**核心反射机制 java.lang.reflect 提供对任意类的编程访问。**类似动态语言。
缺点
失去了编译时类型检查的所有好处。
执行反射访问所需的代码既笨拙又冗长。
性能降低。
对于许多程序,它们必须用到在编译时无法获取的类,在编译时存在一个适当的接口或超类来引用该类(详见第 64 条)。如果是这种情况,可以用反射方式创建实例,并通过它们的接口或超类正常地访问它们。
66. 明智审慎地本地方法
Java 本地接口(JNI)允许 Java 程序调用本地方法,这些方法是用 C 或 C++ 等本地编程语言编写的。
用途:
1、特定于平台的设施(如注册中心)的访问
2、提供对现有本地代码库的访问,包括提供对遗留数据访问
3、本地方法可以通过本地语言编写应用程序中注重性能的部分,以提高性能
使用本地方法有严重的缺点:
1、使用本地方法的应用程序不再能免受内存毁坏错误的影响
2、依赖平台,可移植性较差
3、本地方法可能会降低性能,因为垃圾收集器无法自动跟踪本地内存使用情况
67. 明智审慎地进行优化
优化格言
比起其他任何单一的原因(包括盲目的愚蠢),很多计算上的过失都被归昝于效率(不一定能实现)。
—William A. Wulf [Wulf72]
不要去计较效率上的一些小小的得失,在 97% 的情况下,不成熟的优化才是一切问题的根源。
—Donald E. Knuth [Knuth74]
在优化方面,我们应该遵守两条规则:
规则 1:不要进行优化。
规则 2 (仅针对专家):还是不要进行优化,也就是说,在你还没有绝对清晰的未优化方案之前,请不要进行
优化。
—M. A. Jackson [Jackson75]
努力编写 好的程序,而不是快速的程序
如果一个好的程序不够快,它的架构将允许它被优化(可扩展、阅读性好,容易优化)。但是不是说可以忽略性能,性能不足可以在以后考虑。
尽量避免限制性能的设计决策
例如协议,通讯这些设计在中后期将难以更改
考虑API设计决策的性能结果
api的返回类型是可变的,可能需要大量不必要的防御性复制(详见第 50 条)
在一个公共类中使用继承(在这个类中组合将是合适的)将该类永远绑定到它的超类,这会人为地限制子类的性能(详见第 18 条)
API 中使用实现类而不是接口将你绑定到特定的实现,即使将来可能会编写更快的实现也无法使用(详见第 64 条)。
一旦你仔细地设计了你的程序,成了一个清晰、简洁、结构良好的实现,那么可能是时候考虑优化了,假设此时你还不满意程序的性能。
68. 遵守被广泛认可的命名约定
有利于交流与维护
Package or module | Example |
---|---|
Package or module | org.junit.jupiter.api , com.google.common.collect |
Class or Interface | Stream, FutureTask, LinkedHashMap,HttpClient |
Method or Field | remove, groupingBy, getCrc |
Constant Field | MIN_VALUE, NEGATIVE_INFINITY |
Local Variable | i, denom, houseNum |
Type Parameter | T, E, K, V, X, R, U, V, T1, T2 |
69. 只针对异常的情况下才使用异常
/* Horrible abuse of exceptions. Don't ever do this! */
try {
int i = 0;
while ( true )
range[i++].climb();
} catch ( ArrayIndexOutOfBoundsException e ) {
}
使用这种方式的错误理解:
1、认为trycatch就是用在不正常的情况。
2、把代码放在 try-catch 块中反而阻止了现代 JVM 实现本可能执行的某些特定优化
3、对数据进行遍历的标准模式并不会导致冗余的检查。有些 JVM 实现会将它们优化掉
这段代码的缺点:1、模糊意图,2、降低性能
正确的做法:
1、异常应该只用于异常的情况下;他们永远不应该用于正常的程序控制流程
2、设计良好的 API 不应该强迫它的客户端为了正常的控制流程而使用异常
例如,Iterator接口如果没有hasNext方法,那么将会这么调用
/
* Do not use this hideous code for iteration over a collection! */
try {
Iterator<Foo> i = collection.iterator();
while ( true )
{
Foo foo = i.next();
...
}
} catch ( NoSuchElementException e ) {
}
70. 对可恢复的情况使用受检异常,对编程错误使用运行时异常
Java 程序设计语言提供了三种 throwable:受检异常(checked exceptions)、运行时异常(runtime exceptions)和错误(errors)。
如何选择:
1、如果期望调用者能够合理的恢复程序运行,对于这种情况就应该使用受检异常。(必须要捕获的异常)
2、用运行时异常来表明编程错误,例如客户端没有按照规定调用接口,或者数组越界问题
3、 实现的所有非受检的 throwable 都应该是 RuntimeExceptiond 子类,而不应该是Error的子类
对于可恢复的情况,要抛出受检异常;对于程序错误,就要抛出运行时异常。不确定是否可恢复,就跑出为受检异常。不要定义任何既不是受检异常也不是运行异常的抛出类型。要在受检异常上提供方法,以便协助程序恢复。