本篇主要講述不同編碼之間的轉換問題,比較繁雜,如果平時處理文本不多,或者語言比較單一,沒有多語言文本處理的需求,則可以略過此篇。
本篇主要講述Python對文本字符串的處理。主要內容如下:
字符集基本概念以及Unicode;
Python中的字節序列;
Python對編碼錯誤的處理以及BOM;
Python對文本文件的編解碼,以及對Unicode字符的比較和排序,而這便是本篇的主要目的;
雙模式API和Unicode數據庫
如果對字符編碼很熟悉,也可直接跳過第2節。
筆者在初學字符集相關內容的時候,對這個概念并沒有什么疑惑:字符集嘛,就是把我們日常使用的字符(漢子,英文,符號,甚至表情等)轉換成為二進制嘛,和摩斯電碼本質上沒啥區別,用數學的觀點就是一個函數變換,這有什么好疑惑的?直到后來越來也多地接觸字符編碼,終于,筆者被這堆概念搞蒙了:一會兒Unicode編碼,一會兒又Unicode字符集,UTF-8編碼,UTF-16字符集還有什么字符編碼、字節序列。到底啥時候該叫“編碼”,啥時候該叫“字符集”?這些概念咋這么相似呢?既然這么相似,干嘛取這么多名字?后來仔細研究后發現,確實很多學術名次都是同義詞,比如“字符集”和“字符編碼”其實就是同義詞;有的譯者又在翻譯外國的書的時候,無意識地把一個概念給放大或者給縮小了。
說到這不得不吐槽一句,我們國家互聯網相關的圖書質量真的低。國人自己寫的IT方面的書,都不求有多經典,能稱為好書的都少之又少;而翻譯的書,要么翻譯得晦澀難懂,還不如直接看原文;要么故作風騷,非得體現譯者的文學修養有多“高”;要么生造名詞,同一概念同一單詞,這本書里你翻譯成這樣,另一本書里我就偏要翻譯成那樣(你們這是在翻譯小說嗎)。所以勸大家有能力的話還是直接看原文吧,如果要買譯本,還請大家認真比較比較,否則讀起來真的很痛苦。
回到主題,我們繼續討論字符集相關問題。翻閱網上大量資料,做出如下總結。
始終記住編碼的核心思想:就是給每個字符都對應一個二進制序列,其他的所有工作都是讓這個過程更規范,更易于管理。
現代編碼模型將這個過程分了5個層次,所用的術語列舉如下(為了避免混淆,這里不再列出它們的同義詞):
抽象字符表(Abstract character repertoire):
系統支持的所有抽象字符的集合。可以簡單理解為人們使用的文字、符號等。
這里需要注意一個問題:有些語系里面的字母上方或者下方是帶有特殊符號的,比如一點或者一撇;有的字符表里面會將字母和特殊符號組合成一個新的字符,為它單獨編碼;有的則不會單獨編碼,而是字母賦予一個編碼,特殊符號賦予一個編碼,然后當這倆在文中相遇的時候再將這倆的編碼組合起來形成一個字符。后面我們會談到這個問題,這也是以前字符編碼轉換常出毛病的一個原因。
提醒:雖然這里扯到了編碼,但抽象字符表這個概念還和編碼沒有聯系。
編碼字符集(Coded Character Set,CCS):字符 –> 碼位
首先給出總結:編碼字符集就是用數字代替抽象字符集中的每一個字符!
將抽象字符表中的每一個字符映射到一個坐標(整數值對:(x, y),比如我國的GBK編碼)或者表示為一個非負整數N,便生成了編碼字符集。與之相應的還有兩個抽象概念:編碼空間(encoding space)、碼位(code point)和碼位值(code point value)。
簡單的理解,編碼空間就相當于許多空位的集合,這些空位稱之為碼位,而這個碼位的坐標通常就是碼位值。我們將抽象字符集中的字符與碼位一一對應,然后用碼位值來代表字符。以二維空間為例,相當于我們有一個10萬行的表,每一行相當于一個碼位,二維的情況下,通常行號就是碼位值(當然你也可以設置為其他值),然后我們把每個漢字放到這個表中,最后用行號來表示每一個漢字。一個編碼字符集就是把抽象字符映射為碼位值。這里區分碼位和碼位值只是讓這個映射的過程更形象,兩者類似于座位和座位號的區別,但真到用時,并不區分這兩者,以下兩種說法是等效的:
“字符A的碼位是123456”==“字符A的碼位值是123456”
編碼空間并不只能是二維的,它也可以是三維的,甚至更高,比如當你以二維坐標(x, y)來編號字符,并且還對抽象字符集進行了分類,那么此時的編碼空間就可能是三維的,z坐標表示分類,最終使用(x, y, z)在這個編碼空間中來定位字符。不過筆者還沒真見過(或者見過但不知道……)三維甚至更高維的編碼,最多也就見過變相的三維編碼空間。但編碼都是人定的,你也可以自己定一個編碼規則~~
并不是每一個碼位都會被使用,比如我們的漢字有8萬多個,用10萬個數字來編號的話還會剩余1萬多個,這些剩余的碼位則留作擴展用。
注意:到這一步我們只是將抽象字符集進行了編號,但這個編號并不一定是二進制的,而且它一般也不是二進制的,而是10進制或16進制。該層依然是個抽象層。
而這里之所以說了這么多,就是為了和下面這個概念區分。
字符編碼表(Character Encoding Form,CEF):碼位 –> 碼元
將編碼字符集中的碼位轉換成有限比特長度的整型值的序列。這個整型值的單位叫碼元(code unit)。即一個碼位可由一個或多個碼元表示。而這個整型值通常就是碼位的二進制表示。
到這里才完成了字符到二進制的轉換。程序員的工作通常到這里就完成了。但其實還有后續兩步。
注意:直到這里都還沒有將這些序列存到存儲器中!所以這里依然是個抽象,只是相比上面兩步更具體而已。
字符編碼方案(Character Encoding Scheme,CES):碼元 –> 序列化
也稱為“serialization format”(常說的“序列化”)。將上面的整型值轉換成可存儲或可傳輸8位字節序列。簡單說就是將上面的碼元一個字節一個字節的存儲或傳輸。每個字節里的二進制數就是字節序列。這個過程中還會涉及大小端模式的問題(碼元的低位字節里的內容放在內存地址的高位還是低位的問題,感興趣的請自行查閱,這里不再贅述)。
直到這時,才真正完成了從我們使用的字符轉換到機器使用的二進制碼的過程。 抽象終于完成了實例化。
傳輸編碼語法(transfer encoding syntax):
這里則主要涉及傳輸的問題,如果用計算機網絡的概念來類比的話,就是如何實現透明傳輸。相當于將上面的字節序列的值映射到一個更受限的值域內,以滿足傳輸環境的限制。比如Email的Base64或quoted-printable協議,Base64是6bit作為一個單位,quoted-printable是7bit作為一個單位,所以我們得想辦法把8bit的字節序列映射到6bit或7bit的單位中。另一個情況則是壓縮字節序列的值,如LZW或進程長度編碼等無損壓縮技術。
綜上,整個編碼過程概括如下:
字符 –> 碼位 –> 碼元 –> 序列化,如果還要在特定環境傳輸,還需要再映射。從左到右是編碼的過程,從右到左就是解碼的過程。
下面我們以Unicode為例,來更具體的說明上述概念。
每個國家每個地區都有自己的字符編碼標準,如果你開發的程序是面向全球的,則不得不在這些標準之間轉換,而許多問題就出在這些轉換上。Unicode的初衷就是為了避免這種轉換,而對全球各種語言進行統一編碼。既然都在同一個標準下進行編碼,那就不存在轉換的問題了唄。但這只是理想,至今都沒編完,所以還是有轉換的問題,但已經極大的解決了以前的編碼轉換的問題了。
Unicode編碼就是上面的編碼字符集CCS。而與它相伴的則是經常用到的utf-8,utf-16等,這些則是上面的字符編碼表CEF。
最新版的Unicode庫已經收錄了超過10萬個字符,它的碼位一般用16進制表示,并且前面還要加上“U+”,十進制表示的話則是前面加“&#”,例如字母“A”的Unicode碼位是“U+0041”,十進制表示為“A”。
Unicode目前一共有17個Plane(面),從U+0000到U+10FFFF,每個Plane包含65536(=2^16^)個碼位,比如英文字符集就在0號平面中,它的范圍是U+0000 ~ U+FFFF。這17個Plane中4號到13號都還未使用,而15、16號Plane保留為私人使用區,而使用的5個Plane也并沒有全都用完,所以Unicode還沒有很大的未編碼空間,相當長的時間內夠用了。
注意:自2003年起,Unicode的編碼空間被規范為了21bit,但Unicode編碼并沒有占多少位之說,而真正涉及到在存儲器中占多少位時,便到了字符編碼階段,即utf-8,utf-16,utf-32等,這些字符編碼表在編程中也叫做編解碼器。
utf-n表示用n位作為碼元來編碼Unicode的碼位。以utf-8為例,它的碼元是1字節,且最多用4個碼元為Unicode的碼位進行編碼,編碼規則如下表所示:
表中的“×”用Unicode的16進制碼位的2進制序列從右向左依次替換,比如U+07FF的二進制序列為 :00000,11111,111111(這里的逗號位置只是為了和后面作比較,并不是正確的位置);
那么U+07FF經utf-8編碼后的比特序列則為 110 11111,10 111111,暫時將這個序列命名為a。
至此已經完成了前3步工作,現在開始執行序列化:
如果CPU是大端模式,那么序列a就是U+07FF在機器中的字節序列,但如果是小端模式,序列a的這兩個字節需要調換位置,變為10 111111,110 11111,這才是實際的字節序列。
Python3明確區分了人類可讀的字符串和原始的字節序列。Python3中,文本總是Unicode,由str類型表示,二進制數據由bytes類型表示,并且Python3不會以任何隱式的方式混用str和bytes。Python3中的str類型基本相當于Python2中的unicode類型。
Python3內置了兩種基本的二進制序列類型:不可變bytes類型和可變bytearray類型。這兩個對象的每個元素都是介于0-255之間的整數,而且它們的切片始終是同一類型的二進制序列(而不是單個元素)。
以下是關于字節序列的一些基本操作:
二進制序列實際是整數序列,但在輸出時為了方便閱讀,將其進行了轉換,以”b“開頭,其余部分:
可打印的ASCII范圍內的字節,使用ASCII字符本身;
制表符、換行符、回車符和 \ 對應的字節,使用轉義序列 \t,\n,\r 和 \;
其他字節的值,使用十六進制轉義序列,以 \x開頭。
bytes和bytesarray的構造方法如下:
一個str對象和一個encoding關鍵字參數;
一個可迭代對象,值的范圍是range(256);
一個實現了緩沖協議的對象(如bytes,bytearray,memoryview,array.array),此時它將源對象中的字節序列復制到新建的二進制序列中。并且,這是一種底層操作,可能涉及類型轉換。
除了格式化方法(format和format_map)和幾個處理Unicode數據的方法外,bytes和bytearray都支持str的其他方法,例如 bytes. endswith,bytes.replace等。同時,re模塊中的正則表達式函數也能處理二進制序列(當正則表達式編譯自二進制序列時會用到)。
二進制序列有個str沒有的方法fromhex,它解析十六進制數字對,構件二進制序列:
補充:struct模塊提供了一些函數,這些函數能把打包的字節序列轉換成不同類型字段組成的元組,或者相反,把元組轉換成打包的字節序列。struct模塊能處理bytes、bytearray和memoryview對象。這個不是本篇重點,不再贅述。
如第2節所述,我們常說的UTF-8,UTF-16實際上是字符編碼表,在編程中一般被稱為編解碼器。本節主要講述關于編解碼器的錯誤處理:UnicodeEncodeError,UnicodeDecodeError和SyntaxError。
Python中一般會明確的給出某種錯誤,而不會籠統地拋出UnicodeError,所以,在我們自行編寫處理異常的代碼時,也最好明確錯誤類型。
當從文本轉換成字節序列時,如果編解碼器沒有定義某個字符,則有可能拋出UnicodeEncodeError。
可以指定錯誤處理方式:
相應的,當從字節序列轉換成文本時,則有可能發生UnicodeDecodeError。
當加載Python模塊時,如果源碼的編碼與文件解碼器不符時,則會出現SyntaxError。比如Python3默認UTF-8編碼源碼,如果你的Python源碼編碼時使用的是其他編碼,而代碼中又沒有聲明編解碼器,那么Python解釋器可能就會發出SyntaxError。為了修正這個問題,可在文件開頭指明編碼類型,比如表明編碼為UTF-8,則應在源文件頂部寫下此行代碼:“#-*- coding: utf8 -*-”(沒有引號!)
補充:Python3允許在源碼中使用非ASCII標識符,也就是說,你可以用中文來命名變量(笑。。。)。如下:
但是極不推薦!還是老老實實用英文吧,哪怕拼音也行。
4.4 找出字節序列的編碼
有時候一個文件并沒有指明編碼,此時該如何確定它的編碼呢?實際并沒有100%確定編碼類型的方法,一般都是靠試探和分析找出編碼。比如,如果b’\x00’字節經常出現,就很有可能是16位或32位編碼,而不是8位編碼。Chardet就是這樣工作的。它是一個Python庫,能識別所支持的30種編碼。以下是它的用法,這是在終端命令行中,不是在Python命令行中:
當使用UTF-16編碼時,字節序列前方會有幾個額外的字節,如下:
BOM用于指明編碼時使用的是大端模式還是小端模式,上述例子是小端模式。UTF-16在要編碼的文本前面加上特殊的不可見字符ZERO WIDTH NO-BREAK SPACE(U+FEFF)。UTF-16有兩個變種:UTF-16LE,顯示指明使用小端模式;UTF-16BE,顯示指明大端模式。如果顯示指明了模式,則不會生成BOM:
根據標準,如果文件使用UTF-16編碼,且沒有BOM,則應假定它使用的是UTF-16大端模式編碼。然而Intel x86架構用的是小端模式,因此很多文件用的是不帶BOM的小端模式UTF-16編碼。這就容易造成混淆,如果把這些文件直接用在采用大端模式的機器上,則會出問題(比較老的AMD也有大端模式,現在的AMD也是x86架構了)。
由于大小端模式(字節順序)只對一個字(word)占多個字節的編碼有影響,所以對于UTF-8來說,不管設備使用哪種模式,生成的字節序列始終一致,因此不需要BOM。但在Windows下就比較扯淡了,有些應用依然會添加BOM,并且會根據有無BOM來判斷是不是UTF-8編碼。
補充:筆者查資料時發現有“顯示指明BOM”一說,剛看到的時候筆者以為是在函數中傳遞一個bom關鍵字參數來指明BOM,然而不是,而是傳入一個帶有BOM標識的編解碼器,如下:
處理文本的最佳實踐是”Unicode三明治”模型。圖示如下:
此模型的意思是:
對輸入的字節序列應盡早解碼為字符串;
第二層相當于程序的業務邏輯,這里應該保證只處理字符串,而不應該有編碼或解碼的操作存在;
對于輸出,應盡晚地把字符串編碼為字節序列。
當我們用Python處理文本時,我們實際對這個模型并沒有多少感覺,因為Python在讀寫文件時會為我們做必要的編解碼工作,我們實際處理的是這個三明治的中間層。
Python中調用open函數打開文件時,默認使用的是編解碼器與平臺有關,如果你的程序將來要跨平臺,推薦的做法是明確傳入encoding關鍵字參數。其實不管跨不跨平臺,這都是推薦的做法。
對于open函數,當以二進制模式打開文件時,它返回一個BufferedReader對象;當以文本模式打開文件時,它返回的是一個TextIOWrapper對象:
這里有幾個點:
除非想判斷編碼方式,或者文件本身就是二進制文件,否則不要以二進制模式打開文本文件;就算想判斷編碼方式,也應該使用Chardet,而不是重復造輪子。
如果打開文件時未傳入encoding參數,默認值將由locale.getpreferredencoding()提供,但從這么函數名可以看出,其實它返回的也不一定是系統的默認設置,而是用戶的偏好設置。用戶的偏好設置在不同系統中不一定相同,而且有的系統還沒法設置偏好,所以,正如官方文檔所說,該函數返回的是一個猜測的值;
如果設定了PYTHONENCODING環境變量,sys.stdout/stdin/stderr的編碼則使用該值,否則繼承自所在的控制臺;如果輸入輸出重定向到文件,編碼方式則由locale.getpreferredencoding()決定;
Python讀取文件時,對文件名(不是文件內容!)的編解碼器由sys.getfilesystemencoding()函數提供,當以字符串作為文件名傳入open函數時就會調用它。但如果傳入的文件名是字節序列,則會直接將此字節序列傳給系統相應的API。
總之:別依賴默認值!
如果遵循Unicode三明治模型,并且始終在程序中指定編碼,那將避免很多問題。但Unicode也有不盡人意的地方,比如文本規范化(為了比較文本)和排序。如果你只在ASCII環境中,或者語言環境比較固定單一,那么這兩個操作對你來說會很輕松,但如果你的程序面向多語言文本,那么這兩個操作會很繁瑣。
由于Unicode有組合字符,所以字符串比較起來比較復雜。
補充:組合字符指變音符號和附加到前一個字符上的記號,打印時作為一個整體。
在Unicode標準中,’é’和’e\u0301’叫做標準等價物,應用程序應該將它們視為相同的字符,但從上面代碼可以看出,Python并沒有將它們視為等價物,這就給Python中比較兩個字符串添加了麻煩。
解決的方法是使用unicodedata.normalize函數提供的Unicode規范化。它有四個標準:NFC,NFD,NFKC,NFKD。
NFC使用最少的碼位構成等價的字符串,NFD把組合字符分解成基字符和單獨的組合字符。這兩種規范化方法都能讓比較行為符合預期:
NFC是W3C推薦的規范化形式。西方鍵盤通常能輸出組合字符,因此用戶輸入的文本默認是NFC形式。我們對變音字符用的不多。但還是那句話,如果你的程序面向多語言文本,為了安全起見,最好還是用normalize(”NFC“, user_text)清洗字符串。
使用NFC時,有些單字符會被規范成另一個單字符,例如電阻的單位歐姆(Ω,U+2126,\u2126)會被規范成希臘字母大寫的歐米伽(U+03A9, \u03a9)。這倆看著一樣,現實中電阻歐姆的符號也就是從希臘字母來的,兩者應該相等,但在Unicode中是不等的,因此需要規范化,防止出現意外。
NFKC和NFKD(K表示“compatibility”,兼容性)是比較嚴格的規范化形式,對“兼容字符”有影響。為了兼容現有的標準,Unicode中有些字符會出現多次。比如希臘字母’μ’(U+03BC),Unicode除了有它,還加入了微符號’μ’(U+00B5),以便和latin1標準相互轉換,所以微符號是個“兼容字符”(上述的歐姆符號不是兼容字符!)。這兩個規范會將兼容字符分解為一個或多個字符,如下:
從上面的代碼可以看出,這兩個標準可能會造成格式損失,甚至曲解信息,但可以為搜索和索引提供便利的中間表述。比如用戶在搜索 ’1/2 inch‘ 時,可能還會搜到包含’? inch’的文章,這便增加了匹配選項。
對于搜索或索引,大小寫是個很有用的操作。同時,對于Unicode來說,大小寫折疊還是個復雜的問題。對于此問題,如果是初學者,首先想到的一定是str.lower()和str.upper()。但在處理多語言文本時,str.casefold()更常用,它將字符轉換成小寫。自Python3.4起,str.casefold()和str.lower()得到不同結果的有116個碼位。對于只包含latin1字符的字符串s,s.casefold()得到的結果和s.lower()一樣,但有兩個例外:微符號’μ’會變為希臘字母’μ’;德語Eszett(“sharp s”,?)為變成’ss’。
下面給出用以上內容編寫的幾個規范化匹配函數。對大多數應用來說,NFC是最好的規范形式。不區分大小寫的比較應該使用str.casefold()。對于處理多語言文本,以下兩個函數應該是必不可少的:
有時我們還想把變音符號去掉(例如“café”變“cafe”),比如谷歌在搜索時就有可能去掉變音符號;或者想讓URL更易讀時,也需要去掉變音符號。如果想去掉文本中的全部變音符號,則可用如下函數:
上述代碼去掉了所有的變音字符,包括非拉丁字符,但有時我們想只去掉拉丁字符中的變音字符,為此,我們還需要對基字符進行判斷,以下這個版本只去掉拉丁字符中的變音字符:
注意<1>處,如果一開始直接 latin_base=False,那么遇到刁鉆的人,該程序的結果將是錯誤的:大家可以試一試,把<1>處改成 latin_base=False,然后運行該程序,看c上面的變音符號去掉了沒有。之所以第7行寫成上述形式,就是考慮到可能有的人閑著沒事,將變音符號放在字符串的開頭。
更徹底的規范化步驟是把西文中的常見符號替換成ASCII中的對等字符,如下:
Python中,非ASCII文本的標準排序方式是使用locale.strxfrm函數,該函數“把字符串轉換成適合所在地區進行比較的形式”,即和系統設置的地區相關。在使用locale.strxfrm之前,必須先為應用設置合適的區域,而這還得指望著操作系統支持用戶自定義區域設置。比如以下排序:
筆者是Windows系統,不支持區域設置,不知道Linux下支不支持,大家可以試試。
想要正確實現Unicode排序,可以使用PyPI中的PyUCA庫,這是Unicode排序算法的純Python實現。它沒有考慮區域設置,而是根據Unicode官方數據庫中的排序表排序,只支持Python3。以下是它的簡單用法:
如果想定制排序方式,可把自定義的排序表路徑傳給Collator()構造方法。
Unicode標準提供了一個完整的數據庫(許多格式化的文本文件),它記錄了字符是否可打印、是不是字母、是不是數字、或者是不是其它數值符號等,這些數據叫做字符的元數據。字符串中的isidentifier、isprintable、isdecimal和isnumeric等方法都用到了該數據庫。unicodedata模塊中有幾個函數可用于獲取字符的元數據,比如unicodedata.name()用于獲取字符的官方名稱(全大寫),unicodedata.numeric()得到數值字符(如①,“1”)的浮點數值。
目前為止,我們一般都將字符串作為參數傳遞給函數,但Python標準庫中有些函數既支持字符串也支持字節序列作為參數,比如re和os模塊中就有這樣的函數。
如果使用字節序列構建正則表達式,\d和\w等模式只能匹配ASCII字符;如果是字符串模式,就能匹配ASCII之外的Unicode數字和字母,如下:
Python的os模塊中的所有函數、文件名或操作路徑參數既能是字符串,也能是字節序列。如下:
在Unix衍生平臺中,這些函數編解碼時使用surrogateescape錯誤處理方式以避免遇到意外字節序列時卡住。surrogateescape把每個無法解碼的字節替換成Unicode中U+DC00到U+DCFF之間的碼位,這些碼位是保留位,未分配字符,共應用程序內部使用。Windows使用的錯誤處理方式是strict。
本節內容較多。本篇首先介紹了編碼的基本概念,并以Unicode為例說明了編碼的具體過程;然后介紹了Python中的字節序列;隨后開始接觸實際的編碼處理,如Python編解碼過程中會引發的錯誤,以及Python中Unicode字符的比較和排序。最后,本篇簡要介紹了Unicode數據庫和雙模式API。
Java是一種強類型語言。這就意味著必須為每一個變量聲明一種類型。在Java中,一共有8種基本類型(primitive type),其中有4種整型,2種浮點型、1種用于表示Unicode編碼的字符單元的字符類型char(請參見論述char類型的章節)和1章用于表示真值的boolean類型。
注釋:Java有一個能夠表示任意精度的算術包,通常稱為"大數值"(big number)。雖然被稱為大數值,但它并不是一種新的Java類型,而是一個Java對象。本章稍后將會詳細地介紹它的用法。
整型用于表示沒有小數部分的數值,它允許是負數。Java提供了4種整型,具體內容如表3-1所示。
思考:byte為什么是-128-127?
至于為什么8位是-128~127,是由于最高一位存儲符號位,所以剩下7位代表數值大小,能從0表示到127
所以能從-127表示到127,但由于+0和-0都代表0,重復了,所以,多出一個位子,放到負數,讓-0代表-128。
所以byte的范圍是-128~127。
參考鏈接:blog.csdn.net/dicong9715/…
通常情況下,int類型最常用。但是如果表示星球上居住的人數,就需要使用long類型了。byte和short類型主要用于特定的應用場合,例如,底層的文件處理或者需要控制占用存儲空間量的大數組。
在Java中,整型的范圍與運行Java代碼的機器無關。這就解決了軟件從一個平臺移植到另一個平臺,或者在同一個平臺中的不同操作系統之間進行移植給程序員帶來的諸多問題。于此相反,C和C++程序需要針對不同的處理器選擇最為有效的整型,這樣就有可能造成一個在32位處理器上運行很好的C程序在16位系統上運行卻發生了整型溢出。由于Java程序必須保證在所有機器上都能夠得到相同的運行結果,所以每一種數據類型的取值范圍必須固定。
長整型數值有一個后綴L(如4000000000L)。十六進制數值有一個前綴0x(如0xCAFE)。八進制有一個前綴0,例如010對應八進制中的8。很顯然,八進制表示法比較容易混淆,所以建議最好不要使用八進制常數。
C++注釋:在C和C++中,int表示的整型與目標機器相關。在8086這樣的16位處理器上整型數值占2字節;在Sun SPARC這樣的32位處理器上,整型數值占4字節;而在Intel Pentium處理器上,C和C++整型依賴于具體的操作系統,對于DOS和Windows 3.1,整型數值占2字節。當Windows程序使用32位模式時,整型數值占4字節。在Java中,所有的數值類型所占據的字節數量與平臺無關。
注意,Java沒有任何無符號類型(unsinged type)
浮點類型用于表示有小數部分的數值。在Java中有2種浮點類型,具體內容如表3-2所示。
double表示這種類型的數值精度是float類型的兩倍(有人稱之為雙精度)。絕大部分應用程序都采用double類型。在很多情況下,float類型的精度很難滿足需求。例如,用7位有效數字足以精確地表示普通雇員的年薪,但表示公司總裁的年薪可能就不夠用了。實際上,只有很少的情況適用適用float,例如,需要快速地處理單精度數據,或者需要存儲大量數據。
float類型的數值有一個后綴F(例如,3.402F)。沒有后綴F的浮點數值(3.402)默認為double。當然,也可以在浮點數值后面添加后綴D(例如,3.402D)。
注釋:在JDK 5.0中,可以使用十六進制表示浮點數值。例如,0.125可以表示成0x1.0p-3。
在十六進制表示法中,使用p表示指數,而不是e。注意,尾數采用十六進制,指數采用
十進制。指數的基數是2,而不是10。
所有的浮點數值計算都遵循IEEE 754規范。下面是用于表示溢出和出錯情況的三個特殊的浮點數值:
? 正無窮大
public static final float POSITIVE_INFINITY=1.0f / 0.0f;
? 負無窮大
public static final float NEGATIVE_INFINITY=-1.0f / 0.0f;
? NaN(不是一個數字)
public static final float NaN=0.0f / 0.0f;
例如,一個正整數除以0的結果為正無窮大。計算0/0或者負數的平方根結果為NaN。
注釋:常量Double.POSITIVE_INFINITY、Double.NEGATIVE_INFINITY和Double.NaN
(與相應的Float類型的常量一樣)分別表示這三個特殊的值,但在實際應用中很少遇到。
特別要說明的是,不能這樣檢測一個特定值是否等于Double.NaN:
if (x==Double.NaN)// is never true
所有“非數值”的值都認為是不相同的。然而,可以使用Double.isNaN方法:
if (Double.isNaN(x)) //check whether x is "not a number"
警告:浮點數值不適用于禁止出現舍入誤差的金融計算中。例如,命令System.out.println
(2.0-1.1)將打印出0.8999999999999999,而不是人們想像的0.9。其主要原因是浮點數值采用二進制系統表示,而在二進制系統中無法精確的表示分數1/10。這就好像十進制無法精確地表示1/3一樣。如果需要在數值計算中不含有任何舍入誤差,就應該使用BigDecimal類,本章稍后將介紹這個類。
char類型用于表示單個字符。通常用來表示字符常量。例如:'A'是編碼為65所對應的字符常量。與"A"不同,"A"是一個包含字符A的字符串。Unicode編碼單元可以表示為16進制值,其范圍從\u0000到\uffff。例如:\u2122表示注冊符號,\u03C0表示希臘字母π。
除了可以采用轉義序列符\u表示Unicode代碼單元的編碼之外,還有一些用于表示特殊字符的轉義序列符,請參看表3-3。所有這些轉義序列符都可以出現在字符常量或字符串的引號內。例如,'\u2122'或"Hello\n"。轉義序列符\u還可以出現在字符常量或字符串的引號之外(而其他轉義序列不可以)。例如:
public static void main(String\u005B\u005D args)
這種形式完全符合語法規則,\u005B和\u005D是[和]的編碼。
要想弄清char類型,就必須了解Unicode編碼表。Unicode打破了傳統字符編碼方法的限制。在Unicode出現之前,已經有許多種不同的標準:美國的ASCII、西歐語言中的ISO 8859-1、俄國的KOI-8、中國的GB118030和BIG-5等等。這樣就產生了下面兩個問題:一個是對于任意給定的代碼值,在不同編碼方案下有可能對應不同的字母;而是采用大字符集的語言其編碼長度有可能不同。例如,有些常用的字符采用單字節編碼,而另一些字符則需要兩個或更多個字節。
設計Unicode編碼的目的就是要解決這些問題。在20世紀80年代開始啟動設計工作時,人們認為兩個字節的代碼寬度足以能夠對世界上各種語言的所有字符進行編碼,并有足夠的空間留給未來的擴展。在19911年發布了Unicode 1.0,當時僅占用65536個代碼值中不到一半的部分。在設計Java時決定采用16位的Unicode字符集,這樣會比使用8位字符集的程序設計語言有很大的改進。
十分遺憾,經過一段時間,不可避免的事情發生了。Unicode字符超過了65536個,其主要原因是增加了大量的漢語、日語和韓國語言中的表意文字。現在,16位的char類型已經不能滿足描述所有Unicode字符的需要了。
下面利用一些專用術語解釋一下Java語言解決這個問題的基本方法。從JDK5.0開始。代碼點(code point)是指與一個編碼表中的某個字符對應的代碼值。在Unicode標準中,代碼點采用十六進制書寫,并加上前綴U+,例如U+0041就是字母A的代碼點。Unicode的代碼點可以分為17個代碼級別(code plane)。第一個代碼級別稱為基本的多語言級別(basic mulitlingual plane),代碼點從U+0000到U+FFFF,其中包括了經典的Unicode代碼;其余的16個附加級別,代碼點從U+10000到U+10FFFF,其中包括了一些輔助字符(supplementary character)。
UTF-16編碼采用不同長度的編碼表示所有的Unicode代碼點。在基本的多語言級別中,每個字符用16位表示,通常被稱為代碼單元(code unit);而輔助字符采用一對連續的代碼單元進行編碼。這樣構成的編碼值一定落入基本的多語言級別中空閑的2048字節內,通常被稱為替代區域(surrogate area)[U+D800--U+DBFF用于第一個代碼單元,U+DC00--U+DFFF用于第二個代碼單元]。這樣設計十分巧妙,我們可以從中迅速地知道一個代碼單元是一個字符的編碼,還是一個輔助字符的第一或第二部分。例如,對于整數集合的數學符號,它的代碼點是U+1D56B,并且是用兩個代碼單元U+D835和U+DD6B編碼的。
在Java中,char類型用UTF-16編碼描述一個代碼單元。
我們強烈建議不用再程序中使用char類型,除非確實需要對UTF-16代碼單元進行操作。最好將需要處理的字符串用抽象數據類型表示(有關這方面的內容將在稍后討論)。
boolean(布爾)類型有兩個值:false和true,用來判定邏輯條件。整型值和布爾值之間不能進行相互轉換。
C++注釋:在C++中,數值或指針可以代替boolean值。整數0相當于布爾值false,非0值相當于布爾值true。在Java中則不行。因此,Java應用程序員不會遇到下述麻煩:
if(x=0)//oops...meant x==0
在C++中這個測試可以編譯運行,其結果總是false。而在Java中,這個測試將不能通過編
譯,其原因是整數表達式x=0不能轉換為布爾值。