19 单行注释
public static void main(String[] args) {
System.out.println(classify('n') + classify('+') + classify('2'));
}
static String classify(char ch) {
if ("0123456789".indexOf(ch) >= 0)
return "NUMERAL ";
if ("abcdefghijklmnopqrstuvwxyz".indexOf(ch) >= 0)
return "LETTER ";
/* (Operators not supported yet)
if ("+-*/&|!=".indexOf(ch) >= 0)
return "OPERATOR ";
*/
return "UNKNOWN ";
}
编译出错,块注释不能嵌套,在注释内的文本都不会被特殊对待。
// Code commented out with an if statement - doesn't always work!
if (false) {
/* Add the numbers from 1 to n */
int sum = 0;
for (int i = 1; i <= n; i++)
sum += i;
}
这是语言规范推荐的一种条件编译的技术,但不是非常适合注释代码。除非包含的语句都是有效的表达式,否则这种条件编译不能用作注释。最好的注释代码方法是用单行注释。
20 反斜杠
Me.class.getName() 返回的是Me类的完整名,如"com.javapuzzlers.Me"。
System.out.println( Me.class.getName().replaceAll(".", "/") + ".class");
应该得到com/javapuzzlers/Me.class?不对。问题出在String.replaceAll把正则表达式作为第一个参数,而不是字符。正则表达是“.”表示配对任何单独的字符,所以类名的每一个字符都被斜线替代。为了只匹配句号,必须用反斜线(\)转义。因为反斜线在字符串中有特殊意义——它是escape sequence的开始——反斜线自身也必须用一个反斜线转义。
正确:System.out.println( Me.class.getName().replaceAll("\\.", "/") + ".class");
为了解决这类问题,java 5提供了一个新的静态方法java.util.regex.Pattern.quote。用一个字符串作为参数,增加任何需要的转义,返回一个和输入字符串完全匹配的正则表达式字符串:
System.out.println(Me.class.getName().replaceAll(Pattern.quote("."), "/") + ".class");
这个程序的另外一问题就是依赖于平台。不是所有的文件系统都是用斜线来组织文件。为了在你运行的平台取得正确的文件名,你必须使用正确的平台分隔符来替换斜线。
21
System.out.println(MeToo.class.getName().
replaceAll("\\.", File.separator) + ".class");
java.io.File.separator 是一个公共的String 属性,指定用来包含平台依赖的文件名分隔符。在UNIX上运行打印com/javapuzzlers/MeToo.class。然而,在Windows上程序抛出异常:
StringIndexOutOfBoundsException: String index out of range: 1
结果是String.replaceAll 的第二个参数不是普通字符串而是一个在java.util.regex 规范中定义的 replacement string,反斜线转义了后面的字符。当在Windows上运行的时候,替换字符是一个单独的反斜线,无效。JAVA 5提供了两个新方法来解决这个问题,一个是java.util.regex.Matcher.quoteReplacement,它替换字符串为相应的替换字符串:
System.out.println(MeToo.class.getName().replaceAll(
"\\.", Matcher.quoteReplacement(File.separator))+".class");
第二个方法提供了更好的解决方法。String.replace(CharSequence, CharSequence)和String.replaceAll做同样的事情,但他把两个参数都作为字符串处理:System.out.println(MeToo.class.getName().replace(".", File.separator) + ".class");
如果用的是java早期版本就没有简单的方法产生替换字符串。完全不用正则表达式,使用String.replace(char, char)跟容易一些:
System.out.println(MeToo.class.getName().replace('.', File.separatorChar) + ".class");
教训:当用不熟悉的库方法的时候,小心点。有怀疑的话,查看Javadoc。当然正则表达式也很棘手:他编译时可能没问题运行时却更容易出错。
22 statement label
认真写注释,及时更新。去掉无用代码。如果有东西看起来奇怪不真实,很有可能是错误的。
23
private static Random rnd = new Random();
public static void main(String[] args) {
StringBuffer word = null;
switch(rnd.nextInt(2)) {
case 1: word = new StringBuffer('P');
case 2: word = new StringBuffer('G');
default: word = new StringBuffer('M');
}
word.append('a');
word.append('i');
word.append('n');
System.out.println(word);
}
在一次又一次的运行中,以相等的概率打印出Pain,Gain或 Main?答案它总是在打印ain。一共有三个bug导致这种情况。
一是 Random.nextInt(int) ,看规范可知这里返回的是0到int值之间的前闭后开区间的随机数。因此程序中永远不会返回2。这是一个相当常见的问题源,被熟知为“栅栏柱错误(fencepost error)”。这个名字来源于对下面这个问题最常见的但却是错误的答案,如果你要建造一个100英尺长的栅栏,其栅栏柱间隔为10英尺,那么你需要多少根栅栏柱呢?11根或9根都是正确答案,这取决于是否要在栅栏的两端树立栅栏柱,但是10根却是错误的。要当心栅栏柱错误,每当你在处理长度、范围或模数的时候,都要仔细确定其端点是否应该被包括在内,并且要确保你的代码的行为要与其相对应。
第二个bug是 case没有配套的break。从5.0版本起,javac提供了-Xlint:fallthrough标志,当你忘记在一个case与下一个case之间添加break语句是,它可以生成警告信息。不要从一个非空的case向下进入了另一个case。这是一种拙劣的风格,因为它并不常用,因此会误导读者。十次中有九次它都会包含错误。如果Java不是模仿C建模的,那么它倒是有可能不需要break。对语言设计者的教训是:应该考虑提供一个结构化的switch语句。
最后一个,也是最微妙的一个bug是表达式new StringBuffer(‘M')可能没有做哪些你希望它做的事情。StringBuffer(char)构造器根本不存在。StringBuffer有一个无参数的构造器,一个接受一个String作为字符串缓冲区初始内容的构造器,以及一个接受一个int作为缓冲区初始容量的构造器。在本例中,编译器会选择接受int的构造器,通过拓宽原始类型转换把字符数值'M'转换为一个int数值77[JLS 5.1.2]。换句话说,new StringBuffer(‘M')返回的是一个具有初始容量77的空的字符串缓冲区。该程序余下的部分将字符a、i和n添加到了这个空字符串缓冲区中,并打印出该字符串缓冲区那总是ain的内容。 为了避免这类问题,不管在什么时候,都要尽可能使用熟悉的惯用法和API。如果你必须使用不熟悉的API,那么请仔细阅读其文档。在本例中,程序应该使用常用的接受一个String的StringBuffer构造器。