Java解惑(2)-- 字符谜题
11 字符串拼接
static void lastLaugh(){
System.out.println("H" + "a"); //+ 执行字符串拼接 输出Ha
System.out.println('H' + 'a'); //+ 执行加法运算 等价于72+97 输出169
System.out.println("" + 'H' + 'a'); //+ 执行字符串拼接 输出Ha
}
12 字符数组
Java对对象引用的字符串转化定义如下:
如果引用为null,它被转化为字符串"null";
否则转换结果就是对象的toString()方法;
如果toString()方法的结果为null,就用字符串"null"代替
static void ABC() {
String letters = "ABC";
char[] numbers = {'1', '2', '3'};
System.out.println(letters + " easy as " + numbers); //输出诸如 ABC easy as [C@7ea987ac
//要想将一个 char 数组转换成一个字符串,就要调用 String.valueOf(char[])方法
System.out.println(letters + " easy as " + String.valueOf(numbers)); //输出ABC easy as 123
}
上面的代码中,会对char数组调用toString()方法。数组是从Object那里继承的toString()方法,规范中描述道:
返回一个字符串,它包含了该对象所属类的名字,'@'符号,以及表示对象散列码的一个无符号十六进制整数
有关Class.getName的规范描述道:
在char[]类型的类对象上调用该方法的结果为字符串"[C"
连在一起就形成了诸如“[C@7ea987ac”这样的结果
13 动物庄园
static void animalFarm() {
final String pig = "length: 10"; //字符串"length: 10"
final String cat = "length: " + 10; //字符串"length: 10"
final String dog = "length: " + pig.length(); //字符串"length: 10"
System.out.println(pig == cat); //输出true
System.out.println(pig == dog); //输出false
System.out.println("Animals are equal:" + pig == dog); //只输出false
System.out.println("Animals are equal:" + (pig == dog)); //输出Animals are equal:false
}
上面的程序反映了两个问题:
pig和cat是同一个引用,指向同一个字符串,但是pig跟dog是两个引用;因为只有使用常量进行初始化的字符串,才会使用常量池里的同一个引用。
当+表示字符串拼接时,运算优先级和执行加法是一致的,即 "Animals are equal:" + pig == dog等价于("Animals are equal:" + pig) == dog
建议:写代码时不要依赖String的常量池机制,永远把它当做一个对象。除非确实要判断两个对象是否同一个引用,否则永远使用equals()方法比较是否相等。
14 转义字符
Java不会对字符串字面常量里的Unicode转义字符做任何特殊处理,编译器在程序解析成各种符号之前,先将Unicode转义字符转换为它们所表示的字符
static void escapeRout() {
//\u0022是双引号的Unicode转义字符
System.out.println("a\u0022.length()+ \u0022b".length()); //输出2
//上述代码中,括号内的部分等价于 "a".length()+"b".length();
//如果确实想在字符串字面量内部添加双引号,应该使用转移字符序列,转义字符序列是在程序解析成各种符号之后处理的
System.out.println("a\".length() + \"b".length()); //输出16
}
15 令人晕头转向的hello
以下代码无法通过编译,编译器会提示“非法的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
*/
static void test15(){
System.out.println("hell");
System.out.println("o world");
}
问题处在第3行注释中的\units。以反斜杠 \ 并紧跟u开头的会被认为是转义字符的开始,然而后面并没有跟4个十六进制数字,这个Unicode转义字符被认为是病构的,编译器拒绝该程序。
因此即使在注释中,也要注意避免病构的转义字符。
16 Unicode换行
下面的程序无法通过编译,参考上一节,\u000A被当作换行,于是后面的内容就不会被当做注释的部分,。
static void linePrinter(){
// Note: \u000A is Unicode representation of linefeed (LF) char c = 0x000A;
char c = 0x000A;
System.out.println(c);
}
总结14、15、16的教训:使用Unicode转义字符很容易引发混乱,不要使用。
17、18、19略
20 我的类是什么(1)
以下代码希望实现获取当前类的完整类名,并把.替换成/。最后输出"com/javapuzzler/Me.class"
package com.javapuzzler;
public class Me {
public static void main(String[] args) {
String s = Me.class.getName().replaceAll(".","/") + ".class";
System.out.println(s); //输出 //.class
}
}
String.replaceAll的第一个参数接收的是一个正则表达式,而非字符串字面量。正则表达式.可以匹配任意单个字符,因此类名中的每个字符都被替换成了/。
如果想匹配句号,需要在前面添加反斜杠进行转义。又由于反斜杠在字符串字面量中有特殊含义,表示转义字符序列的开始,反斜杠又需要另一个反斜杠转义。
package com.javapuzzler;
public class Me {
public static void main(String[] args) {
String s = Me.class.getName().replaceAll("\\.","/") + ".class";
System.out.println(s); //输出 com/javapuzzler/Me.class
}
}
为了解决这类问题,JDK5提供了一个静态方法java.util.regex.Pattern.quote,它接受一个字符串s作为参数,返回一个字符串s1,s1可用于创建一个与s匹配的模式。
package com.javapuzzler;
public class Me {
public static void main(String[] args) {
String p = Pattern.quote(".");
String s = Me.class.getName().replaceAll(p,"/") + ".class";
System.out.println(s); //输出 com/javapuzzler/Me.class
}
}
该程序的另一个问题是,不是所有文件系统都使用斜杠来分隔文件层次。UNIX系统使用斜杠,而Windows系统使用的是反斜杠,参考谜题21。
21 我的类是什么(2)
以下代码为了兼容不同的操作系统,使用了File.separator来替代斜杠。
public class Me {
public static void main(String[] args) {
String p = Pattern.quote(".");
String s = Me.class.getName().replaceAll(p, File.separator) + ".class";
System.out.println(s);
}
}
但是如果在Windows系统上运行,仍然无法达到预期效果。在Windows系统中,File.separator是个反斜杠。replaceAll方法的第二个参数不是普通的字符串,而是替代字符串。在替代字符串中,反斜杠会被认为是转义字符的开头。
上面的场景可以使用String.replace(CharSequence, CharSequence)方法替代,它做的事情和String.replaceAll相同,但是它将模式和替代字符串都当作字面含义的字符串处理。
public class Me {
public static void main(String[] args) {
String s = Me.class.getName().replace(".", File.separator) + ".class";
System.out.println(s);
}
}
22 URL的愚弄
以下代码可以正常运行,并输出 iexplore::maximize
static void browserTest(){
System.out.print("iexplore:");
http://www.google.com;
System.out.println(":maximize");
}
方法的第2行是一个Java语言中不太常用的特性Labeled Statements。这里http被认为是Labeled Statements的标识符。www.google.com被认为是注释。
建议使用 Labeled Statements 时,要对代码进行正确的格式化。例如
static void browserTest() {
System.out.print("iexplore:");
http:
//www.google.com;
System.out.println(":maximize");
}
23 不劳而获
static void rhymes(){
Random rnd = new Random();
StringBuilder word = null;
switch (rnd.nextInt(2)){
case 1: word = new StringBuilder('P');
case 2: word = new StringBuilder('G');
default: word = new StringBuilder('M');
}
word.append('a').append('i').append('n');
System.out.println(word);
}
我们期望以上程序每次运行,都能以相同的概率输出"Pain","Gain","Main"。但是程序永远只会输出"ain"。这个程序有3个bug。
第一,查看Random.next的方法注释:
the next pseudorandom, uniformly distributed int value between zero (inclusive) and bound (exclusive) from this random number generator's sequence
即返回一个伪随机的、均等分布的int数值,数值范围是0(包括0)到指定数值(不包括)之间。所以rnd.nextInt(2)的值只可能是0,1。
第二,switch语句的每个分支后面漏了break,因此 word = new StringBuilder('M') 总是会被执行。
第三,StringBuilder类并没有接收char类型参数的构造函数,当传入一个char类型参数时,调用的是接收int类型参数的构造函数,即
public StringBuilder(int capacity) {
super(capacity);
}
以下是修改后的程序
static void rhymes(){
Random rnd = new Random();
StringBuilder word = null;
switch (rnd.nextInt(3)){
case 1: word = new StringBuilder("P");break;
case 2: word = new StringBuilder("G");break;
default: word = new StringBuilder("M");
}
word.append('a').append('i').append('n');
System.out.println(word);
}