准则一 将局部变量的作用域最小化
- 不要在变量使用之前就申明,在需要使用的时候进行申明。当然这条准则不是那么绝对,大部分时候遵守就好。
- 每个局部变量声明都应该包含一个初始化表达式。 如果你还没有足够的信息来合理地初始化一个变量,你应该推迟声明,直到条件满足。这个规则的一个例外是 try-catch 语句。
- 遍历集合的首选习惯用法是foreach
for (Element e : c) {
}
如果要访问迭代器,书中更推荐使用,传统的for循环:
for (Iterator<Element> i = c.iterator(); i.hasNext(); ) {
Element e = i.next();
}
为什么呢?其实也是最小化局部变量的应用,可以避免bug发生:
Iterator<Element> i = c.iterator();
while (i.hasNext()) {
doSomething(i.next());
}
Iterator<Element> i2 = c2.iterator();
while (i.hasNext()) {
doSomethingElse(i2.next());
}
我们可以看到第二个while循环引用了上面的变量I但是我们实际上的期望是引用i2所以不符合预期,产生bug,如果我们使用传统的for,我们引用不到i这个变量。
准则二 for-each 循环优于传统的 for 循环
书中之所以推荐使用for-each,是因为可以减少错误的发生,例如:
List<Card> deck = new ArrayList<>();
for (Iterator<Suit> i = suits.iterator(); i.hasNext(); )
for (Iterator<Rank> j = ranks.iterator(); j.hasNext(); )
deck.add(new Card(i.next(), j.next()));
就比如上面的实例方法会抛出异常NoSuchElementException,原因就在于外层循环一次以后,里面的迭代器已经到了末尾。如果这样写是不是完全可以避免这个问题。
for (Suit i : suits) {
for (Rank j : ranks) {
}
}
for-each : 三个不易使用的场景
- 破坏性过滤,如果需要遍历一个集合并删除选定元素,则需要使用显式的迭代器,以便调用其 remove 方法。
- 转换,如果需要遍历一个 List 或数组并替换其中部分或全部元素的值,那么需要 List 迭代器或数组索引来替换元素的值。
- 并行迭代,如果需要并行遍历多个集合,那么需要显式地控制迭代器或索引变量,以便所有迭代器或索引变量都可以同步执行。
准则三 通用程序设计
这个没什么好说的,我们尽量使用已有的库。首先来说已有的库经过大量人的验证很少有BUG,边界情况考虑的更加周全。其次是我们使用现成的库可以提高开发效率。
准则四 若需要精确答案就应避免使用 float 和 double 类型
浮点数在计算机中的表示不是完全精确的,因为它们使用了有限长度的二进制来表示数值。这意味着有些十进制小数无法被精确表示为二进制形式,从而导致精度损失。
这个其实在阿里巴巴开发手册也有提到如果是金融业务最好使用BigDecimal。
BigDecimal它与原始算术类型相比很不方便,而且速度要慢得多。但是比起钱的运算不能出错更重要。
对比
在 Java 中,float 和 double 类型用于表示浮点数,它们在计算机内部是以二进制形式存储的。使用 float 和 double 类型时,需要注意以下几点,尤其是当需要进行精确计算时:
- 有限精度:
浮点数在计算机中的表示不是完全精确的,因为它们使用了有限长度的二进制来表示数值。这意味着有些十进制小数无法被精确表示为二进制形式,从而导致精度损失。 - 舍入误差:
在进行浮点数运算时,由于浮点数的有限精度,可能会产生舍入误差。即使是简单的加减法也可能导致结果不精确。 - 比较问题:
直接比较两个浮点数是否相等通常是不可靠的,因为即使是非常接近的数值也可能由于舍入误差而被认为是不相等的。 - 表示范围限制:
float 和 double 类型虽然可以表示非常大的数值,但它们也有各自的表示范围限制。超出这个范围的数值会导致溢出或下溢。
BigDecimal 能够避免精度问题的原因在于它的设计和实现方式。以下是几个关键点:
- 可变精度:
BigDecimal 允许用户指定任意精度来进行数学运算。这意味着用户可以控制小数点后保留多少位数字,从而避免了舍入误差。
基于十进制表示:
BigDecimal 使用十进制表示数值,这与人类习惯的计数方式一致。这意味着可以直接表示常见的分数,如 0.1、0.2 等,而不会出现二进制浮点数表示时的精度损失。 - 内部结构:
BigDecimal 类内部使用两个字段来表示一个数:unscaledValue 和 scale。
unscaledValue 是一个 BigInteger 对象,表示数值的未缩放部分。
scale 是一个整数,表示小数点的位置。例如,对于 0.123,unscaledValue 为 123,scale 为 3。 - 精确的算术运算:
BigDecimal 提供了一系列方法来进行精确的算术运算,包括加法、减法、乘法、除法等。这些方法在执行运算时会考虑到精度和舍入模式,以确保结果的准确性。
舍入模式:
BigDecimal 支持多种舍入模式,如 ROUND_HALF_UP、ROUND_HALF_DOWN、ROUND_HALF_EVEN 等,这允许用户根据需求选择合适的舍入策略。 - 避免溢出:
BigDecimal 使用 BigInteger 来存储数值,这意味着它可以表示非常大或非常小的数,而不会像 float 和 double 那样容易发生溢出或下溢。
准则五 基本数据类型优于包装类
基本类型和包装类型之间有三个主要区别。
首先,基本类型只有它们的值,而包装类型具有与其值不同的标识。换句话说,两个包装类型实例可以具有相同的值和不同的标识。
第二,基本类型只有全功能值,而每个包装类型除了对应的基本类型的所有功能值外,还有一个非功能值,即 null。
最后,基本类型比包装类型更节省时间和空间。
// Broken comparator - can you spot the flaw?
Comparator<Integer> naturalOrder =(i, j) -> (i < j) ? -1 : (i == j ? 0 : 1);
上面这段程序的问题在于在包装类型上使用了==应该使用equals方法。如果要修复这个问题:
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");
}
}
当如果是IDE,上面的代码不会通过编译,如果通过编译呢,结果就是会空指针,为什么呢?看下面这个字节码:
可以看到,原因就是和数字比较时候自动拆箱了,相当于调用了null.initValue();
同样的在前面我们也提到了一段代码:
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),它是包装类型 Long,而不是基本类型 long。程序在没有错误或警告的情况下编译,变量被反复装箱和拆箱,导致产生明显的性能下降。
什么时候使用包装类型呢?如果是使用泛型毫无疑问一定要使用包装类型。
准则六 其他类型更合适时应避免使用字符串
也就是说,数据类型我们最好是转换成对应的类型,不要都用字符串类替代。
准则七 当心字符串连接引起的性能问题
不要使用字符串连接操作符合并多个字符串,除非性能无关紧要。否则使用 StringBuilder 的 append 方法。其本质就是避免创建不必要的对象 。
准则八 通过接口引用对象
如果存在合适的接口类型,那么应该使用接口类型声明参数、返回值、变量和字段。
也就是依赖于更抽象,这样我们可以将多态这个特性发挥的更有力,提高代码的通用性和灵活性。
- 如果没有合适的接口存在,那么用类引用对象是完全合适的。
- 属于框架的对象,框架的基本类型是类而不是接口。但是这样的对象往往是一个抽象类。
- 如果没有合适的接口,就使用类层次结构中提供所需功能的最底层的类,也就是引用最顶层的,因为里氏替换原则父类出现的地方子类一定可以出现。
准则九 接口优于反射
通过非常有限的形式使用反射,你可以获得反射的许多好处,同时花费的代价很少。 对于许多程序,它们必须用到在编译时无法获取的类,在编译时存在一个适当的接口或超类来引用该类(Item-64)。如果是这种情况,可以用反射方式创建实例,并通过它们的接口或超类正常地访问它们。
// Translate the class name into a Class object
Class<? extends Set<String>> cl = null;
try {
cl = (Class<? extends Set<String>>) // Unchecked cast!
Class.forName(args[0]);
} catch (ClassNotFoundException e) {
fatalError("Class not found.");
}
// Get the constructor
Constructor<? extends Set<String>> cons = null;
try {
cons = cl.getDeclaredConstructor();
} catch (NoSuchMethodException e) {
fatalError("No parameterless constructor");
}
// Instantiate the set
Set<String> 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);
书中这个例子,是一个接口,但是具体的实例却是由反射进行创建的,并且类的名称是通过参数类确定了,这就是反射结合接口的最佳实践。同时这个实践是我们不确定类型的时候,也就是说如果我们确定了类型,最好是使用接口直接引用,因为反射有自己的缺陷。
准则十 明智地使用本地方法
本地方法之前要三思。一般很少需要使用它们来提高性能。如果必须使用本地方法来访问底层资源或本地库,请尽可能少地使用本地代码,并对其进行彻底的测试。本地代码中的一个错误就可以破坏整个应用程序。
准则十一 明智地进行优化
规则 1:不要进行优化。
规则 2 (仅针对专家):还是不要进行优化,也就是说,在你还没有绝对清晰的未优化方案之前,请不要进行优化。
努力编写 好的程序,而不是快速的程序。为了获得良好的性能而改变 API 是一个非常糟糕的想法。
准则十二 遵守被广泛认可的命名约定
这个准则其实就是说命名规范,这个可以多看源码,参考源码去命名。