一、Java編碼轉換過程
我們總是用一個java類文件和用戶進行最直接的交互(輸入、輸出),這些交互內容包含的文字可能會包含中文。無論這些java類是與數據庫交互,還是與前端頁面交互,他們的生命周期總是這樣的:
(1)、程序員在操作系統上通過編輯器編寫程序代碼并且以.java的格式保存操作系統中,這些文件我們稱之為源文件。
(2)、通過JDK中的javac.exe編譯這些源文件形成.class類。
(3)、直接運行這些類或者部署在WEB容器中運行,得到輸出結果。
這些過程是從宏觀上面來觀察的,了解這個肯定是不行的,我們需要真正來了解java是如何來編碼和被解碼的:
第一步:當我們用編輯器編寫java源文件,程序文件在保存時會采用操作系統默認的編碼格式(一般我們中文的操作系統采用的是GBK編碼格式)形成一個.java文件。java源文件是采用操作系統默認支持的file.encoding編碼格式保存的。下面代碼可以查看系統的file.encoding參數值。
1
|
System.out.println(System.getProperty( "file.encoding" )); |
第二步:當我們使用javac.exe編譯我們的java文件時,JDK首先會確認它的編譯參數encoding來確定源代碼字符集,如果我們不指定該編譯參數,JDK首先會獲取操作系統默認的file.encoding參數,然后JDK就會把我們編寫的java源程序從file.encoding編碼格式轉化為JAVA內部默認的UNICODE格式放入內存中。
第三步:JDK將上面編譯好的且保存在內存中信息寫入class文件中,形成.class文件。此時.class文件是Unicode編碼的,也就是說我們常見的.class文件中的內容無論是中文字符還是英文字符,他們都已經轉換為Unicode編碼格式了。
在這一步中對對JSP源文件的處理方式有點兒不同:WEB容器調用JSP編譯器,JSP編譯器首先會查看JSP文件是否設置了文件編碼格式,如果沒有設置則JSP編譯器會調用調用JDK采用默認的編碼方式將JSP文件轉化為臨時的servlet類,然后再編譯為.class文件并保持到臨時文件夾中。
第四步:運行編譯的類:在這里會存在一下幾種情況
(1)、直接在console上運行。
(2)、JSP/Servlet類。
(3)、java類與數據庫之間。
這三種情況每種情況的方式都會不同,
1.Console上運行的類
這種情況下,JVM首先會把保存在操作系統中的class文件讀入到內存中,這個時候內存中class文件編碼格式為Unicode,然后JVM運行它。如果需要用戶輸入信息,則會采用file.encoding編碼格式對用戶輸入的信息進行編碼同時轉換為Unicode編碼格式保存到內存中。程序運行后,將產生的結果再轉化為file.encoding格式返回給操作系統并輸出到界面去。整個流程如下:
在上面整個流程中,凡是涉及的編碼轉換都不能出現錯誤,否則將會產生亂碼。
2.Servlet類
由于JSP文件最終也會轉換為servlet文件(只不過存儲的位置不同而已),所以這里我們也將JSP文件納入其中。
當用戶請求Servlet時,WEB容器會調用它的JVM來運行Servlet。首先JVM會把servlet的class加載到內存中去,內存中的servlet代碼是Unicode編碼格式的。然后JVM在內存中運行該Servlet,在運行過程中如果需要接受從客戶端傳遞過來的數據(如表單和URL傳遞的數據),則WEB容器會接受傳入的數據,在接收過程中如果程序設定了傳入參數的的編碼則采用設定的編碼格式,如果沒有設置則采用默認的ISO-8859-1編碼格式,接收的數據后JVM會將這些數據進行編碼格式轉換為Unicode并且存入到內存中。運行Servlet后產生輸出結果,同時這些輸出結果的編碼格式仍然為Unicode。緊接著WEB容器會將產生的Unicode編碼格式的字符串直接發送置客戶端,如果程序指定了輸出時的編碼格式,則按照指定的編碼格式輸出到瀏覽器,否則采用默認的ISO-8859-1編碼格式。整個過程流程圖如下:
3.數據庫部分
我們知道java程序與數據庫的連接都是通過JDBC驅動程序來連接的,而JDBC驅動程序默認的是ISO-8859-1編碼格式的,也就是說我們通過java程序向數據庫傳遞數據時,JDBC首先會將Unicode編碼格式的數據轉換為ISO-8859-1的編碼格式,然后在存儲在數據庫中,即在數據庫保存數據時,默認格式為ISO-8859-1。
二、編碼&解碼
下面將結束java在那些場合需要進行編碼和解碼操作,并詳序中間的過程,進一步掌握java的編碼和解碼過程。在java中主要有四個場景需要進行編碼解碼操作:
(1):I/O操作
(2):內存
(3):數據庫
(4):javaWeb
下面主要介紹前面兩種場景,數據庫部分只要設置正確編碼格式就不會有什么問題,javaWeb場景過多需要了解URL、get、POST的編碼,servlet的解碼,所以javaWeb場景下節LZ介紹。
1.I/O操作
在前面LZ就提過亂碼問題無非就是轉碼過程中編碼格式的不統一產生的,比如編碼時采用UTF-8,解碼采用GBK,但最根本的原因是字符到字節或者字節到字符的轉換出問題了,而這中情況的轉換最主要的場景就是I/O操作的時候。當然I/O操作主要包括網絡I/O(也就是javaWeb)和磁盤I/O。網絡I/O下節介紹。
首先我們先看I/O的編碼操作。
InputStream為字節輸入流的所有類的超類,Reader為讀取字符流的抽象類。java讀取文件的方式分為按字節流讀取和按字符流讀取,其中InputStream、Reader是這兩種讀取方式的超類。
按字節
我們一般都是使用InputStream.read()方法在數據流中讀取字節(read()每次都只讀取一個字節,效率非常慢,我們一般都是使用read(byte[])),然后保存在一個byte[]數組中,最后轉換為String。在我們讀取文件時,讀取字節的編碼取決于文件所使用的編碼格式,而在轉換為String過程中也會涉及到編碼的問題,如果兩者之間的編碼格式不同可能會出現問題。例如存在一個問題test.txt編碼格式為UTF-8,那么通過字節流讀取文件時所獲得的數據流編碼格式就是UTF-8,而我們在轉化成String過程中如果不指定編碼格式,則默認使用系統編碼格式(GBK)來解碼操作,由于兩者編碼格式不一致,那么在構造String過程肯定會產生亂碼,如下:
1
2
3
4
5
6
7
8
|
File file = new File( "C:\\test.txt" ); InputStream input = new FileInputStream(file); StringBuffer buffer = new StringBuffer(); byte [] bytes = new byte [ 1024 ]; for ( int n ; (n = input.read(bytes))!=- 1 ; ){ buffer.append( new String(bytes, 0 ,n)); } System.out.println(buffer); |
輸出結果為亂碼....
test.txt中的內容為:我是 cm。
要想不出現亂碼,在構造String過程中指定編碼格式,使得編碼解碼時兩者編碼格式保持一致即可:
1
|
buffer.append( new String(bytes, 0 ,n, "UTF-8" )); |
按字符
其實字符流可以看做是一種包裝流,它的底層還是采用字節流來讀取字節,然后它使用指定的編碼方式將讀取字節解碼為字符。在java中Reader是讀取字符流的超類。所以從底層上來看按字節讀取文件和按字符讀取沒什么區別。在讀取的時候字符讀取每次是讀取留個字節,字節流每次讀取一個字節。
字節&字符轉換
字節轉換為字符一定少不了InputStreamReader。API解釋如下:InputStreamReader 是字節流通向字符流的橋梁:它使用指定的 charset 讀取字節并將其解碼為字符。它使用的字符集可以由名稱指定或顯式給定,或者可以接受平臺默認的字符集。 每次調用 InputStreamReader 中的一個 read() 方法都會導致從底層輸入流讀取一個或多個字節。要啟用從字節到字符的有效轉換,可以提前從底層流讀取更多的字節,使其超過滿足當前讀取操作所需的字節。API解釋非常清楚,InputStreamReader在底層讀取文件時仍然采用字節讀取,讀取字節后它需要根據一個指定的編碼格式來解析為字符,如果沒有指定編碼格式則采用系統默認編碼格式。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
|
String file = "C:\\test.txt" ; String charset = "UTF-8" ; // 寫字符換轉成字節流 FileOutputStream outputStream = new FileOutputStream(file); OutputStreamWriter writer = new OutputStreamWriter(outputStream, charset); try { writer.write( "我是 cm" ); } finally { writer.close(); } // 讀取字節轉換成字符 FileInputStream inputStream = new FileInputStream(file); InputStreamReader reader = new InputStreamReader( inputStream, charset); StringBuffer buffer = new StringBuffer(); char [] buf = new char [ 64 ]; int count = 0 ; try { while ((count = reader.read(buf)) != - 1 ) { buffer.append(buf, 0 , count); } } finally { reader.close(); } System.out.println(buffer); |
2.內存
首先我們看下面這段簡單的代碼
1
2
3
4
|
String s = "我是 cm" ; byte [] bytes = s.getBytes(); String s1 = new String(bytes, "GBK" ); String s2 = new String(bytes); |
在這段代碼中我們看到了三處編碼轉換過程(一次編碼,兩次解碼)。先看String.getTytes():
1
2
3
|
public byte [] getBytes() { return StringCoding.encode(value, 0 , value.length); } |
內部調用StringCoding.encode()方法操作:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
|
static byte [] encode( char [] ca, int off, int len) { String csn = Charset.defaultCharset().name(); try { // use charset name encode() variant which provides caching. return encode(csn, ca, off, len); } catch (UnsupportedEncodingException x) { warnUnsupportedCharset(csn); } try { return encode( "ISO-8859-1" , ca, off, len); } catch (UnsupportedEncodingException x) { // If this code is hit during VM initialization, MessageUtils is // the only way we will be able to get any kind of error message. MessageUtils.err( "ISO-8859-1 charset not available: " + x.toString()); // If we can not find ISO-8859-1 (a required encoding) then things // are seriously wrong with the installation. System.exit( 1 ); return null ; } } |
encode(char[] paramArrayOfChar, int paramInt1, int paramInt2)方法首先調用系統的默認編碼格式,如果沒有指定編碼格式則默認使用ISO-8859-1編碼格式進行編碼操作,進一步深入如下:
1
|
String csn = (charsetName == null ) ? "ISO-8859-1" : charsetName; |
同樣的方法可以看到new String 的構造函數內部是調用StringCoding.decode()方法:
1
2
3
4
5
6
|
public String( byte bytes[], int offset, int length, Charset charset) { if (charset == null ) throw new NullPointerException( "charset" ); checkBounds(bytes, offset, length); this .value = StringCoding.decode(charset, bytes, offset, length); } |
decode方法和encode對編碼格式的處理是一樣的。
對于以上兩種情況我們只需要設置統一的編碼格式一般都不會產生亂碼問題。
3.編碼&編碼格式
首先先看看java編碼類圖
首先根據指定的chart設置ChartSet類,然后根據ChartSet創建ChartSetEncoder對象,最后再調用 CharsetEncoder.encode 對字符串進行編碼,不同的編碼類型都會對應到一個類中,實際的編碼過程是在這些類中完成的。下面時序圖展示詳細的編碼過程:
通過這編碼的類圖和時序圖可以了解編碼的詳細過程。下面將通過一段簡單的代碼對ISO-8859-1、GBK、UTF-8編碼
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
|
public class Test02 { public static void main(String[] args) throws UnsupportedEncodingException { String string = "我是 cm" ; Test02.printChart(string.toCharArray()); Test02.printChart(string.getBytes( "ISO-8859-1" )); Test02.printChart(string.getBytes( "GBK" )); Test02.printChart(string.getBytes( "UTF-8" )); } /** * char轉換為16進制 */ public static void printChart( char [] chars){ for ( int i = 0 ; i < chars.length ; i++){ System.out.print(Integer.toHexString(chars[i]) + " " ); } System.out.println( "" ); } /** * byte轉換為16進制 */ public static void printChart( byte [] bytes){ for ( int i = 0 ; i < bytes.length ; i++){ String hex = Integer.toHexString(bytes[i] & 0xFF ); if (hex.length() == 1 ) { hex = '0' + hex; } System.out.print(hex.toUpperCase() + " " ); } System.out.println( "" ); } } |
輸出:
1
2
3
4
|
6211 662f 20 63 6d 3F 3F 20 63 6D CE D2 CA C7 20 63 6D E6 88 91 E6 98 AF 20 63 6D |
通過程序我們可以看到“我是 cm”的結果為:
1
2
3
4
|
char[]:6211 662f 20 63 6d ISO-8859-1:3F 3F 20 63 6D GBK:CE D2 CA C7 20 63 6D UTF-8:E6 88 91 E6 98 AF 20 63 6D |
圖如下: