ITPub博客

首页 > Linux操作系统 > Linux操作系统 > java解惑

java解惑

原创 Linux操作系统 作者:literfly3 时间:2008-08-31 10:41:22 0 删除 编辑

Java 谜题 1——表达式谜题
谜题 1:奇数性
      下面的方法意图确定它那唯一的参数是否是一个奇数。这个方法能够正确运转
吗?
     public static boolean isOdd(int i){
            return i % 2 == 1;
     }
        奇数可以被定义为被 2 整除余数为 1 的整数。表达式 i % 2 计算的是 i 整除 2时所产生的余数,因此看起来这个程序应该能够正确运转。遗憾的是,它不能;它在四分之一的时间里返回的都是错误的答案。为什么是四分之一?因为在所有的 int 数值中,有一半都是负数,而 isOdd 方法对于对所有负奇数的判断都会失败。在任何负整数上调用该方法都回返回false ,不管该整数是偶数还是奇数。这是 Java 对取余操作符(%)的定义所产生的后果。该操作符被定义为对于所有的 int 数值 a 和所有的非零 int 数值 b,都满足下面的恒等式:
               (a / b) * b + (a % b) == a
换句话说,如果你用 b 整除 a,将商乘以 b,然后加上余数,那么你就得到了最初的值 a 。该恒等式具有正确的含义,但是当与 Java 的截尾整数整除操作符相结合时,它就意味着:当取余操作返回一个非零的结果时,它与左操作数具有相同的正负符号。当 i 是一个负奇数时,i % 2 等于-1 而不是 1, 因此 isOdd 方法将错误地返回 false。 为了防止这种意外, 请测试你的方法在为每一个数值型参数传递负数、零和正数数值时,其行为是否正确。
        这个问题很容易订正。只需将 i % 2 与 0 而不是与 1 比较,并且反转比较的含
义即可:
        public static boolean isOdd(int i){
              return i % 2 != 0;
        }
如果你正在在一个性能临界(performance-critical)环境中使用 isOdd 方法,那么用位操作符 AND(&)来替代取余操作符会显得更好:
        public static boolean isOdd(int i){
              return (i & 1) != 0;
         }
        总之,无论你何时使用到了取余操作符,都要考虑到操作数和结果的符号。该操    作符的行为在其操作数非负时是一目了然的,  但是当一个或两个操作数都是负数时,它的行为就不那么显而易见了。


    谜题 2:找零时刻
    请考虑下面这段话所描述的问题:
        Tom 在一家汽车配件商店购买了一个价值$1.10 的火花塞,但是他钱包中都是两美元一张的钞票。如果他用一张两美元的钞票支付这个火花塞,那么应该找给他 多少零钱呢?
       下面是一个试图解决上述问题的程序,它会打印出什么呢?
            public class Change{
                    public static void main(String args[]){
                           System.out.println(2.00 - 1.10);
                    }
             }
       你可能会很天真地期望该程序能够打印出 0.90,但是它如何才能知道你想要打印小数点后两位小数呢?如果你对在 Double.toString 文档中所设定的将 double 类型的值转换为字符串 的规则有所了解,你就会知道该程序打印出来的小数,是足以将double 类型的 值与最靠近它的临近值区分出来的最短的小数, 它在小数点之前和之后都至少有一位。因此,看起来,该程序应该打印 0.9 是合理的。
        这么分析可能显得很合理,但是并不正确。如果你运行该程序,你就会发现它打    印的是 0.8999999999999999。问题在于 1.1 这个数字不能被精确表示成为一个 double,因此它被表示成为最接近它的 double 值。该程序从 2 中减去的就是这个值。遗憾的是,这个计算的结果并不是最接近 0.9 的 double 值。表示结果的 double 值的最短表示就是你所看到的打印出来的那个可恶的数字。更一般地说,问题在于并不是所有的小数都可以用二进制浮点数来精确表示的。如果你正在用的是 JDK 5.0 或更新的版本,那么你可能会受其诱惑,通过使用printf 工具来设置输出精度的方订正该程序:
        //拙劣的解决方案——仍旧是使用二进制浮点数
          System.out.printf("%.2f%n",2.00 - 1.10);
       这条语句打印的是正确的结果,  但是这并不表示它就是对底层问题的通用解决方案:它使用的仍旧是二进制浮点数的 double 运算。浮点运算在一个范围很广的 值域上提供了很好的近似,但是它通常不能产生精确的结果。二进制浮点对于货币计算是非常不适合的,  因为它不可能将 0.1——或者 10 的其它任何次负幂——精确表示为一个长度有限的二进制小数。
        解决该问题的一种方式是使用某种整数类型,例如 int 或 long,并且以分为单位来执行计算。如果你采纳了此路线,请确保该整数类型大到足够表示在程序中你将要用到的所有值。对这里举例的谜题来说, 就足够了。int下面是我们用int 类型来以分为单位表示货币值后重写的 println 语句。这个版本将打印出正确答案 90 分:
            System.out.println((200 - 110) + "cents");
        解决该问题的另一种方式是使用执行精确小数运算的 BigDecimal。它还可以通过 JDBC 与 SQL DECIMAL 类型进行互操作。这里要告诫你一点: 一定要用BigDecimal(String)构造器,而千万不要用 BigDecimal(double)。后一个构造器将用它的参数的“精确”值来创建一个实例:new BigDecimal(.1)将返回一个表示 0.100000000000000055511151231257827021181583404541015625 的BigDecimal。通过正确使用 BigDecimal,程序就可以打印出我们所期望的结果0.90:
        import java.math.BigDecimal;
        public class Change1{
               public static void main(String args[]){
                      System.out.println(new BigDecimal("2.00").
                      subtract(new BigDecimal("1.10")));
              }
        }
         因为 Java 并没有为 BigDecimal 提供任何语言上的这个版本并不是十分地完美,支持。使用 BigDecimal 的计算很有可能比那些使用原始类型的计算要慢一些,对某些大量使用小数计算的程序来说, 这可能会成为问题,  而对大多数程序来说,这显得一点也不重要。总之, 在需要精确答案的地方,要避免使用 float 和 double;对于货币计算, 要使用 int、long 或 BigDecimal。对于语言设计者来说,应该考虑对小数运算提供语言支持。一种方式是提供对操作符重载的有限支持,以使得运算符可以被 塑造为能够对数值引用类型起作用,例如 BigDecimal。另一种方式是提供原始的小数类型,就像 COBOL 与 PL/I 所作的一样。


    谜题 3:长整除
        这个谜题之所以被称为长整除是因为它所涉及的程序是有关两个 long 型数值整除的。被除数表示的是一天里的微秒数;而除数表示的是一天里的毫秒数。这个    程序会打印出什么呢?
    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);
            }
    }
    这个谜题看起来相当直观。每天的毫秒数和每天的微秒数都是常量。为清楚起见,它们都被表示成积的形式。每天的微秒数是(24 小时/天*60 分钟/小时*60 秒/分钟*1000 毫秒/秒*1000 微秒/毫秒)。而每天的毫秒数的不同之处只是少了最后一个因子 1000。
        当你用每天的毫秒数来整除每天的微秒数时,除数中所有的因子都被约掉了,只剩下 1000,这正是每毫秒包含的微秒数。 除数和被除数都是 long 类型的,long 类型大到了可以很容易地保存这两个乘积而不产生溢出。因此,看起来程序打印的必定是 1000。
       遗憾的是,它打印的是 5。这里到底发生了什么呢?
       问题在于常数 MICROS_PER_DAY 的计算“确实”溢出了。尽管计算的结果适合放入long 中,并且其空间还有富余,但是这个结果并不适合放入int 中。这个计算完全是以 int 运算来执行的,并且只有在运算完成之后,其结果才被提升到long,而此时已经太迟了:  计算已经溢出了, 它返回的是一个小了 200 倍的数值。
    从 int 提升到 long 是一种拓宽原始类型转换(widening primitive conversion),它保留了(不正确的)数值。这个值之后被 MILLIS_PER_DAY 整除,而LLIS_PER_DAY 的计算是正确的,因为它适合 int 运算。这样整除的结果就得到了5。那么为什么计算会是以 int 运算来执行的呢?因为所有乘在一起的因子都是int 数值。当你将两个 int 数值相乘时,你将得到另一个int 数值。Java 不具有目标确定类型的特性,这是一种语言特性,其含义是指存储结果的变量的类型会影响到计算所使用的类型。
        通过使用 long 常量来替代 int 常量作为每一个乘积的第一个因子,我们就可以很容易地订正这个程序。这样做可以强制表达式中所有的后续计算都用long 运作来完成。尽管这么做只在 MICROS_PER_DAY 表达式中是必需的,但是在两个乘积中都这么做是一种很好的方式。相似地,使用 long 作为乘积的“第一个”数值也并不总是必需的,  但是这么做也是一种很好的形式。在两个计算中都以long数值开始可以很清楚地表明它们都不会溢出。下面的程序将打印出我们所期望的1000:
    public class LongDivision{
            public static void main(String args[ ]){
                    final long MICROS_PER_DAY = 24L * 60 * 60 * 1000 * 1000;
                    final long MILLIS_PER_DAY = 24L * 60 * 60 * 1000;
                    System.out.println(MICROS_PER_DAY/MILLIS_PER_DAY);
            }
    }
        这个教训很简单:当你在操作很大的数字时,千万要提防溢出——它可是一个缄默杀手。即使用来保存结果的变量已显得足够大,也并不意味着要产生结果的计算具有正确的类型。当你拿不准时,就使用 long 运算来执行整个计算。
        语言设计者从中可以吸取的教训是:  也许降低缄默溢出产生的可能性确实是值得做的一件事。这可以通过对不会产生缄默溢出的运算提供支持来实现。程序可以抛出一个异常而不是直接溢出,就像 Ada 所作的那样,或者它们可以在需要的时候自动地切换到一个更大的内部表示上以防止溢出,就像 Lisp 所作的那样。这降低缄默溢出的另一种方式是支持目标确定类型,但是这么做会显著地增加类型系统的复杂度。两种方式都可能会遭受与其相关的性能方面的损失。  

  谜题 4:初级问题
        前面那个谜题是有点棘手,但它是有关整除的,每个人都知道整除是很麻
    烦的。那么下面的程序只涉及加法,它又会打印出什么呢?
        public class Elementary{
              public static void main(String[] args){
                    System.out.println(12345+5432l);
              }
        }
        从表面上看,这像是一个很简单的谜题——简单到不需要纸和笔你就可以解决它。加号的左操作数的各个位是从 1 到 5 升序排列的, 而右操作数是降序排列的。因此,相应各位的和仍然是常数,程序必定打印66666。对于这样的分析,只有一个问题:当你运行该程序时,它打印出的是 17777。难道是 Java 对打印这样的非常数字抱有偏见吗?不知怎么的,这看起来并不像是一个合理的解释。事物往往有别于它的表象。就以这个问题为例, 它并没有打印出我们想要的输出。
        请仔细观察 + 操作符的两个操作数,我们是将一个int 类型的12345 加到了long 类型的5432l 上。请注意左操作数开头的数字1 和右操作数结尾的小写字母 l 之间的细微差异。数字1 的水平笔划(称为“臂(arm)”)和垂直笔划(称为“茎(stem)”)之间是一个锐角,而与此相对照的是,小写字母 l 的臂和茎之间是一个直角。
        在你大喊“恶心!”之前,你应该注意到这个问题确实已经引起了混乱,这里确实有一个教训: long 型字面常量中,在一定要用大写的L, 千万不要用小写的 l。这样就可以完全掐断这个谜题所产生的混乱的源头:
            System.out.println(12345+5432L);
        相类似的,要避免使用单独的一个 l 字母作为变量名。例如,我们很难通过观察下面的代码段来判断它到底是打印出列表 l 还是数字 1。
            //不良代码-使用了 l 作为变量名
            List l = new ArrayList();
             l.add("Foo");
             System.out.println(1);
        总之,小写字母 l 和数字 1 在大多数打字机字体中都是几乎一样的。为避免你的程序的读者对二者产生混淆,千万不要使用小写的 l 来作为 long 型字面常量的结尾或是作为变量名。Java 从 C 编程语言中继承良多,包括 long 型字面常量的语法。也许当初允许用小写的 l 来编写 long 型字面常量本身就是一个错误。

   谜题 5:十六进制的趣事
       下面的程序是对两个十六进制(hex)字面常量进行相加,然后打印出十六进制
    的结果。这个程序会打印出什么呢?
        public class JoyOfHex{
              public static void main(String[] args){
                    System.out.println(
                            Long.toHexString(0x100000000L + 0xcafebabe));
               }
        }
        看起来很明显,该程序应该打印出 1cafebabe。毕竟,这确实就是十六进制数字
    10000000016 与 cafebabe16 的和。该程序使用的是 long 型运算,它可以支持 16位十六进制数,因此运算溢出是不可能的。 然而,如果你运行该程序,你就会发现它打印出来的是 cafebabe,并没有任何前导的1。这个输出表示的是正确结果的低 32 位,但是不知何故,第 33 位丢失了。
        看起来程序好像执行的是 int 型运算而不是 long 型运算,或者是忘了加第一个操作数。这里到底发生了什么呢?
        十进制字面常量具有一个很好的属性,即所有的十进制字面常量都是正的,而十六进制或是八进制字面常量并不具备这个属性。要想书写一个负的十进制常量, 可以使用一元取反操作符(-)连接一个十进制字面常量。以这种方式,你可以用十进制来书写任何 int 或 long 型的数值,不管它是正的还是负的,并且负的十进制常数可以很明确地用一个减号符号来标识。但是十六进制和八进制字面常量并不是这么回事,它们可以具有正的以及负的数值。如果十六进制和八进制字面常量的最高位被置位,   那么它们就是负数。在这个程序中,  数字 0xcafebabe是一个 int 常量,它的最高位被置位了,所以它是一个负数。它等于十进制数值 -889275714。
         该程序执行的这个加法是一种“混合类型的计算(mixed-type computation):左操作数是 long 类型的,而右操作数是 int 类型的。为了执行该计算,Java 将int 类型的数值用拓宽原始类型转换提升为一个long 类型,然后对两个long 类 型数值相加。因为 int 是一个有符号的整数类型,所以这个转换执行的是符合扩展:它将负的 int 类型的数值提升为一个在数值上相等的 long 类型数值。
        这个加法的右操作数 0xcafebabe 被提升为了 long 类型的数fffffcafebabeL。这个数值之后被加到了左操作数 0x100000000L 上。当作为int 类型来被审视时,经过符号扩展之后的右操作数的高 32 位是-1,而左操作数的高 32 位是 1,将这两个数值相加就得到了 0,这也就解释了为什么在程序输出中前导 1 丢失了。下面所示是用手写的加法实现。(在加法上面的数字是进位)
        1111111
      0xffffffffcafebabeL
    + 0x0000000100000000L
    ---------------------
      0x00000000cafebabeL
        只需用一个 long 十六进制字面常量来表示右操作数即可。订正该程序非常简单, 这就可以避免了具有破坏力的符号扩展,   并且程序也就可以打印出我们所期望的结果 1cafebabe:
        public class JoyOfHex{
              public static void main(String[] args){
                      System.out.println(
                             Long.toHexString(0x100000000L + 0xcafebabeL));
               }
        }
       这个谜题给我们的教训是:混合类型的计算可能会产生混淆,尤其是十六进制和八进制字面常量无需显式的减号符号就可以表示负的数值。为了避免这种窘境, 通常最好是避免混合类型的计算。对于语言的设计者们来说,应该考虑支持无符号的整数类型,从而根除符号扩展的可能性。可能会有这样的争辩:负的十六进制和八进制字面常量应该被禁用,但是这可能会挫伤程序员,他们经常使用十六进制字面常量来表示那些符号没有任何重要含义的数值。


    谜题 6:多重转型
        转型被用来将一个数值从一种类型转换到另一种类型。下面的程序连续使用了三个转型。那么它到底会打印出什么呢?
    public class Multicast{
            public static void main (String[] args){
                    System.out.println((int)(char)(byte) -1);
            }
    }
      无论你怎样分析这个程序,都会感到很迷惑。它以 int 数值-1 开始,然后从 int转型为 byte,之后转型为 char,最后转型回 int。第一个转型将数值从 32 位窄化到了 8 位,第二个转型将数值从 8 位拓宽到了 16 位,最后一个转型又将数值从 16 位拓宽回了 32 位。这个数值最终是回到了起点吗?如果你运行该程序,你就会发现不是。它打印出来的是 65535,但是这是为什么呢?
       该程序的行为紧密依赖于转型的符号扩展行为。Java 使用了基于 2 的补码的二进制运算,因此 int 类型的数值-1 的所有 32 位都是置位的。从 int 到 byte 的转型是很简单的,它执行了一个窄化原始类型转化(narrowing primitive conversion),直接将除低 8 位之外的所有位全部砍掉。这样做留下的是一个8位都被置位了的 byte,它仍旧表示-1。从 byte 到 char 的转型稍微麻烦一点,因为 byte 是一个有符号类型,而 char是一个无符号类型。在将一个整数类型转换成另一个宽度更宽的整数类型时,通 常是可以保持其数值的,但是却不可能将一个负的 byte 数值表示成一个 char。因此,从 byte 到 char 的转换被认为不是一个拓宽原始类型的转换,而是一个拓宽并窄化原始类型的转换(widening and narrowing primitive conversion): byte 被转换成了 int,而这个 int 又被转换成了 char。
        所有这些听起来有点复杂,幸运的是,有一条很简单的规则能够描述从较窄的整型转换成较宽的整型时的符号扩展行为:如果最初的数值类型是有符号的,那么就执行符号扩展;如果它是 char,那么不管它将要被转换成什么类型,都执行零扩展。了解这条规则可以使我们很容易地解决这个谜题。
        因为 byte 是一个有符号的类型,所以在将 byte 数值-1 转换成 char 时,会发生符号扩展。作为结果的char 数值的16 个位就都被置位了,因此它等于 216-1, 即65535。从char 到 int 的转型也是一个拓宽原始类型转换,所以这条规则告它将执行零扩展而不是符号扩展。作为结果的 int 数值也就成了65535,诉我们,这正是程序打印出的结果。
       尽管这条简单的规则描述了在有符号和无符号整型之间进行拓宽原始类型时的符号扩展行为,你最好还是不要编写出依赖于它的程序。如果你正在执行一个转型到 char 或从 char 转型的拓宽原始类型转换,并且这个 char 是仅有的无符号整型,那么你最好将你的意图明确地表达出来。
      如果你在将一个 char 数值 c 转型为一个宽度更宽的类型,并且你不希望有符号扩展,那么为清晰表达意图,可以考虑使用一个位掩码,即使它并不是必需的:
           int i = c & 0xffff;
      或者,书写一句注释来描述转换的行为:
           int i = c; //不会执行符号扩展
      如果你在将一个 char 数值 c 转型为一个宽度更宽的整型,并且你希望有符号扩展,那么就先将 char 转型为一个 short,它与 char 具有同样的宽度,但是它是有符号的。在给出了这种细微的代码之后,你应该也为它书写一句注释:
           int i = (short) c; //转型将引起符号扩展
      如果你在将一个 byte 数值 b 转型为一个 char,并且你不希望有符号扩展,那么你必须使用一个位掩码来限制它。这是一种通用做法,所以不需要任何注释:
          char c = (char) (b & 0xff);
    这个教训很简单:如果你通过观察不能确定程序将要做什么,那么它做的就很有可能不是你想要的。要为明白清晰地表达你的意图而努力。尽管有这么一条简单的规则,描述了涉及有符号和无符号整型拓宽转换的符号扩展行为,但是大多数程序员都不知道它。如果你的程序依赖于它,   那么你就应该把你的意图表达清楚。


    谜题 7:互换内容
    下面的程序使用了复合的异或赋值操作符,它所展示的技术是一种编程习俗。那
    么它会打印出什么呢?
    public class CleverSwap{
            public static void main(String[] args){
                    int x = 1984; // (0x7c0)
                    int y = 2001; // (0x7d1)
                    x^= y^= x^= y;
                    System.out.println("x= " + x + "; y= " + y);
            }
    }
        就像其名称所暗示的,这个程序应该交换变量 x 和 y 的值。如果你运行它,就会发现很悲惨,它失败了,打印的是
            x = 0; y = 1984。
        交换两个变量的最显而易见的方式是使用一个临时变量:
                int  tmp = x;
                x = y;
                y = tmp;
       很久以前,当中央处理器只有少数寄存器时,人们发现可以通过利用异或操作符   (^)的属性(x ^ y ^ x) == y 来避免使用临时变量:
                 x = x ^ y;
                 y = y ^ x;
                 x = y ^ x;
       这个惯用法曾经在 C 编程语言中被使用过,并进一步被构建到了 C++中,但是它并不保证在二者中都可以正确运行。但是有一点是肯定的,那就是它在Java 中肯定是不能正确运行的。Java 语言规范描述到:操作符的操作数是从左向右求值的。为了求表达式 x ^=expr 的值,x 的值是在计算 expr 之前被提取的,并且这两个值的异或结果被赋给变量 x。在 CleverSwap 程序中,变量 x 的值被提取了两次——每次在表达式中出现时都提取一次——但是两次提取都发生在所有的赋值操作之前。
    下面的代码段详细地描述了将互换惯用法分解开之后的行为,并且解释了为什么产生的是我们所看到的输出:
        // Java 中 x^= y^= x^= y 的实际行为
        int tmp1 = x ; // x 在表达式中第一次出现
        int tmp2 = y ; // y 的第一次出现
        int tmp3 = x ^ y ; // 计算 x ^ y
        x = tmp3 ; // 最后一个赋值:存储 x ^ y 到 x
        y = tmp2 ^ tmp3 ; // 第二个赋值:存储最初的 x 值到 y 中
        x = tmp1 ^ y ; // 第一个赋值:存储 0 到 x 中
      在 C 和 C++中,并没有指定表达式的计算顺序。当编译表达式 x ^= expr 时,许多C 和C++编译器都是在计算expr 之后才提取 x 的值的,这就使得上述的惯用法可以正常运转。尽管它可以正常运转,但是它仍然违背了 C/C++有关不能在两个连续的序列点之间重复修改变量的规则。因此,这个惯用法的行为在 C 和 C++中也没有明确定义。
        为了看重其价值,我们还是可以写出不用临时变量就可以互换两个变量内容的Java 表达式的。但是它同样是丑陋而无用的:
           // 杀鸡用牛刀的做法,千万不要这么做!
           y = (x^= (y^= x))^ y ;
        这个教训很简单:在单个的表达式中不要对相同的变量赋值两次。表达式如果包含对相同变量的多次赋值,就会引起混乱,并且很少能够执行你希望的操作。即使对多个变量进行赋值也很容易出错。更一般地讲, 要避免所谓聪明的编程技巧。它们都是易于产生 bug 的,很难以维护,并且运行速度经常是比它们所替代掉的简单直观的代码要慢。
        语言设计者可能会考虑禁止在一个表达式中对相同的变量多次赋但是在一般的情况下,强制执行这条禁令会因为别名机制的存在而显得很不灵活。例如,请考虑表达式 x = a[i]++ - a[j]++,它是否递增了相同的变量两次呢?这取决于在表达式被计算时 i 和 j 的值,并且编译器通常是无法确定这一点。


    谜题 8:Dos Equis
        这个谜题将测试你对条件操作符的掌握程度,操作符有一个更广为人知的名
    字:问号冒号操作符。下面的程序将会打印出什么呢?
    public class DosEquis{
            public static void main(String[] args){
                    char x = 'X';
                    int i = 0;
                    System.out.println(true ? x : 0);
                    System.out.println(false ? i : x);
            }
    }
        这个程序由两个变量声明和两个 print 语句构成。print 语句计算条件表达式(true ? x : 0)并打印出结果,这个结果是 char 类型变量 x 的值’X’。而第二个 print 语句计算表达式(false ? i : x)并打印出结果,这个结果还是依旧是’X’的 x,因此这个程序应该打印 XX。然而,如果你运行该程序,你就会发现它打印出来的是 X88。这种行为看起来挺怪的。第一个 print 语句打印的是X,而第二个打印的却是 88。它们的不同行为说明了什么呢?
        答案就在规范有关条件表达式部分的一个阴暗的角落里。请注意在这两个表达式中,每一个表达式的第二个和第三个操作数的类型都不相同:x 是 char 类型的, 而 0 和 i 都是 int 类型的。就像在谜题 5 的解答中提到的,混合类型的计算会引混乱,而这一点比在条件表达式中比在其它任何地方都表现得更明显。你可能考虑过,这个程序中两个条件表达式的结果类型是相同的,就像它们的操作数类型是相同的一样,尽管操作数的顺序颠倒了一下,但是实际情况并非如此。
        确定条件表达式结果类型的规则过于冗长和复杂,很难完全记住它们,但是其核心就是一下三点:
       •  如果第二个和第三个操作数具有相同的类型,   那么它就是条件表达式的类
          型。换句话说,你可以通过绕过混合类型的计算来避免大麻烦。
       •  如果一个操作数的类型是T,T 表示byte、short 或char,而另一个操作
          数是一个int 类型的常量表达式,它的值是可以用类型 T 表示的,那么条
          件表达式的类型就是T。
       •  否则,将对操作数类型运用二进制数字提升,而条件表达式的类型就是第
          二个和第三个操作数被提升之后的类型。
       2、3 两点对本谜题是关键。在程序的两个条件表达式中,一个操作数的类型是char,另一个的类型是 int。在两个表达式中,int 操作数都是 0,它可以被表示成一个 char。然而,只有第一个表达式中的 int 操作数是常量(0),而第二个表达式中的 int 操作数是变量(i)。因此,第 2 点被应用到了第一个表达式上,它返回的类型是 char,而第 3 点被应用到了第二个表达式上,其返回的类型是对 int 和 char 运用了二进制数字提升之后的类型,即 int。
        条件表达式的类型将确定哪一个重载的 print 方法将被调用。 对第一个表达式来说,PrintStream.print(char)将被调用,而对第二个表达式来说,PrintStream.print(int)将被调用。前一个重载方法将变量 x 的值作为Unicode字符(X)来打印,而后一个重载方法将其作为一个十进制整数(88)来打印。 至此,谜题被解开了。
        总之,通常最好是在条件表达式中使用类型相同的第二和第三操作数。否则,你和你的程序的读者必须要彻底理解这些表达式行为的复杂规范。对语言设计者来说,也许可以设计一个牺牲掉了部分灵活性,但是增加了简洁性的条件操作符。例如,要求第二和第三操作数必须就有相同的类型,这看起来就很合理。或者,条件操作符可以被定义为对常量没有任何特殊处理。为了让这些选择对程序员来说更加容易接受,可以提供用来表示所有原始类型字面常量的语法。这也许确实是一个好注意,因为它增加了语言的一致性和完备性,同时又减少了对转型的需求。


    谜题 9:半斤
         现在该轮到你来写些代码了,好消息是,你只需为这个谜题编写两行代码,并为下一个谜题也编写两行代码。这有什么难的呢?我们给出一个对变量 x 和 i 的声明即可,它肯定是一个合法的语句:
            x += i;
        但是,它并不是:
           x = x + i;
       许多程序员都会认为该迷题中的第一个表达式(x += i)只是第二个表达式(x =x + i)的简写方式。但是这并不十分准确。这两个表达式都被称为赋值表达式。 第二条语句使用的是简单赋值操作符(=),而第一条语句使用的是复合赋值操作符。(复合赋值操作符包括 +=、-=、*=、/=、%=、<<=、>>=、>>>=、&=、^= 和|=)Java 语言规范中讲到,复合赋值 E1 p= E2 等价于简单赋值 E1 =(T)((E1)op(E2)),其中T 是E1 的类型,除非 E1 只被计算一次。换句话说,    复合赋值表达式自动地将它们所执行的计算的结果转型为其左侧变量的类型。如果结果的类型与该变量的类型相同, 那么这个转型不会造成任何影响。然而,如果结果的类型比该变量的类型要宽,那么复合赋值操作符将悄悄地执行 一个窄化原始类型转换。因此,我们有很好的理由去解释为什么在尝试着执行等价的简单赋值可能会产生一个编译错误。
        为了说得具体一些,并提供一个解决方案给这个谜题,假设我们在该谜题的两个赋值表达式之前有下面这些声明:
            short x = 0;
            int i = 123456;
        复合赋值编译将不会产生任何错误:
             x += i; // 包含了一个隐藏的转型!
        你可能期望 x 的值在这条语句执行之后是 123,456,但是并非如此 l,它的值是-7,616。int 类型的数值 123456 对于 short 来说太大了。自动产生的转型悄悄地把 int 数值的高两位给截掉了。这也许就不是你想要的了。
         相对应的简单赋值是非法的,因为它试图将 int 数值赋值给 short 变量,它需要一个显式的转型:
             x = x + i; // 不要编译——“可能会丢掉精度”
        这应该是明显的,复合赋值表达式可能是很危险的。为了避免这种令人不快的突 袭,请不要将复合赋值操作符作用于 byte、short 或 char 类型的变量上。在将复合赋值操作符作用于 int 类型的变量上时,   要确保表达式右侧不是 long、float 或 double 类型。在将复合赋值操作符作用于 float 类型的变量上时,要确保表达式右侧不是 double 类型。这些规则足以防止编译器产生危险的窄化转型。
        总之,复合赋值操作符会悄悄地产生一个转型。如果计算结果的类型宽于变量的类型,那么所产生的转型就是一个危险的窄化转型。这样的转型可能会悄悄地丢弃掉精度或数量值。对语言设计者来说,也许让复合赋值操作符产生一个不可见的转型本身就是一个错误;对于在复合赋值中的变量类型比计算结果窄的情况,也许应该让其非法才对。

        
    谜题 10:八两
        与上面的例子相反,如果我们给出的关于变量 x 和 i 的声明是如下的合法语句:
              x = x + i;
        但是,它并不是:
              x += i;
        乍一看,    这个谜题可能看起来与前面一个谜题相同。但是请放心,   它们并不一样。这两个谜题在哪一条语句必是合法的,以及哪一条语句必是不合法的方面,正好   相反。
        就像前面的谜题一样,这个谜题也依赖于有关复合赋值操作符的规范中的细节。 二者的相似之处就此打住。基于前面的谜题,你可能会想:符合赋值操作符比简单赋值操作符的限制要少一些。在一般情况下, 这是对的,  但是有这么一个领域,在其中简单赋值操作符会显得更宽松一些。复合赋值操作符要求两个操作数都是原始类型的,例如 int,或包装了的原始类 型,例如 Integer,但是有一个例外:如果在+=操作符左侧的操作数是 String 类型的,那么它允许右侧的操作数是任意类型,在这种情况下,该操作符执行的是字符串连接操作。简单赋值操作符(=)允许其左侧的是对象引用类型,这就显得要宽松许多了:你可以使用它们来表示任何你想要表示的内容,只要表达式的右侧与左侧的变量是赋值兼容的即可。
        你可以利用这一差异来解决该谜题。要想用+= 操作符来执行字符串连接操作,你就必须将左侧的变量声明为 String 类型。通过使用直接赋值操作符,字符串连接的结果可以存放到一个Object 类型的变量中。
       为了说得具体一些,并提供一个解决方案给这个谜题,假设我们在该谜题的两个赋值表达式之前有下面这些声明:
               Object x = "Buy ";
               String i = "Effective Java!";
        因为 x + i 是 String 类型的, String 类型又是与Object 简单赋值是合法而赋值兼容的:
                x = x + i;
        复合赋值是非法的,因为左侧是一个 Object 引用类型,而右侧是一个 String 类型:
                x += i;
        这个谜题对程序员来说几乎算不上什么教训。对语言设计者来说,加法的复合赋 值操作符应该在右侧是 String 类型的情况下,允许左侧是 Object 类型。这项修改将根除这个谜题所展示的违背直觉的行为。

                   
    Java 谜题 2——字符谜题
          11:
    谜题 11:最后的笑声
    下面的程序将打印出什么呢?
    public class LastLaugh{
        public static void main(String[] args){
            System.out.print("H"+"a");
            System.out.print('H'+'a');
        }
    }
    你可能会认为这个程序将打印 HaHa。该程序看起来好像是用两种方式连接了 H
    和 a,但是你所见为虚。如果你运行这个程序,就会发现它打印的是 Ha169。那
    么,为什么它会产生这样的行为呢?
    正如我们所期望的,第一个对 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 类型时,才会执行字符串连接操作;否则,它执行的就是加
    法。如果要连接的没有一个数值是字符串类型的,那么你可以有几种选择:
       •    预置一个空字符串;
       •    将第一个数值用 String.valueOf 显式地转换成一个字符串;
       •    使用一个字符串缓冲区;
       •    或者如果你使用的 JDK 5.0,可以用 printf 方法。
    这个谜题还包含了一个给语言设计者的教训。操作符重载,即使在 Java 中只在
    有限的范围内得到了支持,它仍然会引起混淆。为字符串连接而重载 + 操作符
    可能就是一个已铸成的错误。
           12:
    谜题 12:ABC
    这个谜题要问的是一个悦耳的问题,下面的程序将打印什么呢?
    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 方法,使其
    返回数组内容的一个字符串表示。
          13:
    谜题 13:畜牧场
    George Orwell 的《畜牧场(Animal Farm)》一书的读者可能还记得老上校的
    宣言:“所有的动物都是平等的。”下面的 Java 程序试图要测试这项宣言。那
    么,它将打印出什么呢?
    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));
    这个谜题对语言设计者来说有两个教训。
       •    字符串连接的优先级不应该和加法一样。这意味着重载 + 操作符来执行
            字符串连接是有问题的,就像在谜题 11 中提到的一样。
       •    还有就是,对于不可修改的类型,例如 String,其引用的等价性比值的
                                  也许 == 操作符在被应用于不可修改的类型时
            等价性更加让人感到迷惑。
            应该执行值比较。要实现这一点,一种方法是将 == 操作符作为 equals
            方法的简便写法,并提供一个单独的类似于 System.identityHashCode
            的方法来执行引用标识的比较。
           14:
    谜题 14:转义字符的溃败
    下面的程序使用了两个 Unicode 的转义字符,它们是用其十六进制代码来表示
    Unicode 字符。那么,这个程序会打印什么呢?
    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 字符插入到源文件中。
           15:
    谜题 15:令人晕头转向的 Hello
    下面的程序是对一个老生常谈的例子做出了稍许的变化之后的版本。那么,它会
    打印出什么呢?
    /**
      * 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é”,但是后
    一个注释在源文件中还是可理解的。
    可能你会感到很诧异,在这个谜题中,问题出在注释这一信息来源自一个实际的
    bug 报告。该程序是机器生成的,这使得我们很难追踪到问题的源头
    ——IDL-to-Java 编译器。为了避免让其他程序员也陷入此境地,在没有将
    Windows 文件名进行预先处理,以消除的其中的反斜杠的情况下,工具应该确保
    不将 Windows 文件名置于所生成的 Java 源文件的注释中。
    总之,要确保字符\u 不出现在一个合法的 Unicode 转义字符上下文之外,即使
    是在注释中也是如此。在机器生成的代码中要特别注意此问题。
           16:
    谜题 16:行打印程序
    行分隔符     (line separator) 是为用来分隔文本行的字符或字符组合而起的名字,
    并且它在不同的平台上是存在差异的。在 Windows 平台上,它是 CR 字符(回车)
    和紧随其后的 LF 字符(换行)组成的,而在 UNIX 平台上,通常单独的 LF 字符
    被当作换行字符来引用。下面的程序将这个字符传递给了 println 方法,那么,
    它将打印出什么呢?它的行为是否是依赖于平台的呢?
    public class LinePrinter{
         public static void main(String[] args){
         // Note: \u000A is Unicode representation of linefeed (LF)
             char c = 0x000A;
             System.out.println(c);
         }
    }
    这个程序的行为是平台无关的:它在任何平台上都不能通过编译。如果你尝试着
    去编译它,就会得到类似下面的出错信息:
    LinePrinter.java:3: ';' expected
    // Note: \u000A is Unicode representation of linefeed (LF)
    ^
    1 error
    如果你和大多数人一样,那么这条信息对界定问题是毫无用处的。
    这个谜题的关键就是程序第三行的注释。与最好的注释一样,这条注释也是一种
    准确的表达,遗憾的是,它有一点准确得过头了。编译器不仅会在将程序解析成
    为符号之前把 Unicode 转义字符转换成它们所表示的字符(谜题 14),而且它
    是在丢弃注释和空格之前做这些事的[JLS 3.2]。
    这个程序包含了一个 Unicode 转移字符           (\u000A) 它位于程序唯一的注释行中。
                                               ,
    就像注释所陈述的,这个转义字符表示换行符,编译器将在丢弃注释之前适时地
    转换它。遗憾的是,这个换行符是表示注释开始的两个斜杠符之后的第一个行终
    结符(line terminator),因此它将终结该注释[JLS 3.4]。所以,该转义字符
    之后的字(is Unicode representation of linefeed (LF))就不是注释的一部
    分了,而它们在语法上也不是有效的。
    订正该程序的最简单的方式就是在注释中移除 Unicode 转义字符,                       但是更好的方
    式是用一个转义字符序列而不是一个十六进制整型字面常量来初始化 c,从而消
    除使用注释的必要:
    public class LinePrinter{
        public static void main(String[] args){
            char c = '\n';
            System.out.println(c);
        }
    }
    只要这么做了,程序就可以编译并运行,但是这仍然是一个有问题的程序:它是
    平台相关的,这正是本谜题所要表达的真正意图。在某些平台上,例如 UNIX,
&nb

来自 “ ITPUB博客 ” ,链接:https://blog.itpub.net/15479909/viewspace-439040/ ,如需转载,请注明出处,否则将追究法律责任。

上一篇: 没有了~
下一篇: Java 运行时监控
请登录后发表评论 登录
全部评论

注册时间:2008-08-28

  • 博文量
    6
  • 访问量
    3767