谜题11:最后的笑声
下面的程序将打印出什么呢?
public class LastLaugh{ public static void main(String[] args){ System.out.print("H"+"a"); System.out.print('H'+'a'); } }
你可能会认为这个程序将打印HaHa。该程序看起来好像是用两种方式连接了H和a,但是你所见为虚。如果你运行这个程序,就会发现它打印的是Ha169。那么,为什么它会产生这样的行为呢?
</td></tr> <tr> <td height="20" colspan="2">
</td>
</tr>
</table>
正如我们所期望的,第一个对System.out.print的调用打印的是Ha:它的参数是表达式”H”+”a”,显然它执行的是一个字符串连接。而第二个对System.out.print的调用就是另外一回事了。问题在于'H'和'a'是字符型字面常量,因为这两个操作数都不是字符串类型的,所以 + 操作符执行的是加法而不是字符串连接。
编译器在计算常量表达式'H'+'a'时,是通过我们熟知的拓宽原始类型转换将两个具有字符型数值的操作数('H'和'a')提升为int数值而实现的。从char到int的拓宽原始类型转换是将16位的char数值零扩展到32位的int。对于'H',char数值是72,而对于'a',char数值是97,因此表达式'H'+'a'等价于int常量72 + 97,或169。
站在语言的立场上,若干个char和字符串的相似之处是虚幻的。语言所关心的是,char是一个无符号16位原始类型整数——仅此而已。对类库来说就不尽如此了,类库包含了许多可以接受char参数,并将其作为Unicode字符处理的方法。
那么你应该怎样将字符连接在一起呢?你可以使用这些类库。例如,你可以使用一个字符串缓冲区:
StringBuffer sb = new StringBuffer(); sb.append(‘H'); sb.append(‘a'); System.out.println(sb);
这么做可以正常运行,但是显得很丑陋。其实我们还是有办法去避免这种方式所产生的拖沓冗长的代码。 你可以通过确保至少有一个操作数为字符串类型,来强制 + 操作符去执行一个字符串连接操作,而不是一个加法操作。这种常见的惯用法用一个空字符串(””)作为一个连接序列的开始,如下所示:
System.out.println(“” + ‘H' + ‘a');
这种惯用法可以确保子表达式都被转型为字符串。尽管这很有用,但是多少有一点难看,而且它自身可能会引发某些混淆。你能猜到下面的语句将会打印出什么吗?如果你不能确定,那么就试一下:
System.out.print("2 + 2 = " + 2+2);
如果使用的是JDK 5.0,你还可以使用
System.out.printf("%c%c", 'H', 'a');
总之,使用字符串连接操作符使用格外小心。+ 操作符当且仅当它的操作数中至少有一个是String类型时,才会执行字符串连接操作;否则,它执行的就是加法。如果要连接的没有一个数值是字符串类型的,那么你可以有几种选择:
这个谜题还包含了一个给语言设计者的教训。操作符重载,即使在Java中只在有限的范围内得到了支持,它仍然会引起混淆。为字符串连接而重载 + 操作符可能就是一个已铸成的错误。
public class ABC{ public static void main(String[] args){ String letters = "ABC"; char[] numbers = {'1', '2', '3'}; System.out.println(letters + " easy as " + numbers); } }可能大家希望这个程序打印出ABC easy as 123。遗憾的是,它没有。如果你运行它,就会发现它打印的是诸如ABC easy as [C@16f0472之类的东西。为什么这个输出会如此丑陋?
尽管char是一个整数类型,但是许多类库都对其进行了特殊处理,因为char数值通常表示的是字符而不是整数。例如,将一个char数值传递给println方法会打印出一个Unicode字符而不是它的数字代码。字符数组受到了相同的特殊处理:println的char[]重载版本会打印出数组所包含的所有字符,而String.valueOf和StringBuffer.append的char[]重载版本的行为也是类似的。
然而,字符串连接操作符在这些方法中没有被定义。该操作符被定义为先对它的两个操作数执行字符串转换,然后将产生的两个字符串连接到一起。对包括数组在内的对象引用的字符串转换定义如下[JLS 15.18.1.1]:
如果引用为null,它将被转换成字符串”null”。否则,该转换的执行就像是不用任何参数调用该引用对象的toString方法一样;但是如果调用toString方法的结果是null,那么就用字符串”null”来代替。
那么,在一个非空char数组上面调用toString方法会产生什么样的行为呢?数组是从Object那里继承的toString方法[JLS 10.7],规范中描述到:“返回一个字符串,它包含了该对象所属类的名字,'@'符号,以及表示对象散列码的一个无符号十六进制整数”[Java-API]。有关Class.getName的规范描述到:在char[]类型的类对象上调用该方法的结果为字符串”[C”。将它们连接到一起就形成了在我们的程序中打印出来的那个丑陋的字符串。
有两种方法可以订正这个程序。你可以在调用字符串连接操作之前,显式地将一个数组转换成一个字符串:
System.out.println(letters + “ easy as ” + String.valueOf(numbers));或者,你可以将System.out.println调用分解为两个调用,以利用println的char[]重载版本:
System.out.print(letters + " easy as "); System.out.println(numbers);请注意,这些订正只有在你调用了valueOf和println方法正确的重载版本的情况下,才能正常运行。换句话说,它们严格依赖于数组引用的编译期类型。
下面的程序说明了这种依赖性。看起来它像是所描述的第二种订正方式的具体实现,但是它产生的输出却与最初的程序所产生的输出一样丑陋,因为它调用的是println的Object重载版本,而不是char[]重载版本。
class ABC2{ public static void main(String[] args){ String letters = "ABC"; Object numbers = new char[] { '1', '2', '3' }; System.out.print(letters + " easy as "); System.out.println(numbers); } }总之,char数组不是字符串。要想将一个char数组转换成一个字符串,就要调用String.valueOf(char[])方法。某些类库中的方法提供了对char数组的类似字符串的支持,通常是提供一个Object版本的重载方法和一个char[]版本的重载方法,而之后后者才能产生我们想要的行为。
对语言设计者的教训是:char[]类型可能应该覆写toString方法,使其返回数组中包含的字符。更一般地讲,数组类型可能都应该覆写toString方法,使其返回数组内容的一个字符串表示。
public class AnimalFarm{ public static void main(String[] args){ final String pig = "length: 10"; final String dog = "length: " + pig.length(); System.out. println("Animals are equal: " + pig == dog); } }对该程序的表面分析可能会认为它应该打印出Animal are equal: true。毕竟,pig和dog都是final的string类型变量,它们都被初始化为字符序列“length: 10”。换句话说,被pig和dog引用的字符串是且永远是彼此相等的。然而,==操作符测试的是这两个对象引用是否正好引用到了相同的对象上。在本例中,它们并非引用到了相同的对象上。
你可能知道String类型的编译期常量是内存限定的。换句话说,任何两个String类型的常量表达式,如果标明的是相同的字符序列,那么它们就用相同的对象引用来表示。如果用常量表达式来初始化pig和dog,那么它们确实会指向相同的对象,但是dog并不是用常量表达式初始化的。既然语言已经对在常量表达式中允许出现的操作作出了限制,而方法调用又不在其中,那么,这个程序就应该打印Animal are equal: false,对吗?
嗯,实际上不对。如果你运行该程序,你就会发现它打印的只是false,并没有其它的任何东西。它没有打印Animal are equal: 。它怎么会不打印这个字符串字面常量呢?毕竟打印它才是正确的呀!谜题11的解谜方案包含了一条暗示:+ 操作符,不论是用作加法还是字符串连接操作,它都比 == 操作符的优先级高。因此,println方法的参数是按照下面的方式计算的:
System.out.println((“Animals are equal: ” + pig) == dog);这个布尔表达式的值当然是false,它正是该程序的所打印的输出。
有一个肯定能够避免此类窘境的方法:在使用字符串连接操作符时,总是将非平凡的操作数用括号括起来。更一般地讲,当你不能确定你是否需要括号时,应该选择稳妥地做法,将它们括起来。如果你在println语句中像下面这样把比较部分括起来,它将产生所期望的输出Animals are equal: false :
System.out.println(“Animals are equal: ” + (pig == dog));可以论证,该程序仍然有问题。
如果可以的话,你的代码不应该依赖于字符串常量的内存限定机制。内存限定机制只是设计用来减少虚拟机内存占有量的,它并不是作为程序员可以使用的一种工具而设计的。就像这个谜题所展示的,哪一个表达式会产生字符串常量并非总是很显而易见。
更糟的是,如果你的代码依赖于内存限定机制实现操作的正确性,那么你就必须仔细地了解哪些域和参数必定是内存限定的。编译器不会帮你去检查这些不变量,因为内存限定的和不限定的字符串使用相同的类型(String)来表示的。这些因在内存中限定字符串失败而导致的bug是非常难以探测到的。
在比较对象引用时,你应该优先使用equals方法而不是 == 操作符,除非你需要比较的是对象的标识而不是对象的值。通过把这个教训应用到我们的程序中,我们给出了下面的println语句,这才是它应该具有的模样。很明显,在用这种方式订正了该程序之后,它将打印出true:
System.out.println("Animals are equal: " + pig.equals(dog));这个谜题对语言设计者来说有两个教训。
public class EscapeRout{ public static void main(String[] args){ // \u0022 是双引号的Unicode转义字符 System.out.println("a\u0022.length() +\u0022b".length()); } }对该程序的一种很肤浅的分析会认为它应该打印出26,因为在由两个双引号“a\u0022.length()+\u0022b”标识的字符串之间总共有26个字符。
稍微深入一点的分析会认为该程序应该打印16,因为两个Unicode转义字符每一个在源文件中都需要用6个字符来表示,但是它们只表示字符串中的一个字符。因此这个字符串应该比它的外表看其来要短10个字符。 如果你运行这个程序,就会发现事情远不是这么回事。它打印的既不是26也不是16,而是2。
理解这个谜题的关键是要知道:Java对在字符串字面常量中的Unicode转义字符没有提供任何特殊处理。编译器在将程序解析成各种符号之前,先将Unicode转义字符转换成为它们所表示的字符[JLS 3.2]。因此,程序中的第一个Unicode转义字符将作为一个单字符字符串字面常量(”a”)的结束引号,而第二个Unicode转义字符将作为另一个单字符字符串字面常量(”b”)的开始引号。程序打印的是表达式”a”.length()+”b”.length(),即2。
如果该程序的作者确实希望得到这种行为,那么下面的语句将要清楚得多:
System.out.println(”a”.length()+”b”.length());更有可能的情况是该作者希望将两个双引号字符置于字符串字面常量的内部。使用Unicode转义字符你是不能实现这一点的,但是你可以使用转义字符序列来实现[JLS 3.10.6]。表示一个双引号的转义字符序列是一个反斜杠后面紧跟着一个双引号(\”)。如果将最初的程序中的Unicode转义字符用转义字符序列来替换,那么它将打印出所期望的16:
System.out.println(”a\”.length()+\”b”.length());许多字符都有相应的转义字符序列,包括单引号(\')、换行(\n)、制表符(\t)和反斜线(\\)。你可以在字符字面常量和字符串字面常量中使用转义字符序列。
实际上,你可以通过使用被称为八进制转义字符的特殊类型的转义字符序列,将任何ASCII字符置于一个字符串字面常量或一个字符字面常量中,但是最好是尽可能地使用普通的转义字符序列。
普通的转义字符序列和八进制转义字符都比Unicode转义字符要好得多,因为与Unicode转义字符不同,转义字符序列是在程序被解析为各种符号之后被处理的。
ASCII是字符集的最小公共特性集,它只有128个字符,但是Unicode有超过65,000个字符。一个Unicode转义字符可以被用来在只使用ASCII字符的程序中插入一个Unicode字符。一个Unicode转义字符精确地等价于它所表示的字符。
Unicode转义字符被设计为用于在程序员需要插入一个不能用源文件字符集表示的字符的情况。它们主要用于将非ASCII字符置于标识符、字符串字面常量、字符字面常量以及注释中。偶尔地,Unicode转义字符也被用来在看起来颇为相似的数个字符中明确地标识其中的某一个,从而增加程序的清晰度。
总之,在字符串和字符字面常量中要优先选择的是转义字符序列,而不是Unicode转义字符。Unicode转义字符可能会因为它们在编译序列中被处理得过早而引起混乱。不要使用Unicode转义字符来表示ASCII字符。在字符串和字符字面常量中,应该使用转义字符序列;对于除这些字面常量之外的情况,应该直接将ASCII字符插入到源文件中。
/** * Generated by the IBM IDL-to-Java compiler, version 1.0 * from F:\TestRoot\apps\a1\units\include\PolicyHome.idl * Wednesday, June 17, 1998 6:44:40 o'clock AM GMT+00:00 */ public class Test{ public static void main(String[] args){ System.out.print("Hell"); System.out.println("o world"); } }这个谜题看起来相当简单。该程序包含了两条语句,第一条打印Hell,而第二条在同一行打印o world,从而将两个字符串有效地连接在了一起。因此,你可能期望该程序打印出Hello world。但是很可惜,你犯了错,实际上,它根本就通不过编译。
问题在于注释的第三行,它包含了字符\units。这些字符以反斜杠(\)以及紧跟着的字母u开头的,而它(\u)表示的是一个Unicode转义字符的开始。遗憾的是,这些字符后面没有紧跟四个十六进制的数字,因此,这个Unicode转义字符是病构的,而编译器则被要求拒绝该程序。Unicode转义字符必须是良构的,即使是出现在注释中也是如此。
在注释中插入一个良构的Unicode转义字符是合法的,但是我们几乎没有什么理由去这么做。程序员有时会在JavaDoc注释中使用Unicode转义字符来在文档中生成特殊的字符。
// Unicode转义字符在JavaDoc注释中有问题的用法 /** * This method calls itself recursively, causing a * StackOverflowError to be thrown. * The algorithm is due to Peter von der Ah\u00E9. */这项技术表示了Unicode转义字符的一种没什么用处的用法。在Javadoc注释中,应该使用HTML实体转义字符来代替Unicode转义字符:
/** * This method calls itself recursively, causing a * StackOverflowError to be thrown. * The algorithm is due to Peter von der Ahé. */前面的两个注释都应该是的在文档中出现的名字为“Peter der Ahé”,但是后一个注释在源文件中还是可理解的。
可能你会感到很诧异,在这个谜题中,问题出在注释这一信息
↑返回目录
前一篇: Java谜题??循环
后一篇: Java谜题??表达式