关闭

关闭

封号提示

内容

首页 java解惑.pdf

java解惑.pdf

java解惑.pdf

上传者: xiao_hzhang 2014-02-22 评分 5 0 175 24 797 暂无简介 简介 举报

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

JavaJavaJavaJava谜题谜题谜题谜题表达式谜题表达式谜题表达式谜题表达式谜题谜题谜题谜题谜题::::奇数性奇数性奇数性奇数性下面的方法意图确定它那唯一的参数是否是一个奇数。这个方法能够正确运转吗?publicstaticbooleanisOdd(inti){returni==}奇数可以被定义为被整除余数为的整数。表达式i计算的是i整除时所产生的余数因此看起来这个程序应该能够正确运转。遗憾的是它不能它在四分之一的时间里返回的都是错误的答案。为什么是四分之一?因为在所有的int数值中有一半都是负数而isOdd方法对于对所有负奇数的判断都会失败。在任何负整数上调用该方法都回返回false不管该整数是偶数还是奇数。这是Java对取余操作符()的定义所产生的后果。该操作符被定义为对于所有的int数值a和所有的非零int数值b都满足下面的恒等式:(ab)*b(ab)==a换句话说如果你用b整除a将商乘以b然后加上余数那么你就得到了最初的值a。该恒等式具有正确的含义但是当与Java的截尾整数整除操作符相结合时它就意味着:当取余操作返回一个非零的结果时它与左操作数具有相同的正负符号。当i是一个负奇数时i等于而不是因此isOdd方法将错误地返回false。为了防止这种意外请测试你的方法在为每一个数值型参数传递负数、零和正数数值时其行为是否正确。这个问题很容易订正。只需将i与而不是与比较并且反转比较的含义即可:publicstaticbooleanisOdd(inti){returni!=}如果你正在在一个性能临界(performancecritical)环境中使用isOdd方法那么用位操作符AND()来替代取余操作符会显得更好:publicstaticbooleanisOdd(inti){return(i)!=}总之无论你何时使用到了取余操作符都要考虑到操作数和结果的符号。该操作符的行为在其操作数非负时是一目了然的但是当一个或两个操作数都是负数时它的行为就不那么显而易见了。谜题谜题谜题谜题::::找零时刻找零时刻找零时刻找零时刻请考虑下面这段话所描述的问题:Tom在一家汽车配件商店购买了一个价值$的火花塞但是他钱包中都是两美元一张的钞票。如果他用一张两美元的钞票支付这个火花塞那么应该找给他多少零钱呢?下面是一个试图解决上述问题的程序它会打印出什么呢?publicclassChange{publicstaticvoidmain(Stringargs){Systemoutprintln()}}你可能会很天真地期望该程序能够打印出但是它如何才能知道你想要打印小数点后两位小数呢?如果你对在DoubletoString文档中所设定的将double类型的值转换为字符串的规则有所了解你就会知道该程序打印出来的小数是足以将double类型的值与最靠近它的临近值区分出来的最短的小数它在小数点之前和之后都至少有一位。因此看起来该程序应该打印是合理的。这么分析可能显得很合理但是并不正确。如果你运行该程序你就会发现它打印的是。问题在于这个数字不能被精确表示成为一个double因此它被表示成为最接近它的double值。该程序从中减去的就是这个值。遗憾的是这个计算的结果并不是最接近的double值。表示结果的double值的最短表示就是你所看到的打印出来的那个可恶的数字。更一般地说问题在于并不是所有的小数都可以用二进制浮点数来精确表示的。如果你正在用的是JDK或更新的版本那么你可能会受其诱惑通过使用printf工具来设置输出精度的方订正该程序:拙劣的解决方案仍旧是使用二进制浮点数Systemoutprintf("fn",)这条语句打印的是正确的结果但是这并不表示它就是对底层问题的通用解决方案:它使用的仍旧是二进制浮点数的double运算。浮点运算在一个范围很广的值域上提供了很好的近似但是它通常不能产生精确的结果。二进制浮点对于货币计算是非常不适合的因为它不可能将或者的其它任何次负幂精确表示为一个长度有限的二进制小数解决该问题的一种方式是使用某种整数类型例如int或long并且以分为单位来执行计算。如果你采纳了此路线请确保该整数类型大到足够表示在程序中你将要用到的所有值。对这里举例的谜题来说int就足够了。下面是我们用int类型来以分为单位表示货币值后重写的println语句。这个版本将打印出正确答案分:Systemoutprintln(()"cents")解决该问题的另一种方式是使用执行精确小数运算的BigDecimal。它还可以通过JDBC与SQLDECIMAL类型进行互操作。这里要告诫你一点:一定要用BigDecimal(String)构造器而千万不要用BigDecimal(double)。后一个构造器将用它的参数的“精确”值来创建一个实例:newBigDecimal()将返回一个表示的BigDecimal。通过正确使用BigDecimal程序就可以打印出我们所期望的结果:importjavamathBigDecimalpublicclassChange{publicstaticvoidmain(Stringargs){Systemoutprintln(newBigDecimal("")subtract(newBigDecimal("")))}}这个版本并不是十分地完美因为Java并没有为BigDecimal提供任何语言上的支持。使用BigDecimal的计算很有可能比那些使用原始类型的计算要慢一些对某些大量使用小数计算的程序来说这可能会成为问题而对大多数程序来说这显得一点也不重要。总之在需要精确答案的地方要避免使用float和double对于货币计算要使用int、long或BigDecimal。对于语言设计者来说应该考虑对小数运算提供语言支持。一种方式是提供对操作符重载的有限支持以使得运算符可以被塑造为能够对数值引用类型起作用例如BigDecimal。另一种方式是提供原始的小数类型就像COBOL与PLI所作的一样。谜题谜题谜题谜题::::长整除长整除长整除长整除这个谜题之所以被称为长整除是因为它所涉及的程序是有关两个long型数值整除的。被除数表示的是一天里的微秒数而除数表示的是一天里的毫秒数。这个程序会打印出什么呢?publicclassLongDivision{publicstaticvoidmain(Stringargs){finallongMICROSPERDAY=****finallongMILLISPERDAY=***Systemoutprintln(MICROSPERDAYMILLISPERDAY)}}这个谜题看起来相当直观。每天的毫秒数和每天的微秒数都是常量。为清楚起见它们都被表示成积的形式。每天的微秒数是(小时天*分钟小时*秒分钟*毫秒秒*微秒毫秒)。而每天的毫秒数的不同之处只是少了最后一个因子。当你用每天的毫秒数来整除每天的微秒数时除数中所有的因子都被约掉了只剩下这正是每毫秒包含的微秒数。除数和被除数都是long类型的long类型大到了可以很容易地保存这两个乘积而不产生溢出。因此看起来程序打印的必定是。遗憾的是它打印的是。这里到底发生了什么呢?问题在于常数MICROSPERDAY的计算“确实”溢出了。尽管计算的结果适合放入long中并且其空间还有富余但是这个结果并不适合放入int中。这个计算完全是以int运算来执行的并且只有在运算完成之后其结果才被提升到long而此时已经太迟了:计算已经溢出了它返回的是一个小了倍的数值。从int提升到long是一种拓宽原始类型转换(wideningprimitiveconversion)它保留了(不正确的)数值。这个值之后被MILLISPERDAY整除而MILLISPERDAY的计算是正确的因为它适合int运算。这样整除的结果就得到了。那么为什么计算会是以int运算来执行的呢?因为所有乘在一起的因子都是int数值。当你将两个int数值相乘时你将得到另一个int数值。Java不具有目标确定类型的特性这是一种语言特性其含义是指存储结果的变量的类型会影响到计算所使用的类型。通过使用long常量来替代int常量作为每一个乘积的第一个因子我们就可以很容易地订正这个程序。这样做可以强制表达式中所有的后续计算都用long运作来完成。尽管这么做只在MICROSPERDAY表达式中是必需的但是在两个乘积中都这么做是一种很好的方式。相似地使用long作为乘积的“第一个”数值也并不总是必需的但是这么做也是一种很好的形式。在两个计算中都以long数值开始可以很清楚地表明它们都不会溢出。下面的程序将打印出我们所期望的:publicclassLongDivision{publicstaticvoidmain(Stringargs){finallongMICROSPERDAY=L****finallongMILLISPERDAY=L***Systemoutprintln(MICROSPERDAYMILLISPERDAY)}}这个教训很简单:当你在操作很大的数字时千万要提防溢出它可是一个缄默杀手。即使用来保存结果的变量已显得足够大也并不意味着要产生结果的计算具有正确的类型。当你拿不准时就使用long运算来执行整个计算。语言设计者从中可以吸取的教训是:也许降低缄默溢出产生的可能性确实是值得做的一件事。这可以通过对不会产生缄默溢出的运算提供支持来实现。程序可以抛出一个异常而不是直接溢出就像Ada所作的那样或者它们可以在需要的时候自动地切换到一个更大的内部表示上以防止溢出就像Lisp所作的那样。这两种方式都可能会遭受与其相关的性能方面的损失。降低缄默溢出的另一种方式是支持目标确定类型但是这么做会显著地增加类型系统的复杂度谜题谜题谜题谜题::::初级问题初级问题初级问题初级问题得啦前面那个谜题是有点棘手但它是有关整除的每个人都知道整除是很麻烦的。那么下面的程序只涉及加法它又会打印出什么呢?publicclassElementary{publicstaticvoidmain(Stringargs){Systemoutprintln(l)}}从表面上看这像是一个很简单的谜题简单到不需要纸和笔你就可以解决它。加号的左操作数的各个位是从到升序排列的而右操作数是降序排列的。因此相应各位的和仍然是常数程序必定打印。对于这样的分析只有一个问题:当你运行该程序时它打印出的是。难道是Java对打印这样的非常数字抱有偏见吗?不知怎么的这看起来并不像是一个合理的解释。事物往往有别于它的表象。就以这个问题为例它并没有打印出我们想要的输出。请仔细观察操作符的两个操作数我们是将一个int类型的加到了long类型的l上。请注意左操作数开头的数字和右操作数结尾的小写字母l之间的细微差异。数字的水平笔划(称为“臂(arm)”)和垂直笔划(称为“茎(stem)”)之间是一个锐角而与此相对照的是小写字母l的臂和茎之间是一个直角。在你大喊“恶心!”之前你应该注意到这个问题确实已经引起了混乱这里确实有一个教训:在long型字面常量中一定要用大写的L千万不要用小写的l。这样就可以完全掐断这个谜题所产生的混乱的源头。Systemoutprintln(L)相类似的要避免使用单独的一个l字母作为变量名。例如我们很难通过观察下面的代码段来判断它到底是打印出列表l还是数字。不良代码使用了l作为变量名Listl=newArrayList<String>()ladd("Foo")Systemoutprintln()总之小写字母l和数字在大多数打字机字体中都是几乎一样的。为避免你的程序的读者对二者产生混淆千万不要使用小写的l来作为long型字面常量的结尾或是作为变量名。Java从C编程语言中继承良多包括long型字面常量的语法。也许当初允许用小写的l来编写long型字面常量本身就是一个错误。谜题谜题谜题谜题::::十六进制的趣事十六进制的趣事十六进制的趣事十六进制的趣事下面的程序是对两个十六进制(hex)字面常量进行相加然后打印出十六进制的结果。这个程序会打印出什么呢?publicclassJoyOfHex{publicstaticvoidmain(Stringargs){Systemoutprintln(LongtoHexString(xLxcafebabe))}}看起来很明显该程序应该打印出cafebabe。毕竟这确实就是十六进制数字与cafebabe的和。该程序使用的是long型运算它可以支持位十六进制数因此运算溢出是不可能的。然而如果你运行该程序你就会发现它打印出来的是cafebabe并没有任何前导的。这个输出表示的是正确结果的低位但是不知何故第位丢失了。看起来程序好像执行的是int型运算而不是long型运算或者是忘了加第一个操作数。这里到底发生了什么呢?十进制字面常量具有一个很好的属性即所有的十进制字面常量都是正的而十六进制或是八进制字面常量并不具备这个属性。要想书写一个负的十进制常量可以使用一元取反操作符()连接一个十进制字面常量。以这种方式你可以用十进制来书写任何int或long型的数值不管它是正的还是负的并且负的十进制常数可以很明确地用一个减号符号来标识。但是十六进制和八进制字面常量并不是这么回事它们可以具有正的以及负的数值。如果十六进制和八进制字面常量的最高位被置位了那么它们就是负数。在这个程序中数字xcafebabe是一个int常量它的最高位被置位了所以它是一个负数。它等于十进制数值。该程序执行的这个加法是一种“混合类型的计算(mixedtypecomputation):左操作数是long类型的而右操作数是int类型的。为了执行该计算Java将int类型的数值用拓宽原始类型转换提升为一个long类型然后对两个long类型数值相加。因为int是一个有符号的整数类型所以这个转换执行的是符合扩展:它将负的int类型的数值提升为一个在数值上相等的long类型数值。这个加法的右操作数xcafebabe被提升为了long类型的数值xffffffffcafebabeL。这个数值之后被加到了左操作数xL上。当作为int类型来被审视时经过符号扩展之后的右操作数的高位是而左操作数的高位是将这两个数值相加就得到了这也就解释了为什么在程序输出中前导丢失了。下面所示是用手写的加法实现。(在加法上面的数字是进位。)xffffffffcafebabeLxLxcafebabeL订正该程序非常简单只需用一个long十六进制字面常量来表示右操作数即可。这就可以避免了具有破坏力的符号扩展并且程序也就可以打印出我们所期望的结果cafebabe:publicclassJoyOfHex{publicstaticvoidmain(Stringargs){Systemoutprintln(LongtoHexString(xLxcafebabeL))}}这个谜题给我们的教训是:混合类型的计算可能会产生混淆尤其是十六进制和八进制字面常量无需显式的减号符号就可以表示负的数值。为了避免这种窘境通常最好是避免混合类型的计算。对于语言的设计者们来说应该考虑支持无符号的整数类型从而根除符号扩展的可能性。可能会有这样的争辩:负的十六进制和八进制字面常量应该被禁用但是这可能会挫伤程序员他们经常使用十六进制字面常量来表示那些符号没有任何重要含义的数值。谜题谜题谜题谜题::::多重转型多重转型多重转型多重转型转型被用来将一个数值从一种类型转换到另一种类型。下面的程序连续使用了三个转型。那么它到底会打印出什么呢?publicclassMulticast{publicstaticvoidmain(Stringargs){Systemoutprintln((int)(char)(byte))}}无论你怎样分析这个程序都会感到很迷惑。它以int数值开始然后从int转型为byte之后转型为char最后转型回int。第一个转型将数值从位窄化到了位第二个转型将数值从位拓宽到了位最后一个转型又将数值从位拓宽回了位。这个数值最终是回到了起点吗?如果你运行该程序你就会发现不是。它打印出来的是但是这是为什么呢?该程序的行为紧密依赖于转型的符号扩展行为。Java使用了基于的补码的二进制运算因此int类型的数值的所有位都是置位的。从int到byte的转型是很简单的它执行了一个窄化原始类型转化(narrowingprimitiveconversion)直接将除低位之外的所有位全部砍掉。这样做留下的是一个位都被置位了的byte它仍旧表示。从byte到char的转型稍微麻烦一点因为byte是一个有符号类型而char是一个无符号类型。在将一个整数类型转换成另一个宽度更宽的整数类型时通常是可以保持其数值的但是却不可能将一个负的byte数值表示成一个char。因此从byte到char的转换被认为不是一个拓宽原始类型的转换而是一个拓宽并窄化原始类型的转换(wideningandnarrowingprimitiveconversion):byte被转换成了int而这个int又被转换成了char。所有这些听起来有点复杂幸运的是有一条很简单的规则能够描述从较窄的整型转换成较宽的整型时的符号扩展行为:如果最初的数值类型是有符号的那么就执行符号扩展如果它是char那么不管它将要被转换成什么类型都执行零扩展。了解这条规则可以使我们很容易地解决这个谜题。因为byte是一个有符号的类型所以在将byte数值转换成char时会发生符号扩展。作为结果的char数值的个位就都被置位了因此它等于即。从char到int的转型也是一个拓宽原始类型转换所以这条规则告诉我们它将执行零扩展而不是符号扩展。作为结果的int数值也就成了这正是程序打印出的结果。尽管这条简单的规则描述了在有符号和无符号整型之间进行拓宽原始类型时的符号扩展行为你最好还是不要编写出依赖于它的程序。如果你正在执行一个转型到char或从char转型的拓宽原始类型转换并且这个char是仅有的无符号整型那么你最好将你的意图明确地表达出来。如果你在将一个char数值c转型为一个宽度更宽的类型并且你不希望有符号扩展那么为清晰表达意图可以考虑使用一个位掩码即使它并不是必需的:inti=cxffff或者书写一句注释来描述转换的行为:inti=c不会执行符号扩展如果你在将一个char数值c转型为一个宽度更宽的整型并且你希望有符号扩展那么就先将char转型为一个short它与char具有同样的宽度但是它是有符号的。在给出了这种细微的代码之后你应该也为它书写一句注释:inti=(short)c转型将引起符号扩展如果你在将一个byte数值b转型为一个char并且你不希望有符号扩展那么你必须使用一个位掩码来限制它。这是一种通用做法所以不需要任何注释:charc=(char)(bxff)这个教训很简单:如果你通过观察不能确定程序将要做什么那么它做的就很有可能不是你想要的。要为明白清晰地表达你的意图而努力。尽管有这么一条简单的规则描述了涉及有符号和无符号整型拓宽转换的符号扩展行为但是大多数程序员都不知道它。如果你的程序依赖于它那么你就应该把你的意图表达清楚。谜题谜题谜题谜题::::互换内容互换内容互换内容互换内容下面的程序使用了复合的异或赋值操作符它所展示的技术是一种编程习俗。那么它会打印出什么呢?publicclassCleverSwap{publicstaticvoidmain(Stringargs){intx=(xc)inty=(xd)x^=y^=x^=ySystemoutprintln("x="x"y="y)}}就像其名称所暗示的这个程序应该交换变量x和y的值。如果你运行它就会发现很悲惨它失败了打印的是x=y=。交换两个变量的最显而易见的方式是使用一个临时变量:inttmp=xx=yy=tmp很久以前当中央处理器只有少数寄存器时人们发现可以通过利用异或操作符(^)的属性(x^y^x)==y来避免使用临时变量:x=x^yy=y^xx=y^x这个惯用法曾经在C编程语言中被使用过并进一步被构建到了C中但是它并不保证在二者中都可以正确运行。但是有一点是肯定的那就是它在Java中肯定是不能正确运行的。Java语言规范描述到:操作符的操作数是从左向右求值的。为了求表达式x^=expr的值x的值是在计算expr之前被提取的并且这两个值的异或结果被赋给变量x。在CleverSwap程序中变量x的值被提取了两次每次在表达式中出现时都提取一次但是两次提取都发生在所有的赋值操作之前。下面的代码段详细地描述了将互换惯用法分解开之后的行为并且解释了为什么产生的是我们所看到的输出:Java中x^=y^=x^=y的实际行为inttmp=xx在表达式中第一次出现inttmp=yy的第一次出现inttmp=x^y计算x^yx=tmp最后一个赋值:存储x^y到xy=tmp^tmp第二个赋值:存储最初的x值到y中x=tmp^y第一个赋值:存储到x中在C和C中并没有指定表达式的计算顺序。当编译表达式x^=expr时许多C和C编译器都是在计算expr之后才提取x的值的这就使得上述的惯用法可以正常运转。尽管它可以正常运转但是它仍然违背了CC有关不能在两个连续的序列点之间重复修改变量的规则。因此这个惯用法的行为在C和C中也没有明确定义。为了看重其价值我们还是可以写出不用临时变量就可以互换两个变量内容的Java表达式的。但是它同样是丑陋而无用的:杀鸡用牛刀的做法千万不要这么做!y=(x^=(y^=x))^y这个教训很简单:在单个的表达式中不要对相同的变量赋值两次。表达式如果包含对相同变量的多次赋值就会引起混乱并且很少能够执行你希望的操作。即使对多个变量进行赋值也很容易出错。更一般地讲要避免所谓聪明的编程技巧。它们都是易于产生bug的很难以维护并且运行速度经常是比它们所替代掉的简单直观的代码要慢。语言设计者可能会考虑禁止在一个表达式中对相同的变量多次赋值但是在一般的情况下强制执行这条禁令会因为别名机制的存在而显得很不灵活。例如请考虑表达式x=aiaj它是否递增了相同的变量两次呢?这取决于在表达式被计算时i和j的值并且编译器通常是无法确定这一点。谜题谜题谜题谜题::::DosEquisDosEquisDosEquisDosEquis这个谜题将测试你对条件操作符的掌握程度这个操作符有一个更广为人知的名字:问号冒号操作符。下面的程序将会打印出什么呢?publicclassDosEquis{publicstaticvoidmain(Stringargs){charx='X'inti=Systemoutprintln(truex:)Systemoutprintln(falsei:x)}}这个程序由两个变量声明和两个print语句构成。第一个print语句计算条件表达式(truex:)并打印出结果这个结果是char类型变量x的值’X’。而第二个print语句计算表达式(falsei:x)并打印出结果这个结果还是依旧是’X’的x因此这个程序应该打印XX。然而如果你运行该程序你就会发现它打印出来的是X。这种行为看起来挺怪的。第一个print语句打印的是X而第二个打印的却是。它们的不同行为说明了什么呢?答案就在规范有关条件表达式部分的一个阴暗的角落里。请注意在这两个表达式中每一个表达式的第二个和第三个操作数的类型都不相同:x是char类型的而和i都是int类型的。就像在谜题的解答中提到的混合类型的计算会引起混乱而这一点比在条件表达式中比在其它任何地方都表现得更明显。你可能考虑过这个程序中两个条件表达式的结果类型是相同的就像它们的操作数类型是相同的一样尽管操作数的顺序颠倒了一下但是实际情况并非如此。确定条件表达式结果类型的规则过于冗长和复杂很难完全记住它们但是其核心就是一下三点:•如果第二个和第三个操作数具有相同的类型那么它就是条件表达式的类型。换句话说你可以通过绕过混合类型的计算来避免大麻烦。•如果一个操作数的类型是TT表示byte、short或char而另一个操作数是一个int类型的常量表达式它的值是可以用类型T表示的那么条件表达式的类型就是T。•否则将对操作数类型运用二进制数字提升而条件表达式的类型就是第二个和第三个操作数被提升之后的类型。、两点对本谜题是关键。在程序的两个条件表达式中一个操作数的类型是char另一个的类型是int。在两个表达式中int操作数都是它可以被表示成一个char。然而只有第一个表达式中的int操作数是常量()而第二个表达式中的int操作数是变量(i)。因此第点被应用到了第一个表达式上它返回的类型是char而第点被应用到了第二个表达式上其返回的类型是对int和char运用了二进制数字提升之后的类型即int。条件表达式的类型将确定哪一个重载的print方法将被调用。对第一个表达式来说PrintStreamprint(char)将被调用而对第二个表达式来说PrintStreamprint(int)将被调用。前一个重载方法将变量x的值作为Unicode字符(X)来打印而后一个重载方法将其作为一个十进制整数()来打印。至此谜题被解开了。总之通常最好是在条件表达式中使用类型相同的第二和第三操作数。否则你和你的程序的读者必须要彻底理解这些表达式行为的复杂规范。对语言设计者来说也许可以设计一个牺牲掉了部分灵活性但是增加了简洁性的条件操作符。例如要求第二和第三操作数必须就有相同的类型这看起来就很合理。或者条件操作符可以被定义为对常量没有任何特殊处理。为了让这些选择对程序员来说更加容易接受可以提供用来表示所有原始类型字面常量的语法。这也许确实是一个好注意因为它增加了语言的一致性和完备性同时又减少了对转型的需求。谜题谜题谜题谜题::::半斤半斤半斤半斤现在该轮到你来写些代码了好消息是你只需为这个谜题编写两行代码并为下一个谜题也编写两行代码。这有什么难的呢?我们给出一个对变量x和i的声明即可它肯定是一个合法的语句:x=i但是它并不是:x=xi许多程序员都会认为该迷题中的第一个表达式(x=i)只是第二个表达式(x=xi)的简写方式。但是这并不十分准确。这两个表达式都被称为赋值表达式。第二条语句使用的是简单赋值操作符(=)而第一条语句使用的是复合赋值操作符。(复合赋值操作符包括=、=、*=、=、=、<<=、>>=、>>>=、=、^=和|=)Java语言规范中讲到复合赋值Eop=E等价于简单赋值E=(T)((E)op(E))其中T是E的类型除非E只被计算一次。换句话说复合赋值表达式自动地将它们所执行的计算的结果转型为其左侧变量的类型。如果结果的类型与该变量的类型相同那么这个转型不会造成任何影响。然而如果结果的类型比该变量的类型要宽那么复合赋值操作符将悄悄地执行一个窄化原始类型转换。因此我们有很好的理由去解释为什么在尝试着执行等价的简单赋值可能会产生一个编译错误。为了说得具体一些并提供一个解决方案给这个谜题假设我们在该谜题的两个赋值表达式之前有下面这些声明:shortx=inti=复合赋值编译将不会产生任何错误:x=i包含了一个隐藏的转型!你可能期望x的值在这条语句执行之后是,但是并非如此l它的值是,。int类型的数值对于short来说太大了。自动产生的转型悄悄地把int数值的高两位给截掉了。这也许就不是你想要的了。相对应的简单赋值是非法的因为它试图将int数值赋值给short变量它需要一个显式的转型:x=xi不要编译“可能会丢掉精度”这应该是明显的复合赋值表达式可能是很危险的。为了避免这种令人不快的突袭请不要将复合赋值操作符作用于byte、short或char类型的变量上。在将复合赋值操作符作用于int类型的变量上时要确保表达式右侧不是long、float或double类型。在将复合赋值操作符作用于float类型的变量上时要确保表达式右侧不是double类型。这些规则足以防止编译器产生危险的窄化转型。总之复合赋值操作符会悄悄地产生一个转型。如果计算结果的类型宽于变量的类型那么所产生的转型就是一个危险的窄化转型。这样的转型可能会悄悄地丢弃掉精度或数量值。对语言设计者来说也许让复合赋值操作符产生一个不可见的转型本身就是一个错误对于在复合赋值中的变量类型比计算结果窄的情况也许应该让其非法才对。谜题谜题谜题谜题::::八两八两八两八两与上面的例子相反如果我们给出的关于变量x和i的声明是如下的合法语句:x=xi但是它并不是:x=i乍一看这个谜题可能看起来与前面一个谜题相同。但是请放心它们并不一样。这两个谜题在哪一条语句必是合法的以及哪一条语句必是不合法的方面正好相反。就像前面的谜题一样这个谜题也依赖于有关复合赋值操作符的规范中的细节。二者的相似之处就此打住。基于前面的谜题你可能会想:符合赋值操作符比简单赋值操作符的限制要少一些。在一般情况下这是对的但是有这么一个领域在其中简单赋值操作符会显得更宽松一些。复合赋值操作符要求两个操作数都是原始类型的例如int或包装了的原始类型例如Integer但是有一个例外:如果在=操作符左侧的操作数是String类型的那么它允许右侧的操作数是任意类型在这种情况下该操作符执行的是字符串连接操作。简单赋值操作符(=)允许其左侧的是对象引用类型这就显得要宽松许多了:你可以使用它们来表示任何你想要表示的内容只要表达式的右侧与左侧的变量是赋值兼容的即可。你可以利用这一差异来解决该谜题。要想用=操作符来执行字符串连接操作你就必须将左侧的变量声明为String类型。通过使用直接赋值操作符字符串连接的结果可以存放到一个Object类型的变量中。为了说得具体一些并提供一个解决方案给这个谜题假设我们在该谜题的两个赋值表达式之前有下面这些声明:Objectx="Buy"Stringi="EffectiveJava!"简单赋值是合法的因为xi是String类型的而String类型又是与Object赋值兼容的:x=xi复合赋值是非法的因为左侧是一个Object引用类型而右侧是一个String类型:x=i这个谜题对程序员来说几乎算不上什么教训。对语言设计者来说加法的复合赋值操作符应该在右侧是String类型的情况下允许左侧是Object类型。这项修改将根除这个谜题所展示的违背直觉的行为。JavaJavaJavaJava谜题谜题谜题谜题字符谜题字符谜题字符谜题字符谜题谜题谜题谜题谜题::::最后的笑声最后的笑声最后的笑声最后的笑声下面的程序将打印出什么呢?publicclassLastLaugh{publicstaticvoidmain(Stringargs){Systemoutprint("H""a")Systemoutprint('H''a')}}你可能会认为这个程序将打印HaHa。该程序看起来好像是用两种方式连接了H和a但是你所见为虚。如果你运行这个程序就会发现它打印的是Ha。那么为什么它会产生这样的行为呢?正如我们所期望的第一个对Systemoutprint的调用打印的是Ha:它的参数是表达式"H""a"显然它执行的是一个字符串连接。而第二个对Systemoutprint的调用就是另外一回事了。问题在于'H'和'a'是字符型字面常量因为这两个操作数都不是字符串类型的所以操作符执行的是加法而不是字符串连接。编译器在计算常量表达式'H''a'时是通过我们熟知的拓宽原始类型转换将两个具有字符型数值的操作数('H'和'a')提升为int数值而实现的。从char到int的拓宽原始类型转换是将位的char数值零扩展到位的int。对于'H'char数值是而对于'a'char数值是因此表达式'H''a'等价于int常量或。站在语言的立场上若干个char和字符串的相似之处是虚幻的。语言所关心的是char是一个无符号位原始类型整数仅此而已。对类库来说就不尽如此了类库包含了许多可以接受char参数并将其作为Unicode字符处理的方法。那么你应该怎样将字符连接在一起呢?你可以使用这些类库。例如你可以使用一个字符串缓冲区:StringBuffersb=newStringBuffer()sbappend('H')sbappend('a')Systemoutprintln(sb)这么做可以正常运行但是显得很丑陋。其实我们还是有办法去避免这种方式所产生的拖沓冗长的代码。你可以通过确保至少有一个操作数为字符串类型来强制操作符去执行一个字符串连接操作而不是一个加法操作。这种常见的惯用法用一个空字符串("")作为一个连接序列的开始如下所示:Systemoutprintln(""'H''a')这种惯用法可以确保子表达式都被转型为字符串。尽管这很有用但是多少有一点难看而且它自身可能会引发某些混淆。你能猜到下面的语句将会打印出什么吗?如果你不能确定那么就试一下:Systemoutprint("=")如果使用的是JDK你还可以使用Systemoutprintf("cc",'H','a')总之使用字符串连接操作符使用格外小心。操作符当且仅当它的操作数中至少有一个是String类型时才会执行字符串连接操作否则它执行的就是加法。如果要连接的没有一个数值是字符串类型的那么你可以有几种选择:•预置一个空字符串•将第一个数值用StringvalueOf显式地转换成一个字符串•使用一个字符串缓冲区•或者如果你使用的JDK可以用printf方法。这个谜题还包含了一个给语言设计者的教训。操作符重载即使在Java中只在有限的范围内得到了支持它仍然会引起混淆。为字符串连接而重载操作符可能就是一个已铸成的错误。谜题谜题谜题谜题::::ABCABCABCABC这个谜题要问的是一个悦耳的问题下面的程序将打印什么呢?publicclassABC{publicstaticvoidmain(Stringargs){Stringletters="ABC"charnumbers={'','',''}Systemoutprintln(letters"easyas"numbers)}}可能大家希望这个程序打印出ABCeasyas。遗憾的是它没有。如果你运行它就会发现它打印的是诸如ABCeasyasCf之类的东西。为什么这个输出会如此丑陋?尽管char是一个整数类型但是许多类库都对其进行了特殊处理因为char数值通常表示的是字符而不是整数。例如将一个char数值传递给println方法会打印出一个Unicode字符而不是它的数字代码。字符数组受到了相同的特殊处理:println的char重载版本会打印出数组所包含的所有字符而StringvalueOf和StringBufferappend的char重载版本的行为也是类似的。然而字符串连接操作符在这些方法中没有被定义。该操作符被定义为先对它的两个操作数执行字符串转换然后将产生的两个字符串连接到一起。对包括数组在内的对象引用的字符串转换定义如下JLS:如果引用为它将被转换成字符串""。否则该转换的执行就像是不用任何参数调用该引用对象的toString方法一样但是如果调用toString方法的结果是那么就用字符串""来代替。那么在一个非空char数组上面调用toString方法会产生什么样的行为呢?数组是从Object那里继承的toString方法JLS规范中描述到:“返回一个字符串它包含了该对象所属类的名字''符号以及表示对象散列码的一个无符号十六进制整数”JavaAPI。有关ClassgetName的规范描述到:在char类型的类对象上调用该方法的结果为字符串"C"。将它们连接到一起就形成了在我们的程序中打印出来的那个丑陋的字符串。有两种方法可以订正这个程序。你可以在调用字符串连接操作之前显式地将一个数组转换成一个字符串:Systemoutprintln(letters"easyas"StringvalueOf(numbers))或者你可以将Systemoutprintln调用分解为两个调用以利用println的char重载版本:Systemoutprint(letters"easyas")Systemoutprintln(numbers)请注意这些订正只有在你调用了valueOf和println方法正确的重载版本的情况下才能正常运行。换句话说它们严格依赖于数组引用的编译期类型。下面的程序说明了这种依赖性。看起来它像是所描述的第二种订正方式的具体实现但是它产生的输出却与最初的程序所产生的输出一样丑陋因为它调用的是println的Object重载版本而不是char重载版本。classABC{publicstaticvoidmain(Stringargs){Stringletters="ABC"Objectnumbers=newchar{'','',''}Systemoutprint(letters"easyas")Systemoutprintln(numbers)}}总之char数组不是字符串。要想将一个char数组转换成一个字符串就要调用StringvalueOf(char)方法。某些类库中的方法提供了对char数组的类似字符串的支持通常是提供一个Object版本的重载方法和一个char版本的重载方法而之后后者才能产生我们想要的行为。对语言设计者的教训是:char类型可能应该覆写toString方法使其返回数组中包含的字符。更一般地讲数组类型可能都应该覆写toString方法使其返回数组内容的一个字符串表示。谜题谜题谜题谜题::::畜牧场畜牧场畜牧场畜牧场GeorgeOrwell的《畜牧场(AnimalFarm)》一书的读者可能还记得老上校的宣言:“所有的动物都是平等的。”

类似资料

编辑推荐

水经注校证[北魏]郦道元 着 陈桥驿 校证.pdf

乐府诗集(宋)郭茂倩.pdf

叶圣陶集 第七卷.pdf

叶圣陶集 第六卷.pdf

西方六大美学观念史.pdf

职业精品

精彩专题

上传我的资料

精选资料

热门资料排行换一换

  • 《雪洞:喜马拉雅山上的悟道历程》…

  • 《中国民间崇拜 佛教传说》(法)…

  • 《中国佛教通史 第3卷》(日)镰…

  • 《中国佛教通史 第1卷》(日)镰…

  • 张帆_中国古代简史_北京大学.p…

  • 《印度佛教史》(英)渥德尔着19…

  • 《行历抄校注》白化文,李鼎霞校注…

  • 《唐代佛教》(美)威斯坦因着.p…

  • 《弥勒净土论》(日)松本文三郎着…

  • 资料评价:

    / 162
    所需积分:2 立即下载

    意见
    反馈

    返回
    顶部