首页 > > > java解惑.pdf

java解惑.pdf

java解惑.pdf

上传者: xiao_hzhang 2014-02-22 评分1 评论0 下载1 收藏10 阅读量797 暂无简介 简介 举报

简介:本文档为《java解惑pdf》,可适用于软件工程领域,主题内容包含JavaJavaJavaJava谜题谜题谜题谜题表达式谜题表达式谜题表达式谜题表达式谜题谜题谜题谜题谜题::::奇数性奇数性奇数性奇数性下面的方法意符等。

JavaJavaJavaJava 谜题谜题谜题谜题 1111————————表达式谜题表达式谜题表达式谜题表达式谜题 谜题谜题谜题谜题 1111::::奇数性奇数性奇数性奇数性 下面的方法意图确定它那唯一的参数是否是一个奇数。这个方法能够正确运转 吗? 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; } 总之,无论你何时使用到了取余操作符,都要考虑到操作数和结果的符号。该操 作符的行为在其操作数非负时是一目了然的,但是当一个或两个操作数都是负数 时,它的行为就不那么显而易见了。 谜题谜题谜题谜题 2222::::找零时刻找零时刻找零时刻找零时刻 请考虑下面这段话所描述的问题: 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所作的一样。 谜题谜题谜题谜题 3333::::长整除长整除长整除长整除 这个谜题之所以被称为长整除是因为它所涉及的程序是有关两个 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 整除,而 MILLIS_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 所作的那样。这 两种方式都可能会遭受与其相关的性能方面的损失。降低缄默溢出的另一种方式 是支持目标确定类型,但是这么做会显著地增加类型系统的复杂度 谜题谜题谜题谜题 4444::::初级问题初级问题初级问题初级问题 得啦,前面那个谜题是有点棘手,但它是有关整除的,每个人都知道整除是很麻 烦的。那么下面的程序只涉及加法,它又会打印出什么呢? 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<String>(); l.add("Foo"); System.out.println(1); 总之,小写字母 l 和数字 1 在大多数打字机字体中都是几乎一样的。为避免你的 程序的读者对二者产生混淆,千万不要使用小写的 l 来作为 long型字面常量的 结尾或是作为变量名。Java从 C编程语言中继承良多,包括 long型字面常量的 语法。也许当初允许用小写的 l 来编写 long型字面常量本身就是一个错误。 谜题谜题谜题谜题 5555::::十六进制的趣事十六进制的趣事十六进制的趣事十六进制的趣事 下面的程序是对两个十六进制(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 类型的数值 0xffffffffcafebabeL。这个数值之后被加到了左操作数 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)); } } 这个谜题给我们的教训是:混合类型的计算可能会产生混淆,尤其是十六进制和 八进制字面常量无需显式的减号符号就可以表示负的数值。为了避免这种窘境, 通常最好是避免混合类型的计算。对于语言的设计者们来说,应该考虑支持无符 号的整数类型,从而根除符号扩展的可能性。可能会有这样的争辩:负的十六进 制和八进制字面常量应该被禁用,但是这可能会挫伤程序员,他们经常使用十六 进制字面常量来表示那些符号没有任何重要含义的数值。 谜题谜题谜题谜题 6666::::多重转型多重转型多重转型多重转型 转型被用来将一个数值从一种类型转换到另一种类型。下面的程序连续使用了三 个转型。那么它到底会打印出什么呢? 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); 这个教训很简单:如果你通过观察不能确定程序将要做什么,那么它做的就很有 可能不是你想要的。要为明白清晰地表达你的意图而努力。尽管有这么一条简单 的规则,描述了涉及有符号和无符号整型拓宽转换的符号扩展行为,但是大多数 程序员都不知道它。如果你的程序依赖于它,那么你就应该把你的意图表达清楚。 谜题谜题谜题谜题 7777::::互换内容互换内容互换内容互换内容 下面的程序使用了复合的异或赋值操作符,它所展示的技术是一种编程习俗。那 么它会打印出什么呢? 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的值,并且编译器通常是无法确定这一点。 谜题谜题谜题谜题 8888::::Dos EquisDos EquisDos EquisDos 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)来打印。 至此,谜题被解开了。 总之,通常最好是在条件表达式中使用类型相同的第二和第三操作数。否则,你 和你的程序的读者必须要彻底理解这些表达式行为的复杂规范。 对语言设计者来说,也许可以设计一个牺牲掉了部分灵活性,但是增加了简洁性 的条件操作符。例如,要求第二和第三操作数必须就有相同的类型,这看起来就 很合理。或者,条件操作符可以被定义为对常量没有任何特殊处理。为了让这些 选择对程序员来说更加容易接受,可以提供用来表示所有原始类型字面常量的语 法。这也许确实是一个好注意,因为它增加了语言的一致性和完备性,同时又减 少了对转型的需求。 谜题谜题谜题谜题 9999::::半斤半斤半斤半斤 现在该轮到你来写些代码了,好消息是,你只需为这个谜题编写两行代码,并为 下一个谜题也编写两行代码。这有什么难的呢?我们给出一个对变量 x和 i 的声 明即可,它肯定是一个合法的语句: x += i; 但是,它并不是: x = x + i; 许多程序员都会认为该迷题中的第一个表达式(x += i)只是第二个表达式(x = x + i)的简写方式。但是这并不十分准确。这两个表达式都被称为赋值表达式。 第二条语句使用的是简单赋值操作符(=),而第一条语句使用的是复合赋值操 作符。(复合赋值操作符包括 +=、-=、*=、/=、%=、<<=、>>=、>>>=、&=、^= 和|=)Java语言规范中讲到,复合赋值 E1 op= 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类型。这些规则足以防止编译器产生危险的窄化转型。 总之,复合赋值操作符会悄悄地产生一个转型。如果计算结果的类型宽于变量的 类型,那么所产生的转型就是一个危险的窄化转型。这样的转型可能会悄悄地丢 弃掉精度或数量值。对语言设计者来说,也许让复合赋值操作符产生一个不可见 的转型本身就是一个错误;对于在复合赋值中的变量类型比计算结果窄的情况, 也许应该让其非法才对。 谜题谜题谜题谜题 10101010::::八两八两八两八两 与上面的例子相反,如果我们给出的关于变量 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类型。这项修 改将根除这个谜题所展示的违背直觉的行为。 JavaJavaJavaJava 谜题谜题谜题谜题 2222————————字符谜题字符谜题字符谜题字符谜题 谜题谜题谜题谜题 11111111::::最后的笑声最后的笑声最后的笑声最后的笑声 下面的程序将打印出什么呢? 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 中只在 有限的范围内得到了支持,它仍然会引起混淆。为字符串连接而重载 + 操作符 可能就是一个已铸成的错误。 谜题谜题谜题谜题 12121212::::ABCABCABCABC 这个谜题要问的是一个悦耳的问题,下面的程序将打印什么呢? 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方法,使其 返回数组内容的一个字符串表示。 谜题谜题谜题谜题 13131313::::畜牧场畜牧场畜牧场畜牧场 George Orwell 的《畜牧场(Animal Farm)》一书的读者可能还记得老上校的 宣言:“所有的动物都是平等的。”

该用户的其他资料

  • 名称/格式
  • 评分
  • 下载次数
  • 资料大小
  • 上传时间

用户评论

0/200
    暂无评论
上传我的资料

相关资料

资料评价:

/ 162
所需积分:2 立即下载
返回
顶部
举报
资料
关闭

温馨提示

感谢您对爱问共享资料的支持,精彩活动将尽快为您呈现,敬请期待!