1. 程式人生 > >【筆面試】字元流和位元組流的區別以及如何解決亂碼問題

【筆面試】字元流和位元組流的區別以及如何解決亂碼問題

工作中經常遇到java編碼問題,由於缺乏研究,總是無法給出確切的答案,這個週末在網上查了一些資料,在此做些彙總。

問題一:在java中讀取檔案時應該採用什麼編碼?

Java讀取檔案的方式總體可以分為兩類:按位元組讀取和按字元讀取。按位元組讀取就是採用InputStream.read()方法來讀取位元組,然後儲存到一個byte[]陣列中,最後經常用new String(byte[]);把位元組陣列轉換成String。在最後一步隱藏了一個編碼的細節,new String(byte[]);會使用作業系統預設的字符集來解碼位元組陣列,中文作業系統就是GBK。而我們從輸入流裡讀取的位元組很可能就不是

GBK編碼的,因為從輸入流裡讀取的位元組編碼取決於被讀取的檔案自身的編碼。舉個例子:我們在D:盤新建一個名為demo.txt的檔案,寫入我們。,並儲存。此時demo.txt編碼是ANSI,中文作業系統下就是GBK。此時我們用輸入位元組流讀取該檔案所得到的位元組就是使用GBK方式編碼的位元組。那麼我們最終new String(byte[]);時採用平臺預設的GBK來編碼成String也是沒有問題的(位元組編碼和預設解碼一致)。試想一下,如果在儲存demo.txt檔案時,我們選擇UTF-8編碼,那麼該檔案的編碼就不在是ANSI了,而變成了UTF-8。仍然採用輸入位元組流來讀取,那麼此時讀取的位元組和上一次就不一樣了,這次的位元組是
UTF-8編碼的位元組。兩次的位元組顯然不一樣,一個很明顯的區別就是:GBK每個漢字兩個位元組,而UTF-8每個漢字三個位元組。如何我們最後還使用new String(byte[]);來構造String物件,則會出現亂碼,原因很簡單,因為構造時採用的預設解碼GBK,而我們的位元組是UTF-8位元組。正確的辦法就是使用new String(byte[],”UTF-8”);來構造String物件。此時我們的位元組編碼和構造使用的解碼是一致的,不會出現亂碼問題了。

說完位元組輸入流,再來說說位元組輸出流。

我們知道如果採用位元組輸出流把位元組輸出到某個檔案,我們是無法指定生成檔案的編碼的(

假設檔案以前不存在),那麼生成的檔案是什麼編碼的呢?經過測試發現,其實這取決於寫入的位元組編碼格式。比如以下程式碼:

OutputStream out = new FileOutputStream("d:\\demo.txt");

out.write("我們".getBytes());

getBytes()會採用作業系統預設的字符集來編碼位元組,這裡就是GBK,所以我們寫入demo.txt檔案的是GBK編碼的位元組。那麼這個檔案的編碼就是GBK。如果稍微修改一下程式:out.write("我們".getBytes(“UTF-8”));此時我們寫入的位元組就是UTF-8的,那麼demo.txt檔案編碼就是UTF-8。這裡還有一點,如果把我們換成123abc之類的ascii碼字元,那麼無論是採用getBytes()或者getBytes(“UTF-8”)那麼生成的檔案都將是GBK編碼的。

這裡可以總結一下,InputStream中的位元組編碼取決檔案本身的編碼,而OutputStream生成檔案的編碼取決於位元組的編碼。

下面說說採用字元輸入流來讀取檔案。

首先,我們需要理解一下字元流。其實字元流可以看做是一種包裝流,它的底層還是採用位元組流來讀取位元組,然後它使用指定的編碼方式將讀取位元組解碼為字元。說起字元流,不得不提的就是InputStreamReader。以下是java api對它的說明: InputStreamReader是位元組流通向字元流的橋樑:它使用指定的 charset 讀取位元組並將其解碼為字元。它使用的字符集可以由名稱指定或顯式給定,否則可能接受平臺預設的字符集。說到這裡其實很明白了,InputStreamReader在底層還是採用位元組流來讀取位元組,讀取位元組後它需要一個編碼格式來解碼讀取的位元組,如果我們在構造InputStreamReader沒有傳入編碼方式,那麼會採用作業系統預設的GBK來解碼讀取的位元組。還用上面demo.txt的例子,假設demo.txt編碼方式為GBK,我們使用如下程式碼來讀取檔案:

InputStreamReader  in = new InputStreamReader(new FileInputStream(“demo.txt”));

那麼我們讀取不會產生亂碼,因為檔案採用GBK編碼,所以讀出的位元組也是GBK編碼的,而InputStreamReader預設採用解碼也是GBK。如果把demo.txt編碼方式換成UTF-8,那麼我們採用這種方式讀取就會產生亂碼。這是因為位元組編碼(UTF-8)和我們的解碼編碼(GBK)造成的。解決辦法如下:

InputStreamReader  in = new InputStreamReader(new FileInputStream(“demo.txt”),”UTF-8”);

InputStreamReader指定解碼編碼,這樣二者統一就不會出現亂碼了。

下面說說字元輸出流。

字元輸出流的原理和字元輸入流的原理一樣,也可以看做是包裝流,其底層還是採用位元組輸出流來寫檔案。只是字元輸出流根據指定的編碼將字元轉換為位元組的。字元輸出流的主要類是:OutputStreamWriterJava api解釋如下:OutputStreamWriter 是字元流通向位元組流的橋樑:使用指定的 charset 將要向其寫入的字元編碼為位元組。它使用的字符集可以由名稱指定或顯式給定,否則可能接受平臺預設的字符集。說的很明白了,它需要一個編碼將寫入的字元轉換為位元組,如果沒有指定則採用GBK編碼,那麼輸出的位元組都將是GBK編碼,生成的檔案也是GBK編碼的。如果採用以下方式構造OutputStreamWriter

OutputStreamWriter out = new OutputStreamWriter(new FileOutputStream(“dd.txt”),”UTF-8”);

那麼寫入的字元將被編碼為UTF-8的位元組,生成的檔案也將是UTF-8格式的。

問題二:既然讀檔案要使用和檔案編碼一致的編碼,那麼javac編譯檔案也需要讀取檔案,它使用什麼編碼呢?

這個問題從來就沒想過,也從沒當做是什麼問題。正是因為問題一而引發的思考,其實這裡還是有東西可以挖掘的。下面分三種情況來探討,這三種情況也是我們常用的編譯java原始檔的方法。

       1.javac在控制檯編譯java類檔案。

通常我們手動建立一個java檔案Demo.java,並儲存。此時Demo.java檔案的編碼為ANSI,中文作業系統下就是GBK.然後使用javac命令來編譯該原始檔。”javac Demo.java”Javac也需要讀取java檔案,那麼javac是使用什麼編碼來解碼我們讀取的位元組呢?其實javac採用了作業系統預設的GBK編碼解碼我們讀取的位元組,這個編碼正好也是Demo.java檔案的編碼,二者一致,所以不會出現亂碼情況。讓我們來做點手腳,在儲存Demo.java檔案時,我們選擇UTF-8儲存。此時Demo.java檔案編碼就是UTF-8了。我們再使用”javac Demo.java”來編譯,如果Demo.java裡含有中文字元,此時控制檯會出現警告資訊,也出現了亂碼。究其原因,就是因為javac採用了GBK編碼解碼我們讀取的位元組。因為我們的位元組是UTF-8編碼的,所以會出現亂碼。如果不信的話你可以自己試試。那麼解決辦法呢?解決辦法就是使用javacencoding引數來制定我們的解碼編碼。如下:javac -encoding UTF-8 Demo.java這裡我們指定了使用UTF-8來解碼讀取的位元組,由於這個編碼和Demo.java檔案編碼一致,所以不會出現亂碼情況了。

       2.Eclipse中編譯java檔案。

我習慣把Eclipse的編碼設定成UTF-8。那麼每個專案中的java原始檔的編碼就是UTF-8。這樣編譯也從沒有問題,也沒有出現過亂碼。正是因為這樣才掩蓋了使用javac可能出現的亂碼。那麼Eclipse是如何正確編譯檔案編碼為UTF-8java原始檔的呢?唯一的解釋就是Eclipse自動識別了我們java原始檔的檔案編碼,然後採取了正確的encoding引數來編譯我們的java原始檔。功勞都歸功於IDE的強大了。

       3.使用Ant來編譯java檔案。

       Ant也是我常用的編譯java檔案的工具。首先,必須知道Ant在後臺其實也是採用javac來編譯java原始檔的,那麼可想而知,1會出現的問題在Ant中也會存在。如果我們使用Ant來編譯UTF-8編碼的java原始檔,並且不指定如何編碼,那麼也會出現亂碼的情況。所以Ant的編譯命令<javac>有一個屬性” encoding”允許我們指定編碼,如果我們要編譯原始檔編碼為UTF-8java檔案,那麼我們的命令應該如下:

       <javac destdir="${classes}" target="1.4" source="1.4" deprecation="off" debug="on" debuglevel="lines,vars,source" optimize="off" encoding="UTF-8">

指定了編碼也就相當於”javac –encoding”了,所以不會出現亂碼了。

問題三:tomcat中編譯jsp的情況。

這個話題也是由問題二引出的。既然javac編譯java原始檔需要採用正確的編碼,那麼tomcat編譯jsp時也要讀取檔案,此時tomcat採用什麼編碼來讀取檔案?會出現亂碼情況嗎?下面我們來分析。

我們通常會在jsp開頭寫上如下程式碼:

<%@ page language="java" contentType="text/html; charset=utf-8" pageEncoding="utf-8"%>

我常常不寫pageEncoding這個屬於,也不明白它的作用,但是不寫也沒出現過亂碼情況。其實這個屬性就是告訴tomcat採用什麼編碼來讀取jsp檔案的。它應該和jsp檔案本身的編碼一致。比如我們新建個jsp檔案,設定檔案編碼為GBK,那麼此時我們的pageEncoding應該設定為GBK,這樣我們寫入檔案的字元就是GBK編碼的,tomcat讀取檔案時採用也是GBK編碼,所以能保證正確的解碼讀取的位元組。不會出現亂碼。如果把pageEncoding設定為UTF-8,那麼讀取jsp檔案過程中轉碼就出現了亂碼。上面說我常常不寫pageEncoding這個屬性,但是也沒出現過亂碼,這是怎麼回事呢?那是因為如果沒有pageEncoding屬性,tomcat會採用contentTypecharset編碼來讀取jsp檔案,我的jsp檔案編碼通常設定為UTF-8,contentTypecharset也設定為UTF-8,這樣tomcat使用UTF-8編碼來解碼讀取的jsp檔案,二者編碼一致也不會出現亂碼。這只是contentTypecharset的一個作用,它還有兩個作用,後面再說。可能有人會問:如果我既不設定pageEncoding屬性,也不設定contentTypecharset屬性,那麼tomcat會採取什麼編碼來解碼讀取的jsp檔案呢?答案是iso-8859-1,這是tomcat讀取檔案採用的預設編碼,如果用這種編碼來讀取檔案顯然會出現亂碼。

問題四:輸出。

問題二和問題三分析的過程其實就是從原始檔àclass檔案過程中的轉碼情況。最終的class檔案都是以unicode編碼的,我們前面所做的工作就是把各種不同的編碼轉換為unicode編碼,比如從GBK轉換為unicode,UTF-8轉換為unicode。因為只有採用正確的編碼來轉碼才能保證不出現亂碼。Jvm在執行時其內部都是採用unicode編碼的,其實在輸出時,又會做一次編碼的轉換。讓我們分兩種情況來討論。

1.java中採用Sysout.out.println輸出。

比如:Sysout.out.println(“我們”)。經過正確的解碼後我們unicode儲存在記憶體中的,但是在向標準輸出(控制檯)輸出時,jvm又做了一次轉碼,它會採用作業系統預設編碼(中文作業系統是GBK),將記憶體中的unicode編碼轉換為GBK編碼,然後輸出到控制檯。因為我們作業系統是中文系統,所以往終端顯示裝置上列印字元時使用的也是GBK編碼。因為終端的編碼無法手動改變,所以這個過程對我們來說是透明的,只要編譯時能正確轉碼,最終的輸出都將是正確的,不會出現亂碼。在Eclipse中可以設定控制檯的字元編碼,具體位置在Run Configuration對話方塊的Common標籤裡,我們可以試著設定為UTF-8,此時的輸出就是亂碼了。因為輸出時是採用GBK編碼的,而顯示卻是使用UTF-8,編碼不同,所以出現亂碼。

2.jsp中使用out.println()輸出到客戶端瀏覽器。

Jsp編譯成class後,如果輸出到客戶端,也有個轉碼的過程。Java會採用作業系統預設的編碼來轉碼,那麼tomcat採用什麼編碼來轉碼呢?其實tomcat是根據<%@ page language="java" contentType="text/html; charset=utf-8" pageEncoding="utf-8"%>contentTypecharset引數來轉碼的,contentType用來設定tomcat往瀏覽器傳送HTML內容所使用的編碼。Tomcat根據這個編碼來轉碼記憶體中的unicode。經過轉碼後tomcat輸出到客戶端的字元編碼就是utf-8了。那麼瀏覽器怎麼知道採取什麼編碼格式來顯示接收到的內容呢?這就是contentTypecharset屬性的第三個作用了:這個編碼會在HTTP響應頭中指定以通知瀏覽器。瀏覽器使用http響應頭的contentTypecharset屬性來顯示接收到的內容。

總結一下contentType charset的三個作用:

1).在沒有pageEncoding屬性時,tomcat使用它來解碼讀取的jsp檔案。

2).tomcat向客戶端輸出時,使用它來編碼傳送的內容。

3).通知瀏覽器,應該以什麼編碼來顯示接收到的內容。

為了能更好的理解上面所說的解碼和轉碼過程,我們舉一個例子。

新建一個index.jsp檔案,該檔案編碼為GBK,jsp開頭我們寫上如下程式碼:

<%@ page language="java" contentType="text/html; charset=utf-8" pageEncoding="GBK"%>

這裡的charsetpageEncoding不同,但是也不會出現亂碼,我來解釋一下。首先tomcat讀取jsp內容,並根據pageEncoding指定的GBK