您好,登錄后才能下訂單哦!
這篇“Java語言表達(dá)式的五個謎題是什么”文章的知識點(diǎn)大部分人都不太理解,所以小編給大家總結(jié)了以下內(nèi)容,內(nèi)容詳細(xì),步驟清晰,具有一定的借鑒價值,希望大家閱讀完這篇文章能有所收獲,下面我們一起來看看這篇“Java語言表達(dá)式的五個謎題是什么”文章吧。
下面的方法意圖確定它那唯一的參數(shù)是否是一個奇數(shù)。這個方法能夠正確運(yùn)轉(zhuǎn)嗎?
public static boolean isOdd(int i){ returni%2==1: }
奇數(shù)可以被定義為被2整除余數(shù)為1的整數(shù)。表達(dá)式i%2計算的是i整除2時所產(chǎn)生的余數(shù),因此看起來這個程序應(yīng)該能夠正確運(yùn)轉(zhuǎn)。遺憾的是,它不能;它在四分之一的時間里返回的都是錯誤的答案。
為什么是四分之一?因?yàn)樵谒械膇nt數(shù)值中,有一半都是負(fù)數(shù),而isOdd方法對于對所有負(fù)奇數(shù)的判斷都會失敗。在任何負(fù)整數(shù)上調(diào)用該方法都回返回false,不管該整數(shù)是偶數(shù)還是奇數(shù)。這是Java對取余操作符(%)的定義所產(chǎn)生的后果。該操作符被定義為對于所有的int數(shù)值a和所有的非零int數(shù)值b,都滿足下面的恒等式:
(a/b)*b+(a%b)==a
換句話說,如果你用b整除a,將商乘以b,然后加上余數(shù),那么你就得到了最初的值a。該恒等式具有正確的含義,但是當(dāng)與Java的截尾整數(shù)整除操作符相結(jié)合時,它就意味著:當(dāng)取余操作返回一個非零的結(jié)果時,它與左操作數(shù)具有相同的正負(fù)符號。
當(dāng)i是一個負(fù)奇數(shù)時,i%2等于-1而不是1,因此isOdd方法將錯誤地返回false。為了防止這種意外,請測試你的方法在為每一個數(shù)值型參數(shù)傳遞負(fù)數(shù)、零和正數(shù)數(shù)值時,其行為是否正確。這個問題很容易訂正。只需將i%2與0而不是與1比較,并且反轉(zhuǎn)比較的含義即可:
public static boolean isOdd(inti){ returni%2!=0; }
如果你正在在一個性能臨界(performance-critical)環(huán)境中使用isOdd方法,那么用位操作符AND(&)來替代取余操作符會顯得更好:
public static boolean isOdd(inti){ return(i&1)!=0; }
總之,無論你何時使用到了取余操作符,都要考慮到操作數(shù)和結(jié)果的符號。該操作符的行為在其操作數(shù)非負(fù)時是一目了然的,但是當(dāng)一個或兩個操作數(shù)都是負(fù)數(shù)時,它的行為就不那么顯而易見
了。
請考慮下面這段話所描述的問題:
Tom在一家汽車配件商店購買了一個價值$1.10的火花塞,但是他錢包中都是兩美元一張的鈔票。如果他用一張兩美元的鈔票支付這個火花塞,那么應(yīng)該找給他多少零錢呢?
下面是一個試圖解決上述問題的程序,它會打印出什么呢?
public class Change{ public static void main(String args[]){ Systemoutprintln(2.00-1.10); } }
你可能會很天真地期望該程序能夠打印出0.90但是它如何才能知道你想要打印小數(shù)點(diǎn)后兩位小數(shù)呢?
如果你對在DoubletoString文檔中所設(shè)定的將 double類型的值轉(zhuǎn)換為字符串的規(guī)則有所了解你就會知道該程序打印出來的小數(shù),是足以將 double類型的值與最靠近它的臨近值區(qū)分出來的最短的小數(shù),它在小數(shù)點(diǎn)之前和之后都至少有一位。因此,看起來,該程序應(yīng)該打印0.9是合理的。
這么分析可能顯得很合理,但是并不正確。如果你運(yùn)行該程序,你就會發(fā)現(xiàn)它打印的是:
0.8999999999999999
問題在于1.1這個數(shù)字不能被精確表示成為一個 double,因此它被表示成為最接近它的double值。該程序從2中減去的就是這個值。遺憾的是,這個計算的結(jié)果并不是最接近0.9的double值。表示結(jié)果的double值的最短表示就是你所看到的打印出來的那個可惡的數(shù)字。
更一般地說,問題在于并不是所有的小數(shù)都可以用二進(jìn)制浮點(diǎn)數(shù)來精確表示的。
如果你正在用的是JDK5.0或更新的版本,那么你可能會受其誘惑,通過使用printf工具來設(shè)置輸出精度的方訂正該程序:
//拙劣的解決方案-仍舊是使用二進(jìn)制浮點(diǎn)數(shù)
System.out.printf("%.2f%n",2.00-1.10);
這條語句打印的是正確的結(jié)果,但是這并不表示它就是對底層問題的通用解決方案:它使用的仍日是二進(jìn)制浮點(diǎn)數(shù)的double運(yùn)算。浮點(diǎn)運(yùn)算在一個范圍很廣的值域上提供了很好的近似,但是它通常不能產(chǎn)生精確的結(jié)果。二進(jìn)制浮點(diǎn)對于貨幣計算是非常不適合的,因?yàn)樗豢赡軐?.1-或者10的其它任何次負(fù)冪--精確表示為一個長度有限的二進(jìn)制小數(shù)解決該問題的一種方式是使用某種整數(shù)類型,例如int或long,并且以分為單位來執(zhí)行計算。如果你采納了此路線,請確保該整數(shù)類型大到足夠表示在程序中你將要用到的所有值。對這里舉例的謎題來說,int就足夠了。下面是我們用int類型來以分為單位表示貨幣值后重寫的println語句。這個版本將打印出正確答案90分:
Systemoutprintln((200-110)+"cents")
解決該問題的另一種方式是使用執(zhí)行精確小數(shù)運(yùn)算的BigDecimal。它還可以通過JDBC與SQL DECIMAL類型進(jìn)行互操作。這里要告誡你一點(diǎn):一定要用BigDecimal(String)構(gòu)造器,而千萬不要用BigDecimal(double)。后一個構(gòu)造器將用它的參數(shù)的精確”值來創(chuàng)建一個實(shí)例:new BigDecimal(1)將返回一個表示0100000000000000055511151231257827021181583404541015625BigDecimal。通過正確使用BigDecimal,程序就可以打印出我們所期望的結(jié)果0.90:
import java.math.BigDecimal; public class Changel { public static void main(String args[]){ System.out.println(newBigDecimal(2.00") subtract(new BigDecimal("1.10"))); } }
這個版本并不是十分地完美,因?yàn)镴ava并沒有為 BigDecimal提供任何語言上的支持。使用
BigDecimal的計算很有可能比那些使用原始類型的計算要慢一些,對某些大量使用小數(shù)計算的程序來說,這可能會成為問題,而對大多數(shù)程序來說,這顯得一點(diǎn)也不重要。
總之,在需要精確答案的地方,要避免使用 float和double;對于貨幣計算,要使用int、long或BigDecimal。對于語言設(shè)計者來說,應(yīng)該考慮對小數(shù)運(yùn)算提供語言支持。一種方式是提供對操作符重載的有限支持,以使得運(yùn)算符可以被塑造為能夠?qū)?shù)值引用類型起作用,例如BigDecimal。另一種方式是提供原始的小數(shù)類型,就像COBOL與PL/I所作的一樣。
這個謎題之所以被稱為長整除是因?yàn)樗婕暗某绦蚴怯嘘P(guān)兩個long型數(shù)值整除的。被除數(shù)表示的是一天里的微秒數(shù);而除數(shù)表示的是一天里的毫秒數(shù)。這個程序會打印出什么呢?
public class Longpision{ 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; Systemoutprintln(MICROS PER DAY/ MILLIS PER DAY); } }
這個謎題看起來相當(dāng)直觀。每天的毫秒數(shù)和每天的微秒數(shù)都是常量。為清楚起見,它們都被表示成積的形式。每天的微秒數(shù)是(24小時/天*60分鐘/小時*60秒/分鐘*1000毫秒/秒*1000微秒/毫秒)。而每天的毫秒數(shù)的不同之處只是少了最后一個因子1000。當(dāng)你用每天的毫秒數(shù)來整除每天的微秒數(shù)時,除數(shù)中所有的因子都被約掉了,只剩下1000,這正是每毫秒包含的微秒數(shù)。
除數(shù)和被除數(shù)都是long類型的,long類型大到了可以很容易地保存這兩個乘積而不產(chǎn)生溢出。因此,看起來程序打印的必定是1000。遺憾的是,它打印的是5。這里到底發(fā)生了什么呢?
問題在于常數(shù)MICROS PER DAY的計算確實(shí)”溢出了。盡管計算的結(jié)果適合放入long中,并且其空間還有富余,但是這個結(jié)果并不適合放入 int中。這個計算完全是以int運(yùn)算來執(zhí)行的,并且只有在運(yùn)算完成之后,其結(jié)果才被提升到long,而此時已經(jīng)太遲了:計算已經(jīng)溢出了,它返回的是一個小了200倍的數(shù)值。從int提升到 long是一種拓寬原始類型轉(zhuǎn)換(widening primitive conversion),它保留了(不正確的)數(shù)值。這個值之后被MILLIS PER DAY整除,而MILLIS PER DAY的計算是正確的,因?yàn)樗m合int運(yùn)算。這樣整除的結(jié)果就得到了5。
那么為什么計算會是以int運(yùn)算來執(zhí)行的呢?為所有乘在一起的因子都是int數(shù)值。當(dāng)你將兩個int數(shù)值相乘時,你將得到另一個int數(shù)值。Java不具有目標(biāo)確定類型的特性,這是一種語言特性,其含義是指存儲結(jié)果的變量的類型會影響到計算所使用的類型。
通過使用long常量來替代int常量作為每一個乘積的第一個因子,我們就可以很容易地訂正這個程序。這樣做可以強(qiáng)制表達(dá)式中所有的后續(xù)計算都用long運(yùn)作來完成。盡管這么做只在MICROS PER DAY表達(dá)式中是必需的,但是在兩個乘積中都這么做是一種很好的方式。相似地,使用long作為乘積的“第一個”數(shù)值也并不總是必需的,但是這么做也是一種很好的形式。在兩個計算中都以long數(shù)值開始可以很清楚地表明它們都不會溢出。下面的程序?qū)⒋蛴〕鑫覀兯谕?000:
public class Longpision{ 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; SystemoutprintlnMICROS PER DAY MILLIS PER DAY); } }
這個教訓(xùn)很簡單:當(dāng)你在操作很大的數(shù)字時,千萬要提防溢出--它可是一個緘默殺手。即使用來保存結(jié)果的變量已顯得足夠大,也并不意味著要產(chǎn)生結(jié)果的計算具有正確的類型。當(dāng)你拿不準(zhǔn)時,就使用long運(yùn)算來執(zhí)行整個計算。
語言設(shè)計者從中可以吸取的教訓(xùn)是:也許降低默溢出產(chǎn)生的可能性確實(shí)是值得做的一件事。這可以通過對不會產(chǎn)生緘默溢出的運(yùn)算提供支持來實(shí)現(xiàn)。程序可以拋出一個異常而不是直接溢出。就像Ada所作的那樣,或者它們可以在需要的時候自動地切換到一個更大的內(nèi)部表示上以防止溢出,就像Lisp所作的那樣。這兩種方式都可能會遭受與其相關(guān)的性能方面的損失。降低緘默溢出的另一種方式是支持目標(biāo)確定類型,但是這么做會顯著地增加類型系統(tǒng)的復(fù)雜度。
得啦,前面那個謎題是有點(diǎn)棘手,但它是有關(guān)整除的,每個人都知道整除是很麻煩的。那么下面的程序只涉及加法,它又會打印出什么呢?
public class Elementary{ public static void main(String]args) { Systemoutprintln(12345+54321); } }
從表面上看,這像是一個很簡單的謎題--簡單到不需要紙和筆你就可以解決它。加號的左操作數(shù)的各個位是從1到5升序排列的,而右操作數(shù)是降序排列的。因此,相應(yīng)各位的和仍然是常數(shù),程序必定打印66666。對于這樣的分析,只有一個問題:當(dāng)你運(yùn)行該程序時,它打印出的是17777。難道是Java對打印這樣的非常數(shù)字抱有偏見嗎?不知怎么的,這看起來并不像是一個合理的解釋。
事物往往有別于它的表象。就以這個問題為例,它并沒有打印出我們想要的輸出。請仔細(xì)觀察+操作符的兩個操作數(shù),我們是將一個int類型的12345加到了long類型的54321上。請注意左操作數(shù)開頭的數(shù)字1和右操作數(shù)結(jié)尾的小寫字母1之間的細(xì)微差異。數(shù)字1的水平筆劃(稱為“臂(arm)”)和垂直筆劃(稱為“莖(stem)”)之間是一個銳角,而與此相對照的是,小寫字母l的臂和莖之間是一個直角。
在你大喊“惡心!”之前,你應(yīng)該注意到這個問題確實(shí)已經(jīng)引起了混亂,這里確實(shí)有一個教訓(xùn):在 long型字面常量中,一定要用大寫的L,千萬不要用小寫的1。這樣就可以完全掐斷這個謎題所產(chǎn)生的混亂的源頭。
System.out.println(12345+5432L);
相類似的,要避免使用單獨(dú)的一個1字母作為變量名。例如,我們很難通過觀察下面的代碼段來判斷它到底是打印出列表1還是數(shù)字1。
List l=new ArrayList<String>() ; l.add("Foo"); System.outprintln(1);
總之,小寫字母l和數(shù)字1在大多數(shù)打字機(jī)字體中都是幾乎一樣的。為避免你的程序的讀者對二者產(chǎn)生混淆,千萬不要使用小寫的1來作為long型字面常量的結(jié)尾或是作為變量名。Java從C編程語言中繼承良多,包括long型字面常量的語法。也許當(dāng)初允許用小寫的1來編寫long型字面常量本身就是一個錯誤。
下面的程序是對兩個十六進(jìn)制(hex)字面常量進(jìn)行相加,然后打印出十六進(jìn)制的結(jié)果。這個程序會打印出什么呢?
public class JoyOfHex{ public static void main(String[] args){ System.out.println( Long.toHexString(0x100000000L+0xcafebabe)); } }
看起來很明顯,該程序應(yīng)該打印出1cafebabe。畢竟,這確實(shí)就是十六進(jìn)制數(shù)字10000000016與 cafebabe16的和。該程序使用的是long型運(yùn)算,它可以支持16位十六進(jìn)制數(shù),因此運(yùn)算溢出是不可能的。
然而,如果你運(yùn)行該程序,你就會發(fā)現(xiàn)它打印出來的是cafebabe,并沒有任何前導(dǎo)的1。這個輸出表示的是正確結(jié)果的低32位,但是不知何故第33位丟失了。
看起來程序好像執(zhí)行的是int型運(yùn)算而不是long型運(yùn)算,或者是忘了加第一個操作數(shù)。這里到底發(fā)生了什么呢?
十進(jìn)制字面常量具有一個很好的屬性,即所有的十進(jìn)制字面常量都是正的,而十六進(jìn)制和八進(jìn)制字面常量并不具備這個屬性。要想書寫一個負(fù)的十進(jìn)制常量,可以使用一元取反操作符(-)連接一個十進(jìn)制字面常量。以這種方式,你可以用十進(jìn)制來書寫任何int或long型的數(shù)值,不管它是正的還是負(fù)的,并且負(fù)的十進(jìn)制常數(shù)可以很明確地用一個減號符號來標(biāo)識。但是十六進(jìn)制和八進(jìn)制字面常量并不是這么回事,它們可以具有正的以及負(fù)的數(shù)值。如果十六進(jìn)制和八進(jìn)制字面常量的最高位被置位了,那么它們就是負(fù)數(shù)。在這個程序中,數(shù)字Oxcafebabe是一個int常量,它的最高位被置位了,所以它是一個負(fù)數(shù)。它等于十進(jìn)制數(shù)值-889275714。
該程序執(zhí)行的這個加法是一種“混合類型的計算(mixed-type computation)左操作數(shù)是long類型的,而右操作數(shù)是int類型的。為了執(zhí)行該計算,Java將int類型的數(shù)值用拓寬原始類型轉(zhuǎn)換提升為一個long類型,然后對兩個long類型數(shù)值相加。因?yàn)閕nt是一個有符號的整數(shù)類型,所以這個轉(zhuǎn)換執(zhí)行的是符合擴(kuò)展:它將負(fù)的int類型的數(shù)值提升為一個在數(shù)值上相等的long類型數(shù)值。這個加法的右操作數(shù)0xcafebabe被提升為了long類型的數(shù)值0xffffffffcafebabeL。這個數(shù)值之后被加到了左操作數(shù)0x100000000L上。當(dāng)作為int類型來被審視時,經(jīng)過符號擴(kuò)展之后的右操作數(shù)的高32位是-1,而左操作數(shù)的高32位是1,將這兩個數(shù)相加就得到了0,這也就解釋為什么在程序輸出中前導(dǎo)1丟失了。下面所示是用手寫的加法實(shí)現(xiàn)。(在加法上面的數(shù)字是進(jìn)位。)
1111111 0xffffffffcafebabeL +0x0000000100000000L 0x00000000cafebabeL
訂正該程序非常簡單,只需用一個long十六進(jìn)制字面常量來表示右操作數(shù)即可。這就可以避免了具有破壞力的符號擴(kuò)展,并且程序也就可以打印出我們所期望的結(jié)果1cafebabe:
public class JoyOfHex{ public static void main(String[] args){ System.outprintln( LongtoHexString(0x100000000L+0xcafebabeL)); } }
這個謎題給我們的教訓(xùn)是:混合類型的計算可能會產(chǎn)生混淆,尤其是十六進(jìn)制和八進(jìn)制字面常量無需顯式的減號符號就可以表示負(fù)的數(shù)值。為了避免這種窘境,通常最好是避免混合類型的計算。對于語言的設(shè)計者們來說,應(yīng)該考慮支持無符號的整數(shù)類型,從而根除符號擴(kuò)展的可能性??赡軙羞@樣的爭辯:負(fù)的十六進(jìn)制和八進(jìn)制字面常量應(yīng)該被禁用,但是這可能會挫傷程序員,他們經(jīng)常使用十六進(jìn)制字面常量來表示那些符號沒有任何重要含義的數(shù)值。
以上就是關(guān)于“Java語言表達(dá)式的五個謎題是什么”這篇文章的內(nèi)容,相信大家都有了一定的了解,希望小編分享的內(nèi)容對大家有幫助,若想了解更多相關(guān)的知識內(nèi)容,請關(guān)注億速云行業(yè)資訊頻道。
免責(zé)聲明:本站發(fā)布的內(nèi)容(圖片、視頻和文字)以原創(chuàng)、轉(zhuǎn)載和分享為主,文章觀點(diǎn)不代表本網(wǎng)站立場,如果涉及侵權(quán)請聯(lián)系站長郵箱:is@yisu.com進(jìn)行舉報,并提供相關(guān)證據(jù),一經(jīng)查實(shí),將立刻刪除涉嫌侵權(quán)內(nèi)容。