最近一直在被編碼問題困擾。覺得這是我“職業(yè)生涯”里過不去的坎兒真慢,算是我的夢(mèng)魘潮尝。一想到只要我搬一天的磚榕吼,它就可能折磨我一次,我決定好好看一下勉失。于是我拿起了《Java核心技術(shù)》這本書羹蚣,看是翻起了第2章 輸入與輸出。結(jié)合網(wǎng)上的一些教程乱凿,然后以我的理解顽素,解決了我自己在編程中遇到的一個(gè)亂碼問題。不像之前是邊百度徒蟆,邊嘗試(有種神農(nóng)嘗百草的意思)各種帖子上寫的方法胁出,碰運(yùn)氣解決,這一次我是有點(diǎn)自我意識(shí)在改bug的(驕傲臉)段审。所以我打算在博采眾長(zhǎng)之后全蝶,把我這幾天學(xué)到的東西整理一下,可能中間還是有很多問題寺枉,或者是我理解不對(duì)的抑淫,還需要大家?guī)兔χ赋鰜?lái),我再改正型凳。來(lái)吧丈冬,開始打臉(委屈臉)嘱函。
什么是輸入流甘畅?什么是輸出流?
這是首先需要解決的問題往弓。其實(shí)就是明白自己的定位疏唾。我覺得網(wǎng)上很多的教程其實(shí)是有問題的,因?yàn)樗麄円簧蟻?lái)就是把《Java核心技術(shù)》這本書上的概念再念一遍函似,但是看不懂得還是看不懂槐脏。因?yàn)樗麄兒雎粤恕跋鄬?duì)”和“絕對(duì)”的概念,所以我覺得他們是在耍流氓撇寞。
在Java中顿天,“流”根據(jù)其流動(dòng)的方向是可以分為“輸入流”和“輸出流”的堂氯。那這個(gè)“方向”怎么定義。這是重點(diǎn)牌废,但是很多人都不提:)
今天我就要大聲告訴你咽白,
輸入流,輸出流是以程序?yàn)閰⒖键c(diǎn)來(lái)說(shuō)的鸟缕。
輸入流晶框,輸出流是以程序?yàn)閰⒖键c(diǎn)來(lái)說(shuō)的。
輸入流懂从,輸出流是以程序?yàn)閰⒖键c(diǎn)來(lái)說(shuō)的授段。
輸入流:就是給程序提供數(shù)據(jù)的流,程序可以從輸入流里獲取自己想要的數(shù)據(jù)番甩。
輸出流:是程序要向其寫入數(shù)據(jù)的流侵贵,也就是數(shù)據(jù)的目的地。
我覺得知道這一點(diǎn)对室,其實(shí)就知道是使用InputStream模燥,還是OutputStream了。比如掩宜,需要從文件A讀入數(shù)據(jù)蔫骂,那就new一個(gè)InputStream對(duì)象,然后調(diào)用read()方法牺汤。反之辽旋,要向文件B寫入數(shù)據(jù),就new一個(gè)OutputStream對(duì)象檐迟,然后調(diào)用write()方法补胚。
我講完了。
emmm追迟,是不是覺得我就是一個(gè)“水王”溶其。但是我覺得這是我今天學(xué)到的最有用的知識(shí)了。如果還需要補(bǔ)充一點(diǎn)的話敦间,就是“流”與“流”之間如何傳遞數(shù)據(jù)瓶逃,或者更確切一點(diǎn)說(shuō)就是,之間的“物質(zhì)”是什么廓块?
答案是:字節(jié)流厢绝。(心里默念一遍:一個(gè)字節(jié)等于8bit)
但是,這個(gè)字節(jié)流到底是怎么得到的带猴?我的問題是:我們程序白紙黑字寫的“Hello昔汉,程序媛!”是怎么變成字節(jié)的呢?字節(jié)流又是怎么變成我們認(rèn)識(shí)的文字的呢拴清?
自問自答:編碼 和 解碼
嗯靶病,應(yīng)該知道我接下去要說(shuō)的是什么了吧会通,就是亂碼問題了。
Java字符的編碼與亂碼問題
我覺得知乎上的這篇文章寫的超級(jí)好娄周。值得我們每一個(gè)被“亂碼”問題折磨的人渴语。https://zhuanlan.zhihu.com/p/25435644
雖然他寫了,但我還是想再?gòu)?fù)刻一遍昆咽。(人類的本質(zhì)是復(fù)讀機(jī))
1驾凶、一幅圖和四個(gè)概念
[圖片上傳失敗...(image-60d448-1563776264791)]
字符有三種形態(tài):形狀(顯示在顯示設(shè)備上)、數(shù)字(運(yùn)行于JVM中掷酗,Java統(tǒng)一為unicode編碼)和字節(jié)數(shù)組(不同的字符集有不同的映射方案)调违。
字符集合(Character set) :是一組形狀的集合。例如所有漢字的集合泻轰,發(fā)明于公元前技肩,發(fā)明者是倉(cāng)頡。它體現(xiàn)了字符的“形狀”浮声,它與計(jì)算機(jī)虚婿、編碼等無(wú)關(guān)。
編碼字符集(Coded character set) :是一組字符對(duì)應(yīng)的編碼(即數(shù)字)泳挥,為字符集合中的每一個(gè)字符給予一個(gè)數(shù)字然痊。例如最早的編碼字符集ASCII,發(fā)明于1967年屉符。再例如Java使用的unicode剧浸,發(fā)明于1994年(持續(xù)更新中)。由于編碼字符集為每一個(gè)字符賦予一個(gè)數(shù)字矗钟,因此在java內(nèi)部唆香,字符可以認(rèn)為就是一個(gè)16位的數(shù)字,因此以下方式都可以給字符賦值:
char c =‘中’
char c = 0x4e2d
char c = 20013
字符編碼方案(Character-encoding schema) :將字符編碼(數(shù)字)映射到一個(gè)字節(jié)數(shù)組的方案吨艇,因?yàn)樵诖疟P里躬它,所有信息都是以字節(jié)的方式存儲(chǔ)的。因此Java的16位字符必須轉(zhuǎn)換為一個(gè)字節(jié)數(shù)組才能夠存儲(chǔ)东涡。例如UTF-8字符編碼方案冯吓,它可以將一個(gè)字符轉(zhuǎn)換為1、2软啼、3或者4個(gè)字節(jié)桑谍。
一般認(rèn)為延柠,編碼字符集和字符編碼方案合起來(lái)被稱之為 字符集(Charset) 祸挪,這是一個(gè)術(shù)語(yǔ),要和前面的字符集合(Character set)區(qū)分開贞间。
2贿条、類型之間的轉(zhuǎn)化
2.1 從數(shù)字到形狀
就是說(shuō)從JVM中的數(shù)字雹仿,變?yōu)槠聊簧巷@示的文字,這一轉(zhuǎn)化過程是在字體庫(kù)的幫助下完成的整以,所以無(wú)需我們操心胧辽,也不會(huì)出錯(cuò),只要你給的數(shù)字是對(duì)的公黑,你就能得到你想要的數(shù)據(jù)邑商,所以這一轉(zhuǎn)化知道就行。
2.2 從數(shù)字到字節(jié)組——編碼
這是我們今天的重點(diǎn)凡蚜。
如圖所示人断,從JVM中的數(shù)字轉(zhuǎn)化為字節(jié)數(shù)組,也就是我們心心念念的“物質(zhì)”朝蜘,這個(gè)過程就是“編碼”恶迈。經(jīng)過“編碼”,我們就能得到可以傳輸谱醇,或者便于存儲(chǔ)的字節(jié)流暇仲。JVM上的同一個(gè)數(shù)字,比如0x4e2d副渴,采用不同的字符集進(jìn)行編碼奈附,能得到不同的字節(jié)數(shù)組。就如圖中可以看出煮剧,采用UTF-8的編碼得到的結(jié)果是e4 b8 ad桅狠;采用GBK編碼得到的結(jié)果是d6 d0;采用UTF-16編碼得到的是fe ff 4e 2d轿秧。有興趣的同學(xué)中跌,其實(shí)還是可以想想,這些數(shù)字是怎么得到的菇篡。而我就是這樣一個(gè)好奇且好學(xué)的寶寶漩符,我想知道他有沒有騙我,所以我查了一下資料驱还。其中這篇文章嗜暴,我覺得還是挺良心的:http://www.reibang.com/p/35f5f7d07732
比如就UTF-8這種編碼方式來(lái)舉個(gè)吧:
UTF-8的編碼規(guī)則很簡(jiǎn)單,只有二條:
1议蟆、對(duì)于單字節(jié)的符號(hào)闷沥,字節(jié)的第一位設(shè)為0,后面7位為這個(gè)符號(hào)的unicode碼咐容。因此對(duì)于英語(yǔ)字母舆逃,UTF-8編碼和ASCII碼是相同的。
2、對(duì)于n字節(jié)的符號(hào)(n>1)路狮,第一個(gè)字節(jié)的前n位都設(shè)為1虫啥,第n+1位設(shè)為0,后面字節(jié)的前兩位一律設(shè)為10奄妨。剩下的沒有提及的二進(jìn)制位涂籽,全部為這個(gè)符號(hào)的unicode碼。
看文字很費(fèi)解砸抛,上圖:
【圖略】
就拿我們的“中”字而言评雌,它在JVM的數(shù)字是0x 4e 2d,屬于上面Unicode字符中的第三種情況,所以就可以把轉(zhuǎn)換的16個(gè)二進(jìn)制依次放入上述的x中直焙。我利用在線的二進(jìn)制轉(zhuǎn)化武器柳骄,可以得到e4 b8 ad的結(jié)果,這就可以看出這位作者是真的很良心箕般,糟老頭也不都是壞的耐薯。
至于其他的編碼方式,想驗(yàn)證的可以去看看規(guī)則然后動(dòng)手試一下丝里。
上面那么多看似很高端的東西曲初,其實(shí)看不懂也可以不用看懂,我提一下就是為了zhuangbility杯聚,因?yàn)槲覀兤綍r(shí)寫代碼完全是無(wú)感知的臼婆。了解了最多就是心里踏實(shí)一點(diǎn),不了解知道怎么用就好幌绍。但是颁褂,你要確保你真的會(huì)用,不然你的老板會(huì)不高興的傀广。
編碼的例子代碼如下:
第一種方法颁独,使用String的getBytes方法:
private static byte[] encoding1(String str, String charset) throws UnsupportedEncodingException {
return str.getBytes(charset);
}
第二種方法,使用Charset的encode方法:
private static byte[] encoding2(String str, String charset) {
Charset cset = Charset.forName(charset);
ByteBuffer byteBuffer = cset.encode(str);
byte[] bytes = new byte[byteBuffer.remaining()];
byteBuffer.get(bytes);
return bytes;
}
實(shí)現(xiàn)的方式千千萬(wàn)伪冰,但是我們一定要抓到重點(diǎn):編碼得到的什么結(jié)果誓酒。就是這玩意兒: byte[] 。對(duì)贮聂,就是我們需要的字節(jié)流靠柑,就是我們需要的“物質(zhì)”。
2.3 從字節(jié)數(shù)組到數(shù)字——解碼
在完成了一系列操作以后吓懈,你還是需要讓別人知道你在想什么歼冰,最好的方式就是文字,我們大家能看得到的東西耻警,而字節(jié)數(shù)組這東西隔嫡,太過于抽象甸怕,所以我們需要把它變?yōu)橐粋€(gè)數(shù)字,這個(gè)轉(zhuǎn)化過程就是解碼畔勤。解碼就是把從磁盤或者網(wǎng)絡(luò)上得到的信息,轉(zhuǎn)換為字符或字符串扒磁。
解碼與編碼最大的區(qū)別是庆揪,解碼難。難在哪里妨托。就是你不知道或者你沒有意識(shí)去了解缸榛,你拿到的字節(jié)之前是怎么編碼的。就好像你不知道你現(xiàn)在身邊的人之前遇到過誰(shuí)兰伤。所以解碼時(shí)一定要指定字符集内颗,否則將會(huì)使用默認(rèn)的字符集進(jìn)行解碼。如果使用了錯(cuò)誤的字符集敦腔,則會(huì)出現(xiàn)亂碼均澳。
解碼的例子代碼如下:
第一種方法,使用String的構(gòu)造函數(shù):
private static String decoding1(byte[] bytes,String charset) throws UnsupportedEncodingException {
String str = new String(bytes, charset);
return str;
}
第二種方法符衔,使用Charset的decode方法:
private static String decoding2(byte[] bytes, String charset) {
Charset cset = Charset.forName(charset);
ByteBuffer buffer = ByteBuffer.wrap(bytes);
CharBuffer charBuffer = cset.decode(buffer);
return charBuffer.toString();
}
3找前、 默認(rèn)的字符集
亂碼問題是因?yàn)槲覀冊(cè)诰幋a和解碼的過程中,采用了不一樣的字符集判族。有時(shí)候如果我們沒有指明編碼和解碼的方式就會(huì)采用默認(rèn)的字符集躺盛,如果我們不知道什么是默認(rèn)的字符集,就會(huì)有可能出現(xiàn)亂碼的問題形帮。Java的默認(rèn)字符集槽惫,可以在兩個(gè)地方設(shè)定,一是執(zhí)行java程序時(shí)使用-D file.encoding參數(shù)指定辩撑,例如 -D file.encoding=UTF-8 就指定默認(rèn)字符集是UTF-8界斜。二是在程序執(zhí)行時(shí)使用Properties進(jìn)行指定,如下:
private static void setEncoding(String charset) {
Properties properties = System.getProperties();
properties.put("file.encoding",charset);
System.out.println(properties.get("file.encoding"));
}
注意合冀,這兩種方法如果同時(shí)使用锄蹂,則程序開始時(shí)使用參數(shù)指定的字符集,在Properties方法后使用Properties指定的字符集水慨。
如果這兩種方法都沒有使用得糜,則使用操作系統(tǒng)默認(rèn)的字符集。例如中文版windows 7的默認(rèn)字符集是GBK晰洒。
默認(rèn)字符集的優(yōu)先級(jí)如下:
1.程序執(zhí)行時(shí)使用Properties指定的字符集朝抖;
2.java命令的-Dfile.encoding參數(shù)指定的字符集;
3.操作系統(tǒng)默認(rèn)的字符集谍珊;
4.JDK中默認(rèn)的字符集治宣,我跟蹤了JDK1.8的源代碼,發(fā)現(xiàn)其默認(rèn)字符集指定為ISO-8859-1
4、 亂碼
從上述章節(jié)可知侮邀,字符的形態(tài)有三種坏怪,分別是“形狀”、“數(shù)字”和“字節(jié)”绊茧。字符的三種形態(tài)之間的轉(zhuǎn)換也有三類:從數(shù)字到形狀铝宵,從數(shù)字到字節(jié)(編碼),從字節(jié)到數(shù)字(解碼)华畏。
從數(shù)字到形狀不會(huì)產(chǎn)生亂碼鹏秋,亂碼就產(chǎn)生在編碼和解碼的時(shí)候。仔細(xì)想來(lái)亡笑,編碼也是不會(huì)產(chǎn)生亂碼的侣夷,因?yàn)閺臄?shù)字到字節(jié)(指定某個(gè)字符集)一定能夠轉(zhuǎn)換成功,即使某字符集中不包含該數(shù)字仑乌,它也會(huì)用指定的字節(jié)來(lái)代替百拓,并在轉(zhuǎn)換時(shí)給出指示。
如此一來(lái)晰甚,亂碼只會(huì)產(chǎn)生在解碼時(shí):例如使用某字符集A編碼的字節(jié)耐版,使用字符集B來(lái)進(jìn)行解碼,而A和B并不兼容压汪。這樣一來(lái)粪牲,解碼產(chǎn)生的數(shù)字(字符編碼)就是錯(cuò)誤的止剖,那么它顯示出來(lái)也是錯(cuò)誤的腺阳。