Java解惑(二) puzzle 11--23
上一篇记录了10个puzzle,主要是关于表达式的,表达式的一个关键就是有值,所以很多的谜题也都围绕着数据类型展开,今天要分享的是字符之谜,无论什么编程语言,字符总是一个很好玩的好题.在之前也总结过java中String的一些性能上的问题,发现看过这13个puzzle后又加深了一些理解吧。
public class LastLaugh { public static void main(String args[]) { System.out.print("H" + "a"); System.out.print('H' + 'a'); } }
这个问题还是非常简单的,第一行肯定是打印Ha的,但是第二行就不同了,这是两个字符相加,我们知道两个字符相加会被提升到int型的相加,所以它实际上是72和97相加。解决的一个技巧是 System.out.print(""+'H' + 'a'); 这是把其他数据类型转换为string的一个非常快捷的方式。
public class Abc { public static void main(String[] args) { String letters = "ABC"; char[] numbers = { '1', '2', '3' }; System.out.println(letters + " easy as " + numbers); } }
也许研究过String的同学可能认为打印numbers会理所当然的打印出字符串来,因为StringBuilder这些本身也是用字符数组来实现的,但是这个例子打印的结果是ABC easy as [C@2e6e1408 ,可以看出后面是一个对象名。原因是char数组要转换为string的时候要调用其toString方法,这个方法是从Object那里继承来的,所以就打印了上面的结果。但是上面的代码中我们如果直接打印numbers则不会出现这样的问题,原因是System.out.println方法对于字符数组参数进行了重载使得其可以正常打印数组中包含的内容。
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); } }
不想卖关子,这个打印的结果就是 false(可能和大家想的差很多)。这里面有两个陷阱,第一个就是关于字符串初始化的,这个问题反而会迷倒一些对于string有研究的同学,因为pig和dog引用的字符串内容是相同的,== 比较的是引用的对象是不是同一个(C的思想是比较地址),并且根据java string 常量池的特性(一些介绍参考),任何String类型的常量表达式,如果指定的是相同的字符串,那么他们就会指向相同的对象,所以我们认为可能结果就是Animals are equal : true了。其实不然,用==判断pig和dog会得到false。原因就是dog初始化的时候并非是一个常量表达式。忽然发现自己一直忽略了这个问题,惭愧。这个大家可以自己写两行简单的代码测试一下。
那么为什么不会打印“Animals are equal:”呢?原因是操作符优先级的问题,+的优先级高于==,所以这个表达式实际比较的是 “Animals are euqal:length:10”和“length:10”,所以就直接打印了一个false了。这个给我们的启示就是当一个表达式中涉及到多个操作符的时候,我们不确定优先级的时候一定要加括号。System.out.println("Animals are equal: "+ (pig == dog));
public class EscapeRout { public static void main(String[] args) { // \u0022 is the Unicode escape for double-quote (") System.out.println("a\u0022.length() + \u0022b".length()); } }
背景介绍 \u0022 是unicode对于双引号的表示方法。这个程序可能有两种结果一是把打印的内容当成整个字符串打印,而是先把\u0022转义。实际上就是先进行转移操作,这个是编译器解析最前完成的。所以程序就变成了System.out.println("a".length() + "b".length());,。这告诉我们尽量不要用unicode转义字符。
/** * 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"); } }
其实把这段代码直接放在你的eclipse里面就会发现问题了,报错,而其是注释部分。注意第二行注释里面有个\u这有标志了unicode转移字符的开始,但是后面却不是可识别的16进制数,所以导致程序报错。这个好蛋疼,是么?
问题是很多这样的注释是自动生成的,和windows下目录层级用反斜杠表示,这就很容易引发问题。所以哪天注释报错了,那就搜一下\u试试吧。
public class LinePrinter { public static void main(String[] args) { // Note: \u000A is Unicode representation of linefeed (LF) char c = 0x000A; System.out.println(c); } }
这个例子也比较蛋疼,原因还是在注释里面,这个还是和unicode转移有关,事实上,在编译器去掉代码中的空行和注释之前,unicode的已经被替换为转移字符了,而\u000A代表的是换行符,所以我们就可以发现问题了,这个注释会被拆成两行,自然就会报错了,万恶的unicode。
\u0070\u0075\u0062\u006c\u0069\u0063\u0020\u0020\u0020\u0020 \u0063\u006c\u0061\u0073\u0073\u0020\u0055\u0067\u006c\u0079 \u007b\u0070\u0075\u0062\u006c\u0069\u0063\u0020\u0020\u0020 \u0020\u0020\u0020\u0020\u0073\u0074\u0061\u0074\u0069\u0063 \u0076\u006f\u0069\u0064\u0020\u006d\u0061\u0069\u006e\u0028 \u0053\u0074\u0072\u0069\u006e\u0067\u005b\u005d\u0020\u0020 \u0020\u0020\u0020\u0020\u0061\u0072\u0067\u0073\u0029\u007b \u0053\u0079\u0073\u0074\u0065\u006d\u002e\u006f\u0075\u0074 \u002e\u0070\u0072\u0069\u006e\u0074\u006c\u006e\u0028\u0020 \u0022\u0048\u0065\u006c\u006c\u006f\u0020\u0077\u0022\u002b \u0022\u006f\u0072\u006c\u0064\u0022\u0029\u003b\u007d\u007d
actually。。这是一段可执行的代码,作者指向告诉我们,尽。。量。。不。。要。。使。。用。。unicdoe
public class StringCheese { public static void main(String args[]) { byte bytes[] = new byte[256]; for(int i = 0; i < 256; i++) bytes[i] = (byte)i; String str = new String(bytes); for(int i = 0, n = str.length(); i < n; i++) System.out.print((int)str.charAt(i) + " "); } }
这个程序首先将数字转换为byte数组,然后利用byte数组生成一个String,再将string中的每一个字符转成int打印出来,正常的思维我们想看到是0--255这些数字,但实际上是不确定的。问题出在定义一个新的String的时候,由于用bytes定义,并且没有指定字符集,API中提到,数组的长度是字符集的一个函数,所以如果没有指定就出现了不确定的字符长度。
很多开发过J2EE同学应该非常熟悉另一种有byte初始化String的方法,就是传入第二个字符集参数,在网站开发的时候经常用这个来统一编码,特别是中文乱码的问题。
public class Classifier { 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 "; } }
这个例子的问题,其实很同意看出来了,就是块注释语句的第一个/*和代码中的*/进行了匹配,导致整个代码就乱了。这是块注释引起的一个经典的问题,书中还给了我们提示就是块注释是不支持嵌套的。这个puzzle中,作者还提到了一种程序员喜欢使用的注释方法,就是讲一段代码放在if(false){}的block里面,这也容易产生问题,如果不是为了一些调试上的方便也不建议使用。
package com.javapuzzlers; public class Me { public static void main(String[] args) { System.out.println( Me.class.getName().replaceAll(".", "/") + ".class"); } }
这个例子很简单,他的本意是打印出这个类的名字com.javapuzzlers.Me 然后将.替换为/这样就可以获得这个文件的的具体目录了。但是要注意replaceAll的第一个参数是一个正则表达式,”.“在正则里面,相信大家也知道表示匹配任何字符,这样结果就全变成了//////。解决办法有两个,一是写正确的正则表达式,也就是"\\."第二种方法是用Patern的quote方法,直接表示要匹配的内容。
这里面存在一个隐患,及时我们得到想要的结果即com/javapuzzlers/Me.class 那么它也只在unix/linux上有用,windows上的目录是用反斜杠的,所以失效。下面的puzzle就会涉及到这个问题。
package com.javapuzzlers; import java.io.File; public class MeToo { public static void main(String[] args) { System.out.println(MeToo.class.getName(). replaceAll("\\.", File.separator) + ".class"); } }
这应该是上一个问题的修改版,在windows下会出问题,原因就是File.separator是反斜杠,而在这里作为替代参数,和普通字符串不同,他要进行转移,所以就会发生错误。现在的JDK提供了replace方法,更加适合处理简单的情况,两个参数均为普通的字符串,省去了很多的问题。
public class BrowserTest { public static void main(String[] args) { System.out.print("iexplore:"); http://www.google.com; System.out.println(":maximize"); } }
事实上,我刚发现java的这个特性,语句标号。C语言中的goto就用到过语句标号,事实上写到这里的时候,我还是不知道java中语句标号的作用是什么,以及他为什么这么设计。这个例子中,显然http:作为一个标号了。后面跟一行注释,所以代码没有任何问题,完全可以执行。
import java.util.*; public class Rhymes { 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); } }
本章最后一个puzzle是我最喜欢的,里面三个陷阱,我只发现了一个。那就是我们发现每一个case都没有break,所以最后的结果不可能打印Pain和Gain。那么还有两个陷阱,一个就是关于生成随机数的,nextInt的参数设为2只能生成0和1两个随机数,正确的写法是参数为3.
接下来是最好玩的一个陷阱,很有意思,那就是最后结果只能打印ain,很奇怪吧,事实上问题出在StringBuffer的初始化上,StringBuffer没有字符作为参数的构造器,他只有三种构造器一是无参数的,而是接受String的,三是接受int作为初始容量的(初始化容量的详细讨论),所以这里StringBuilder('M'),字符M会被当成int来处理,所以上面的语句相当于给StringBuilder知识初始化了容量而已。这是非常好的一个puzzle,比前面的好玩多了。
Chapter3关注的是字符之谜,其中四个puzzle涉及到了unicode转移字符引起的问题,还有就是char和String之间的一些问题,最后一个例子是受益最深的,尤其是初始化StringBuilder那里,给我们提了醒。不难发现,好的编程习惯能够帮助我们避免很多问题,读puzzle,变得更聪明。下一章是循环之谜,会更好玩。