一: 《Java Puzzlers》(中文译名《Java解惑》),一句话,好书,有趣,短小精悍的行文,主要是介绍JAVA编程一些必须注意小心的陷阱(trap)。第一章:表达式之谜,俺只记下一些有趣的例子,很多关于16进制的趣事不多说,反正尽量小心使用16进制。1。对于精确答案的地方,要尽量避免使用float和double;对于货币计算,要使用int,long和BigDecimal,并且使用BigDecimal时千万不要使用BigDecimal(double)构造函数。,因为此构造函数将用它的参数的精确值来构建一个实例。
如:
public class Change {
public static void main(String args[]) {
System.out.println(2.00 - 1.10);
}
}
我们希望打印0.9,可是打印的却是0.899999999999999,货币计算最好还是采用BigDecimal:
public class Change {
public static void main(String args[]) {
System.out.println(new BigDecimal("2.00").subtract(new BigDecimal(" 1.10")));
}
}
2。长整除:当你操作很大的数字时,千万要提防溢出:
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);
}
}
你猜测打印什么?1000?不,打印的是5。因为第一个MICRO_PER_DAY的计算完全是以int类型来进行,虽然计算结果是可以放进long的,但在放进之前已经溢出了。改为如此,显示地转为long:
final long MICROS_PER_DAY = 24L * 60 * 60 * 1000 * 1000;
3。基本类型的转型操作:
public class Multicast {
public static void main(String[] args) {
System.out.println((int) (char) (byte) -1);
}
}
输出可能让你很吃惊:65535。其实问题的关键是JAVA不显式区分有符号数与无符号数。在执行char的转换时,一条规则必须记住,如果最初的数值类型是有符号的,那么就执行符号扩展;如果它是char,那么不管它转化成什么类型,都执行零扩展。因为byte是有符号的类型,所以在-1转化为char时,发生符号扩展,作为结果的char的16位都被置位了,所以结果为2的16次方再加上-1。
4。使用条件操作符最好是使用类型相同的第二和第三操作数。请猜测下下面这个例子的输出结果:
public class DosEquis {
public static void main(String[] args) {
char x = 'X';
int i = 0;
System.out.print(true ? x : 0);
System.out.print(false ? i : x);
}
}
你可能以为应该打印XX,可并非如此,结果是X88,why?这与条件操作符的规则有关:
A。如果第2和第三操作数的类型相同,那么它也是条件操作符结果的类型
B。如果一个操作数的类型是T(T包括byte,short,char)时,另一个操作数的类型是int的常量(注意是常量)表达式,并且可以用T类型表示,那么条件表达式的结果为T
C。否则,将对表达式的操作数进行二进制提升,结果即为提升后的类型。
因此第2个输出语句把X转为int型即88。很复杂对不?呵呵,所以还是最好使用相同类型吧!!
5。另一个需要注意的是复合操作符,如+=,*=,/+等,复合赋值表达式将自动把所执行计算的结果转化为其左侧变量的类型。所以要防止窄化转型。
二:Java Puzzlers(2)字符串之谜
来个两个有趣的例子:
1。URL的愚弄,难道SUN对google的待遇很特殊??
public class BrowserTest {
public static void main(String[] args) {
System.out.print("iexplore:");
http://www.google.com;
System.out.println(":maximize");
}
}
上面这个例子能运行吗?你确定吗?是的,我确定,这个例子可以完全正常地运行。为什么?这个例子等价于:
public class BrowserTest {
public static void main(String[] args) {
System.out.print("iexplore:");
http:
//www.google.com;
System.out.println(":maximize");
}
}
哈哈,http只是作为标签(label),因为JAVA没有goto语句,它是通过label和continue语句来模拟的。而//www.google.com;只是注释。我第一次看见这个例子的时候也被搞混了。
2。完全用UNICODE表示的程序:请把下面这段“代码”放进一个Ugly.java的文件中,编译运行即可,输出老掉牙的Hello World。
/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
哈哈,是好玩,可苦了看程序的人,所以尽量避免使用转义符吧。
3。看看下面这个例子,打印什么?
public class LastLaugh {
public static void main(String args[]) {
System.out.print("H" + "a");
System.out.print('H' + 'a');
}
}
哈?下半声我笑不出来了,打印出来的是Ha169。道理很简单,+号只对String 类型做了重载,而char类型没有,所以第2个输出语句其实是把char转为int,再进行+操作,即72+97。解决此问题可以在前面加个空字符串:
System.out.print(""+'H' + 'a');
或者JDK5中使用printf,System.out.printf("%c%c",'H','a');
4。字符串奶酪:
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) + " ");
}
}
你可能以为会打印从0到255,可惜结果却不一定,这依赖于你的平台的缺省字符集。我的机器上默认是GBK,这个程序只有在字符集是ISO-8859-1时才可以,所以可以改成:
String str = new String(bytes,"ISO-8859-1");
平台的默认字符串在JDK5中使用java.nio.charset.Charset.defaultCharset()来查询。
5。另一个需要注意的是String的replaceAll的方法第一个参数是正则表达式,而不是常量字符串。你可以使用String的replace方法来替代。
三:
1:令人混淆的构造器案例,猜测一下,下面的程序打印什么?
public class Confusing {
private Confusing(Object o) {
System.out.println("Object");
}
private Confusing(double[] dArray) {
System.out.println("double array");
}
public static void main(String[] args) {
new Confusing(null);
}
}
你可能认为应该随机调用这两个重载函数中的某一个,不幸的是你会发现一直重复输出的是:double array。证明只有第二个函数被调用。为何?这跟java对重载函数的解析有关,JAVA对重载函数的解析分为两步,首先查找出所有可获得的并且可应用的构造器或者方法,第2步从这些方法中寻找出适合“最精确”的。在本例中,第1个函数能接受的参数范围大于第二个函数,所以对于null来说,第二个函数更为“精确”。这就告诉我们一点,在撰写重载函数时,请确保所有重载版本所接受的参数类型尽量做到不兼容!
2。Null与Void,很有趣的例子:
public class Null {
public static void greet() {
System.out.println("Hello world!");
}
public static void main(String[] args) {
((Null) null).greet();
}
}
这个能编译通过吗?你可能要问,呵呵,确实可以,打印经典的Hello World。本质上,((Null)null).greet();等价于Null.greet();,因为greet是static method,即类方法,它与具体的对象实例无关!虽然你在此指定了表达式null,但此值将被忽略。这样的写法容易造成混淆,在调用类方法(静态方法时)请尽量使用类名来调用!
3。此章中有几个例子详细讨论了类中的变量初始化顺序问题。需要避免几个种情况:
A。要当心类的初始化顺序,特别是在你无法确定的情况下,请编写适当的测试。
B。不要在构造函数中调用可能被覆写的方法。因为这些方法可能在子类中被覆写,导致不可预知的行为。
C。对静态变量的初始化需要特别注意顺序,静态域,甚至是final型的静态域,都可能在它们被正确地初始化之前被使用。
4。看看一个程序,动态绑定与静态绑定。
class Dog {
public static void bark() {
System.out.print("woof ");
}
}
class Basenji extends Dog {
public static void bark() { }
}
public class Bark {
public static void main(String args[]) {
Dog woofer = new Dog();
Dog nipper = new Basenji();
woofer.bark();
nipper.bark();
}
}
啊,你可能一开始就认为这应该是动态绑定吧,多态性!!那么应该只打印一个woof吧!可并非如此,可怜的小狗Basenji也叫唤了(据书中所说,此种狗在非洲,而且从来不叫唤,无法想象这世上有不叫唤的狗,呵呵)。问题就在于bark是一个静态方法,而静态方法的调用不存在任何动态绑定机制!对于静态方法,只会根据调用者的编译期类型进行调用,woofer,nipper都被声明为Dog,他们的编译期类型相同。你可能要问,子类Basenji中的bark()方法不是覆写了父类中的bark方法,那么是怎么回事?答案是它隐藏了父类中的bark方法,静态方法是不能被覆写的,他们只能被隐藏。要想使这只可怜的小狗回归不叫唤的“正常”状态,去掉方法之前的static标签即可。
5。instanceof操作符的特点:
A。null instanceof String时返回false,即左操作数为null时返回false
B。instantceof在编译期就会判断左操作数是否是右操作数的子类型,如果不是,在编译期就无法通过
C。编译期通过了,不意味着左操作数就是右操作数的类型或者子类型了,当你进行转型操作时将进行运行期判断,如转型失败抛出ClassCastException。
6。我们经常遇到这样的需求,跟踪一个类创建出来的实例个数,典型方法是在它的构造函数中递增一个私有的静态变量,如:
public class Creator {
public static void main(String[] args) {
for (int i = 0; i < 100; i++)
Creature creature = new Creature();
System.out.println(Creature.numCreated());
}
}
class Creature {
private static long numCreated = 0;
public Creature() {
numCreated++;
}
public static long numCreated() {
return numCreated;
}
}
你尝试着运行此程序,并指望它打印100,可惜,它没有,因为你连编译都没办法通过。。。。。为什么?出错信息如下:
E:/book/Java/Javapuzzlers/src/com/denny_blue/puzzlers/classy/Creator.java:18: 不是语句
Creature creature = new Creature();
E:/book/Java/Javapuzzlers/src/com/denny_blue/puzzlers/classy/Creator.java:18: 需要 ';'
Creature creature = new Creature();
2 错误
很奇怪是不?其实很简单, Creature creature = new Creature();这是一句local variable declaration statement,而java语言规范(JLS14.4)是不允许一个本地变量声明语句作为一条语句在for,while或者do中循环执行的,它只能直接出现在一个语句块中(block),所以简单的解决办法就是给这句话加上{},或者
for (int i = 0; i < 100; i++)
new Creature();
编译通过,。现在也可以正常打印出100了,还有个问题,如果有多线程并行地创建对象,那么我们需要同步计数器代码和访问计数器的代码:
class Creature {
private static long numCreated = 0;
public Creature() {
synchronized (Creature.class){
numCreated++;
}
}
public static synchronized long numCreated() {
return numCreated;
}
}
如果你使用JDK5或者更新的版本,你可以使用新引入的ActiomicLong,这实在是好东西,我对JDK5新引入的这个并发包了解太少。它在面临并发时可以绕过同步需求:
import java.util.concurrent.atomic.AtomicLong;
/**
*
* @author dennis
*/
public class Creature {
private static AtomicLong numCreated=new AtomicLong();
/** Creates a new instance of Creature */
public Creature() {
numCreated.incrementAndGet();
}
public static long numCreated(){
return numCreated;
}
}