Q:写一个方法确定传入的值是否是一个奇数
# bad code
public static boolean isOdd(int i){
return i % 2 == 1;
}
# good code
public static boolean isOdd(int i){
return i % 2 != 0;
}
Reason:当 i 是一个负奇数时,i % 2 等于-1 而不是 1, 因此 isOdd 方法将错误地返回 false。
Lesson:无论你何时使用到了取余操作符,都要考虑到操作数和结果的符号。
Q:两个浮点数相减,下面的程序会输出什么呢?
A:0.8999999999999999
# bad code
public class Change{
public static void main(String args[]){
System.out.println(2.00 - 1.10);
}
}
# good code
public class Change1{
public static void main(String args[]){
System.out.println(new BigDecimal("2.00").
subtract(new BigDecimal("1.10")));
}
}
Reason:该程序打印出来的小数,是足以将 double 类型的值与最靠近它的临近值区分出来的最短的小数,它在小数点之前和之后都至少有一位。
Lesson:在需要精确答案的地方,要避免使用 float 和 double;对于货币计算,要使用 int、long 或 BigDecimal。
Q: 被除数表示的是一天里的微秒数;而除数表示的是一天里的毫秒数。这个程序会打印出什么呢?
A:5
# bad code
public class LongDivision{
public static void main(String args[]){
final long MICROS_PER_DAY = 24 * 60 * 60 * 1000 * 1000;
final long MILLIS_PER_DAY = 24 * 60 * 60 * 1000;
System.out.println(MICROS_PER_DAY/MILLIS_PER_DAY);
}
}
# good code
public class LongDivision{
public static void main(String args[ ]){
final long MICROS_PER_DAY = 24L * 60 * 60 * 1000 * 1000;
final long MILLIS_PER_DAY = 24L * 60 * 60 * 1000;
System.out.println(MICROS_PER_DAY/MILLIS_PER_DAY);
}
}
Reason:问题在于常数 MICROS_PER_DAY 的计算“确实”溢出了。尽管计算的结果适合放入 long 中,并且其空间还有富余,但是这个结果并不适合放入 int 中。这个计算完全是以 int 运算来执行的,并且只有在运算完成之后,其结果才被提升到long,而此时已经太迟了:计算已经溢出了,它返回的是一个小了 200 倍的数值。
从 int 提升到 long 是一种拓宽原始类型转换(widening primitive conversion),它保留了(不正确的)数值。这个值之后被 MILLIS_PER_DAY 整除,而MILLIS_PER_DAY 的计算是正确的,因为它适合 int 运算。这样整除的结果就得到了 5。Lesson:当你在操作很大的数字时,千万要提防溢出——它可是一个缄默杀手。即使用来保存结果的变量已显得足够大,也并不意味着要产生结果的计算具有正确的类型。当你拿不准时,就使用 long 运算来执行整个计算
Q: 下面的程序将打印出什么呢?
A:Ha169
# bad code
public class LastLaugh{
public static void main(String[] args){
System.out.print("H"+"a");
System.out.print('H'+'a');
}
}
# good code
public class LongDivision{
public static void main(String args[ ]){
StringBuffer sb = new StringBuffer();
sb.append('H');
sb.append('a');
System.out.println(sb);
}
}
Reason:编译器在计算常量表达式'H'+'a'时,是通过我们熟知的拓宽原始类型转换将两个具有字符型数值的操作数('H'和'a')提升为 int 数值而实现的。从 char 到int 的拓宽原始类型转换是将 16 位的 char 数值零扩展到 32 位的 int。对于'H',char 数值是 72,而对于'a',char 数值是 97,因此表达式'H'+'a'等价于 int常量 72 + 97,或 169。
Lesson:使用字符串连接操作符使用格外小心。+ 操作符当且仅当它的操作数中至少有一个是 String 类型时,才会执行字符串连接操作;否则,它执行的就是加法。
Q: 下面的程序将打印出什么呢?
A:///////////////////.class
# bad code
package com.javapuzzlers;
public class Me {
public static void main(String[] args){
System.out.println(
Me.class.getName().replaceAll(".","/") + ".class");
}
}
# good code
package com.javapuzzlers;
public class Me {
public static void main(String[] args){
System.out.println(
Me.class.getName().replace(".","/") + ".class");
}
}
Reason:问题在于 String.replaceAll 接受了一个正则表达式作为它的第一个参数,而并非接受了一个字符序列字面常量。(正则表达式已经被添加到了 Java 平台的 1.4版本中。)正则表达式“.”可以匹配任何单个的字符,因此,类名中的每一个
字符都被替换成了一个斜杠,进而产生了我们看到的输出。Lesson:在使用不熟悉的类库方法时一定要格外小心。
Q: 下面的程序对一个变量重复地进行增量操作,然后打印它的值。那么这个值是什
么呢?
A:0
# bad code
public class Increment {
public static void main(String[] args) {int j = 0;
for (int i = 0; i < 100; i++)
j = j++;
System.out.println(j);
}
}
# good code
public class Increment {
public static void main(String[] args) {int j = 0;
for (int i = 0; i < 100; i++)
j++;
System.out.println(j);
}
}
Reason:当++操作符被置于一个变量值之后时,其作用就是一个后缀增量操作符:表达式 j++的值等于 j 在执行增量操作
之前的初始值。因此,前面提到的赋值语句首先保存 j 的值,然后将 j 设置为其值加 1,最后将 j 复位到它的初始值。换句话说,这个赋值操作等价于下面的语句序列:int tmp = j;
j = j + 1;
j = tmp?;Lesson:不要在单个的表达式中对相同的变量赋值超过一次
Q: 下面的程序计算了一个循环的迭代次数,并且在该循环终止时将这个计数值打印
了出来。那么,它打印的是什么呢?
A:什么都不会打印
# bad code
public class InTheLoop {
public static final int END = Integer.MAX_VALUE;
public static final int START = END - 100;public static void main(String[] args) {
int count = 0;
for (int i = START; i <= END; i++)
count++;
System.out.println(count);
}
}
# good code
public class InTheLoop {
public static final int END = Integer.MAX_VALUE;
public static final int START = END - 100;public static void main(String[] args) {
int count = 0;
for (long i = START; i <= END; i++)
count++;
System.out.println(count);
}
}
Reason:问题在于这个循环会在循环索引(i)小于或等于 Integer.MAX_VALUE 时持续运行,但是所有的 int 变量都是小于或等于 Integer.MAX_VALUE 的。因为它被定义为所有 int 数值中的最大值。当 i 达到 Integer.MAX_VALUE,并且再次被执行增量操作时,它就有绕回到了 Integer.MIN_VALUE。
Lesson:int 不能表示所有的整数。无论你在何时使用了一个整数类型,都要意识到其边界条件。如果其数值下溢或是上溢了,会怎么样呢?所以通常最好是使用一个取之范围更大的类型。(整数类型包括 byte、char、short、int 和 long。)
Q: 下面的程序打印的是什么呢?
A:false
# bad code
public class Indecisive {
public static void main(String[] args) {
System.out.println(decision());
}
static boolean decision() {
try {
return true;
} finally {
return false;
}
}
}
Lesson:每一个 finally 语句块都应该正常结束,除非抛出的是不受检查的异常。千万不要用一个 return、break、continue 或 throw 来退出一个 finally 语句块,并且千万不要允许将一个受检查的异常传播到一个 finally 语句块之外去。
Q: 下面的程序将分别打印的是什么呢?
A:第一个程序,Arcane1,展示了被检查异常的一个基本原则。它看起来应该是可以编译的:try 子句执行 I/O,并且 catch 子句捕获 IOException 异常。但是这个程序不能编译,因为 println 方法没有声明会抛出任何被检查异常,而
IOException 却正是一个被检查异常。语言规范中描述道:如果一个 catch 子句要捕获一个类型为 E 的被检查异常,而其相对应的 try 子句不能抛出 E 的某种子类型的异常,那么这就是一个编译期错误。换句话说Try语句中并没有执行跟IO相关的操作,自然不会产生IOException异常的可能性;这样的话将会在编译时报错。
第二个程序,Arcane2 ,看起来应该是不可以编译的,但是它却可以。它之所以可以编译,是因为它唯一的 catch 子句检查了 Exception。捕获 Exception 或 Throwble 的 catch 子句是合法的,不管与其相对应的 try 子句的内容为何。尽管 Arcane2 是一个合法的程序,但是 catch 子句的内容永远的不会被执行,这个程序什么都不会打印。
第三个程序,Arcane3,看起来它也不能编译。方法 f 在 Type1 接口中声明要抛出被检查异常 CloneNotSupportedException,并且在 Type2 接口中声明要抛出被检查异常 InterruptedException。Type3 接口继承了 Type1 和 Type2,因此,看起来在静态类型为 Type3 的对象上调用方法 f时,有潜在可能会抛出这些异常。一个方法必须要么捕获其方法体可以抛出的所有被检查异常,要么声明它将抛出这些异常。换言之,catch中要么是Exception,要么是具体的Exception类型。然而该程序是可编译的。
一个方法可以抛出的被检查异常集合是它所适用的所有类型声明要抛出的被检查异常集合的交集,而不是合集。
因此,静态类型为 Type3 的对象上的 f 方法根本就不能抛出任何被检查异常。因此,Arcane3可以毫无错误地通过编译,并且打印 Hello world。
# bad code
import java.io.IOException;
public class Arcane1 {
public static void main(String[] args) {
try {
System.out.println("Hello world");
} catch(IOException e) {
System.out.println("I've never seen
println fail!");
}
}
}
public class Arcane2 {
public static void main(String[] args) {
try {
// If you have nothing nice to say, say nothing
} catch(Exception e) {
System.out.println("This can't happen");
}
}
}
interface Type1 {
void f() throws CloneNotSupportedException;
}
interface Type2 {
void f() throws InterruptedException;
}
interface Type3 extends Type1, Type2 {
}
public class Arcane3 implements Type3 {
public void f() {
System.out.println("Hello world");
}
public static void main(String[] args) {
Type3 t3 = new Arcane3();
t3.f();
}
}
Lesson:
第一个程序说明了对于捕获被检查异常的 catch 子句,只有在相应的 try 子句可以抛出这些异常时才被允许。
第三个程序说明了多个继承而来的 throws 子句的交集,将减少而不是增加方法允许抛出的异常数量。
Q: 下面的程序打印的是什么呢?
A:编译出错
# bad code
public class UnwelcomeGuest {
public static final long GUEST_USER_ID = -1;
private static final long USER_ID;
static {
try {
USER_ID = getUserIdFromEnvironment();
} catch (IdUnavailableException e) {
USER_ID = GUEST_USER_ID;
System.out.println("Logging in as guest");
}
}
private static long getUserIdFromEnvironment() throws IdUnavailableException {
throw new IdUnavailableException();
}
public static void main(String[] args) {
System.out.println("User ID: " + USER_ID);
}
}
class IdUnavailableException extends Exception {
}
# good code
public class UnwelcomeGuest {
public static final long GUEST_USER_ID = -1;
private static final long USER_ID = getUserIdOrGuest();
private static long getUserIdOrGuest() {
try {
return getUserIdFromEnvironment();
} catch (IdUnavailableException e) {
System.out.println("Logging in as guest");
return GUEST_USER_ID;
}
}
private static long getUserIdFromEnvironment() throws IdUnavailableException {
throw new IdUnavailableException();
}
public static void main(String[] args) {
System.out.println("User ID: " + USER_ID);
}
}
class IdUnavailableException extends Exception {
}
Reason:USER_ID 域是一个空 final(blank final),它是一个在声明中没有进行初始化操作的 final 域。很明显,只有在对 USER_ID 赋值失败时,才会在 try 语句块中抛出异常,因此,在 catch 语句块中赋值是相当安全的。不管怎样执行静态初始化操作语句块,只会对 USER_ID 赋值一次,这正是空 final 所要求的。
Lesson:解决这类问题的最好方式就是将这个烦人的域从空 final 类型改变为普通的final 类型,用一个静态域的初始化操作替换掉静态的初始化语句块。实现这一点的最佳方式是重构静态语句块中的代码为一个助手方法。
Q: 下面的程序打印的是什么呢?
A:Hello world
# bad code
public class HelloGoodbye {
public static void main(String[] args) {
try {
System.out.println("Hello world");
System.exit(0);
} finally {
System.out.println("Goodbye world");
}
}
}
Reason:不论 try 语句块的执行是正常地还是意外地结束,finally 语句块确实都会执行。然而在这个程序中,try 语句块根本就没有结束其执行过程。System.exit 方法将停止当前线程和所有其他当场死亡的线程。finally 子句的出现并不能给予线
程继续去执行的特殊权限。Lesson:System.exit 将立即停止所有的程序线程,它并不会使 finally 语句块得到调用,但是它在停止 VM 之前会执行关闭挂钩操作。当 VM 被关闭时,请使用关闭挂钩来终止外部资源。
Q: 下面的程序打印的是什么呢?
A:抛出了 StackOverflowError 异常
# bad code
public class Reluctant {
private Reluctant internalInstance = new Reluctant();
public Reluctant() throws Exception {
throw new Exception("I'm not coming out");
}
public static void main(String[] args) {
try {
Reluctant b = new Reluctant();
System.out.println("Surprise!");
} catch (Exception ex) {
System.out.println("I told you so");
}
}
}
Reason:当你调用一个构造器时,实例变量的初始化操作将先于构造器的程序体而运行[JLS 12.5]。在本谜题中, internalInstance 变量的初始化操作递归调用了构造器,而该构造器通过再次调用 Reluctant 构造器而初始化该变量自己的
internalInstance 域,如此无限递归下去。Lesson:总之,实例初始化操作是先于构造器的程序体而运行的。实例初始化操作抛出的任何异常都会传播给构造器。如果初始化操作抛出的是被检查异常,那么构造器必须声明也会抛出这些异常,但是应该避免这样做,因为它会造成混乱。最后,对于我们所设计的类,如果其实例包含同样属于这个类的其他实例,那么对这种无限递归要格外当心。