Java中文乱码详解
文章目錄
- 1 Java編碼轉換
- 1.1 String轉換圖
- 1.2 String和Unicode編碼
- 1.3 new String()的理解
- 1.4 實際例子
- 1.5 java編碼轉換過程
- 1.5.1 編輯源文件
- 1.5.2 編譯源文件
- 1.5.3 運行編譯類
- 1.5.3.1 Console上運行的類
- 1.5.3.2 JSPServlet類
- 1.5.3.4 數據庫部分
- 1.6 java是如何編碼解碼的
- 1.6.1 編碼&解碼
- 1.6.2 I/O操作
- 1.6.3 按字節
- 1.6.4 按字符
- 1.6.5 字節&字符轉換
- 1.6.6 內存
- 1.6.7 編碼&編碼格式
- 1.7 javaWeb中的編碼解碼
- 1.7.1 編碼&解碼
- 1.7.2 URL方式
- 1.7.3 表單GET
- 1.7.4 表單POST
- 1.8 解決URL中文亂碼問題
- 1.8.1 javascript
- 1.8.1.1 escape
- 1.8.1.2 encodeURI
- 1.8.1.3 encodeURIComponent()
- 1.8.1.4 一次轉碼
- 1.8.1.5 二次轉碼
- 2 計算機編碼歷史
- 2.1 認識字符集
- 2.1.1 問題起源
- 2.1.2 常見字符編碼
- 2.2 字符編碼詳解;基礎知識 + ASCII + GB*
- 2.2.1 基礎知識
- 2.2.1.1 編碼
- 2.2.1.2 字符
- 2.2.1.3 字符集
- 2.2.1.4 字符編碼
- 2.2.2 ASCII
- 2.2.2.1 標準ASCII碼
- 2.2.2.2 擴展ASCII碼
- 2.2.3 GB*編碼
- 2.2.3.1 GB2312
- 2.2.3.2 GBK
- 2.2.3.3 GB18030
- 2.2.4 Unicode編碼
- 2.2.4.1 Unicode
- 2.2.4.2 UCS
- 2.2.4.3 Little endian & Big endian
- 2.2.4.4 BOM
- 2.2.4.5 UTF-8
- 2.2.4.6 Unicode與UTF-8之間的轉換
1 Java編碼轉換
1.1 String轉換圖
圖中詳細描述了 字符串類String 與 文件File ,ByteBuffer,CharBuffer,byte[] 數組,char[]數組之間的互相轉換
1.2 String和Unicode編碼
String和Unicode編碼關系:String類始終是以Unicode編碼形式存儲
String.getBytes()的使用:如果不帶字符集參數,就會依賴于JVM的字符集編碼,LINUX上一般為UNICODE,WINDOWS下一般為GBK.(要想改變JVM缺省字符集編碼,啟動JVM時用選項-Dfile.encodeing=UTF-8。為了安全起見,建議始終帶參數調用,例如:String s ; s.getBytes("UTF-8")。
Charset類非常好用,Charset.forName("編碼格式"),同時返回一個實例對象如:charset,那么編碼解碼如下:
- charset.encode是編碼,即把String按你指定的字符集編碼格式進行編碼后輸出字節數組。
- charset.decode是解碼,即把一個字節數組按你指定的字符集編碼格式進行解碼后輸出成字符串。
1.3 new String()的理解
String newStr = new String(oldStr.getBytes(), "UTF-8");java中的String類是按照unicode進行編碼的,當使用String(byte[] bytes, String encoding)構造字符串時,encoding所指的是bytes中的數據是按照那種方式編碼的,而不是最后產生的String是什么編碼方式,換句話說,是讓系統把bytes中的數據由encoding編碼方式轉換成unicode編碼,因為String類始終是以Unicode編碼形式存儲。
如果不指明,bytes的編碼方式將由jdk根據操作系統決定。
1.4 實際例子
舉例如下:
String s = Charset.defaultCharset().displayName();String s1 = "我喜歡你,My Love";ByteBuffer bb1 = ByteBuffer.wrap(s1.getBytes("UTF-8"));for(byte bt:bb1.array()){System.out.printf("%x",bt);}//char[]用法char[] chArray={'I','L','o','v','e','你'};//CharBuffer用法CharBuffer cb = CharBuffer.wrap(chArray);//重新定位指針cb.flip();String s2= new String(chArray);//ByteBuffer用法ByteBuffer bb2 = Charset.forName("utf-8").encode(cb);// 利用Charset編碼為指定字符集ByteBuffer bb3 = Charset.forName("utf-8").encode(s1);byte [] b = bb3.array() ;// 利用Charset按指定字符集解碼為字符串ByteBuffer bb4= ByteBuffer.wrap(b);String s2 = Charset.forName("utf-8").decode(bb4).toString();使用String構造方法和String.getBytes()做好中文字符轉碼
@Testpublic void test() {String testStr = "中";try {// 得到指定編碼的字節數組 字符串--->字節數組 6 byte[] t_iso = testStr.getBytes("ISO8859-1");byte[] t_gbk = testStr.getBytes("GBK");byte[] t_utf8 = testStr.getBytes("UTF-8");System.out.println("使用ISO解碼..." + t_iso.length);System.out.println("使用GBK解碼..." + t_gbk.length);System.out.println("使用UTF8解碼..." + t_utf8.length);// 解碼后在組裝13 String ut_iso = new String(t_iso, "ISO8859-1");String ut_gbk = new String(t_gbk, "GBK");String ut_utf8 = new String(t_utf8, "UTF-8");System.out.println("使用ISO解碼后再用ISO組裝..." + ut_iso);System.out.println("使用GBK解碼后再用GBK組裝..." + ut_gbk);System.out.println("使用UTF8解碼后再用UTF8組裝..." + ut_utf8);// 有時候要求必須是iso字符編碼類型// 可以先用GBK/UTF8編碼后,用ISO8859-1組裝成字符串,解碼時逆向即可獲得正確中文字符21 String t_utf8Toiso = new String(t_utf8, "ISO8859-1");// 將iso編碼的字符串進行還原23 String ut_utf8Toiso = new String(t_utf8Toiso.getBytes("ISO8859-1"),"UTF-8");System.out.println("使用ISO組裝utf8編碼字符..." + t_utf8Toiso);System.out.println("使用ISO解碼utf8編碼字符..." + ut_utf8Toiso);} catch (UnsupportedEncodingException e) {e.printStackTrace();} }1.5 java編碼轉換過程
我們總是用一個java類文件和用戶進行最直接的交互(輸入、輸出),這些交互內容包含的文字可能會包含中文
這些過程是從宏觀上面來觀察的,了解這個肯定是不行的,我們需要真正來了解java是如何來編碼和被解碼的
1.5.1 編輯源文件
當我們用編輯器編寫java源文件,程序文件在保存時會采用操作系統默認的編碼格式(一般我們中文的操作系統采用的是GBK編碼格式)形成一個.java文件。java源文件是采用操作系統默認支持的file.encoding編碼格式保存的。下面代碼可以查看系統的file.encoding參數值。
System.out.println(System.getProperty("file.encoding"));
1.5.2 編譯源文件
當我們使用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.5.3 運行編譯類
運行編譯的類:在這里會存在一下幾種情況
直接在console上運行,JSP/Servlet類,java類與數據庫之間。
這三種情況每種情況的方式都會不同,
1.5.3.1 Console上運行的類
這種情況下,JVM首先會把保存在操作系統中的class文件讀入到內存中,這個時候內存中class文件編碼格式為Unicode,然后JVM運行它。如果需要用戶輸入信息,則會采用file.encoding編碼格式對用戶輸入的信息進行編碼同時轉換為Unicode編碼格式保存到內存中。程序運行后,將產生的結果再轉化為file.encoding格式返回給操作系統并輸出到界面去。整個流程如下:
在上面整個流程中,凡是涉及的編碼轉換都不能出現錯誤,否則將會產生亂碼。
1.5.3.2 JSPServlet類
<%@ page language="java" contentType="text/html; charset=UTF-8" pageEncoding="GBK" %>在上面代碼中有兩個地方存在編碼:pageEncoding、contentType的charset。其中pageEncoding是jsp文件本身的編碼,而contentType的charset是指服務器發送給客戶端時的內容編碼
JVM將JSP編譯為.jsp文件。在這個過程中pageEncoding就起到作用了,JVM首先會獲取pageEncoding的值,如果該值存在則采用它設定的編碼來編譯,否則則采用file.encoding編碼來編譯。
JVM將.java文件轉換為.class文件。在這個過程就與任何編碼的設置都沒有關系了,不管JSP采用了什么樣的編碼格式都將無效。經過這個階段后.jsp文件就轉換成了統一的Unicode格式的.class文件了。
后臺經過業務邏輯處理后將產生的結果輸出到客戶端。在這個過程中contentType的charset就發揮了功效。如果設置了charset則瀏覽器就會使用指定的編碼格式進行解碼,否則采用默認的ISO-8859-1編碼格式進行解碼處理。
傳輸時的編碼格式
當用戶請求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編碼格式。整個過程流程圖如下:
1.5.3.4 數據庫部分
我們知道java程序與數據庫的連接都是通過JDBC驅動程序來連接的,而JDBC驅動程序默認的是ISO-8859-1編碼格式的,也就是說我們通過java程序向數據庫傳遞數據時,JDBC首先會將Unicode編碼格式的數據轉換為ISO-8859-1的編碼格式,然后在存儲在數據庫中,即在數據庫保存數據時,默認格式為ISO-8859-1
1.6 java是如何編碼解碼的
1.6.1 編碼&解碼
在java中主要有四個場景需要進行編碼解碼操作:
I/O操作,內存,數據庫,javaWeb
下面主要介紹前面兩種場景,數據庫部分只要設置正確編碼格式就不會有什么問題,javaWeb場景過多需要了解URL、get、POST的編碼,servlet的解碼
1.6.2 I/O操作
所謂亂碼問題無非就是轉碼過程中編碼格式的不統一產生的,比如編碼時采用UTF-8,解碼采用GBK,但最根本的原因是字符到字節或者字節到字符的轉換出問題了,而這中情況的轉換最主要的場景就是I/O操作的時候。當然I/O操作主要包括網絡I/O(也就是javaWeb)和磁盤I/O。
首先我們先看I/O的編碼操作
InputStream為字節輸入流的所有類的超類,Reader為讀取字符流的抽象類。java讀取文件的方式分為按字節流讀取和按字符流讀取,其中InputStream、Reader是這兩種讀取方式的超類。
1.6.3 按字節
一般都是使用InputStream.read()方法在數據流中讀取字節(read()每次都只讀取一個字節,效率非常慢,我們一般都是使用read(byte[])),然后保存在一個byte[]數組中,最后轉換為String。在我們讀取文件時,讀取字節的編碼取決于文件所使用的編碼格式,而在轉換為String過程中也會涉及到編碼的問題,如果兩者之間的編碼格式不同可能會出現問題。例如存在一個問題test.txt編碼格式為UTF-8,那么通過字節流讀取文件時所獲得的數據流編碼格式就是UTF-8,而我們在轉化成String過程中如果不指定編碼格式,則默認使用系統編碼格式(GBK)來解碼操作,由于兩者編碼格式不一致,那么在構造String過程肯定會產生亂碼,如下
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);輸出結果:锘挎垜鏄?cm要想不出現亂碼,在構造String過程中指定編碼格式,使得編碼解碼時兩者編碼格式保持一致即可:
buffer.append(new String(bytes,0,n,"UTF-8"));
1.6.4 按字符
其實字符流可以看做是一種包裝流,它的底層還是采用字節流來讀取字節,然后它使用指定的編碼方式將讀取字節解碼為字符。在java中Reader是讀取字符流的超類。所以從底層上來看按字節讀取文件和按字符讀取沒什么區別。在讀取的時候字符讀取每次是讀取留個字節,字節流每次讀取一個字節。
1.6.5 字節&字符轉換
字節轉換為字符一定少不了InputStreamReader。API解釋如下:InputStreamReader 是字節流通向字符流的橋梁:它使用指定的 charset讀取字節并將其解碼為字符。它使用的字符集可以由名稱指定或顯式給定,或者可以接受平臺默認的字符集。 每次調用 InputStreamReader 中的一個read()方法都會導致從底層輸入流讀取一個或多個字節。要啟用從字節到字符的有效轉換,可以提前從底層流讀取更多的字節,使其超過滿足當前讀取操作所需的字節。API解釋非常清楚,InputStreamReader在底層讀取文件時仍然采用字節讀取,讀取字節后它需要根據一個指定的編碼格式來解析為字符,如果沒有指定編碼格式則采用系統默認編碼格式。
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);1.6.6 內存
首先我們看下面這段簡單的代碼
String s = "我是 cm"; byte[] bytes = s.getBytes(); String s1 = new String(bytes,"GBK"); String s2 = new String(bytes);在這段代碼中我們看到了三處編碼轉換過程(一次編碼,兩次解碼)。先看String.getTytes():
public byte[] getBytes() {return StringCoding.encode(value, 0, value.length);}內部調用StringCoding.encode()方法操作:
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編碼格式進行編碼操作,進一步深入如下:String csn = (charsetName == null) ? "ISO-8859-1" : charsetName;
同樣的方法可以看到new String 的構造函數內部是調用StringCoding.decode()方法:
decode方法和encode對編碼格式的處理是一樣的。
對于以上兩種情況我們只需要設置統一的編碼格式一般都不會產生亂碼問題。
1.6.7 編碼&編碼格式
首先先看看java編碼類圖
首先根據指定的chart設置ChartSet類,然后根據ChartSet創建ChartSetEncoder對象,最后再調用 CharsetEncoder.encode 對字符串進行編碼,不同的編碼類型都會對應到一個類中,實際的編碼過程是在這些類中完成的。下面時序圖展示詳細的編碼過程:
通過這編碼的類圖和時序圖可以了解編碼的詳細過程。下面將通過一段簡單的代碼對ISO-8859-1、GBK、UTF-8編碼
通過程序我們可以看到“我是 cm”的結果為:
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
圖如下:
1.7 javaWeb中的編碼解碼
1.7.1 編碼&解碼
用戶想服務器發送一個HTTP請求,需要編碼的地方有url、cookie、parameter,經過編碼后服務器接受HTTP請求,解析HTTP請求,然后對url、cookie、parameter進行解碼。在服務器進行業務邏輯處理過程中可能需要讀取數據庫、本地文件或者網絡中的其他文件等等,這些過程都需要進行編碼解碼。當處理完成后,服務器將數據進行編碼后發送給客戶端,瀏覽器經過解碼后顯示給用戶。在這個整個過程中涉及的編碼解碼的地方較多,其中最容易出現亂碼的位置就在于服務器與客戶端進行交互的過程。
上面整個過程可以概括成這樣,頁面編碼數據傳遞給服務器,服務器對獲得的數據進行解碼操作,經過一番業務邏輯處理后將最終結果編碼處理后傳遞給客戶端,客戶端解碼展示給用戶。所以下面我就請求對javaweb的編碼&解碼進行闡述。
客戶端想服務器發送請求無非就通過四中情況:
- URL方式直接訪問。
- 頁面鏈接。
- 表單get提交
- 表單post提交
1.7.2 URL方式
對于URL,如果該URL中全部都是英文的那倒是沒有什么問題,如果有中文就要涉及到編碼了。如何編碼?根據什么規則來編碼?又如何來解碼呢,首先看URL的組成部分:
在這URL中瀏覽器將會對path和parameter進行編碼操作。為了更好地解釋編碼過程,使用如下URL
http://127.0.0.1:8080/perbank/我是cm?name=我是cm
將以上地址輸入到瀏覽器URL輸入框中,通過查看http 報文頭信息我們可以看到瀏覽器是如何進行編碼的。下面是IE、Firefox、Chrome三個瀏覽器的編碼情況:
可以看到各大瀏覽器對我是的編碼情況如下:
| Firefox | E6 88 91 E6 98 AF | E6 88 91 E6 98 AF |
| Chrome | E6 88 91 E6 98 AF | E6 88 91 E6 98 AF |
| IE | E6 88 91 E6 98 AF | CE D2 CA C7 |
對于path部分Firefox、chrome、IE都是采用UTF-8編碼格式,對于Query String部分Firefox、chrome采用UTF-8,IE采用GBK。至于為什么會加上%,這是因為URL的編碼規范規定瀏覽器將ASCII字符非 ASCII 字符按照某種編碼格式編碼成16進制數字然后將每個 16 進制表示的字節前加上%。
當然對于不同的瀏覽器,相同瀏覽器不同版本,不同的操作系統等環境都會導致編碼結果不同,上表某一種情況,對于URL編碼規則下任何結論都是過早的。由于各大瀏覽器、各個操作系統對URL的URI、QueryString編碼都可能存在不同,這樣對服務器的解碼勢必會造成很大的困擾,下面我們將tomcat,看tomcat是如何對URL進行解碼操作的。
解析請求的 URL 是在 org.apache.coyote.HTTP11.InternalInputBuffer 的 parseRequestLine 方法中,這個方法把傳過來的 URL 的 byte[] 設置到 org.apache.coyote.Request 的相應的屬性中。這里的 URL 仍然是 byte 格式,轉成 char 是在 org.apache.catalina.connector.CoyoteAdapter 的 convertURI 方法中完成的:
從上面的代碼可知,對URI的解碼操作是首先獲取Connector的解碼集,該配置在server.xml中
<Connector URIEncoding="utf-8" />如果沒有定義則會采用默認編碼ISO-8859-1來解析。
對于Query String部分,我們知道無論我們是通過get方式還是POST方式提交,所有的參數都是保存在Parameters,然后我們通過request.getParameter,解碼工作就是在第一次調用getParameter方法時進行的。在getParameter方法內部它調用org.apache.catalina.connector.Request 的 parseParameters 方法,這個方法將會對傳遞的參數進行解碼。下面代碼只是parseParameters方法的一部分:
從上面代碼可以看出對query String的解碼格式要么采用設置的ChartSet要么采用默認的解碼格式ISO-8859-1。注意這個設置的ChartSet是在 http Header中定義的ContentType,同時如果我們需要改指定屬性生效,還需要進行如下配置:
<Connector URIEncoding="UTF-8" useBodyEncodingForURI="true"/>
上面部分詳細介紹了URL方式請求的編碼解碼過程。其實對于我們而言,我們更多的方式是通過表單的形式來提交。
1.7.3 表單GET
我們知道通過URL方式提交數據是很容易產生亂碼問題的,所以我們更加傾向于通過表單形式。當用戶點擊submit提交表單時,瀏覽器會更加設定的編碼來編碼數據傳遞給服務器。通過GET方式提交的數據都是拼接在URL后面來提交的,所以tomcat服務器在進行解碼過程中URIEncoding就起到作用了。tomcat服務器會根據設置的URIEncoding來進行解碼,如果沒有設置則會使用默認的ISO-8859-1來解碼。假如我們在頁面將編碼設置為UTF-8,而URIEncoding設置的不是或者沒有設置,那么服務器進行解碼時就會產生亂碼。這個時候我們一般可以通過new String(request.getParameter(“name”).getBytes(“iso-8859-1″),”utf-8″) 的形式來獲取正確數據。
1.7.4 表單POST
對于POST方式,它采用的編碼也是由頁面來決定的即contentType。當我通過點擊頁面的submit按鈕來提交表單時,瀏覽器首先會根據contentType的charset編碼格式來對POST表單的參數進行編碼然后提交給服務器,在服務器端同樣也是用contentType中設置的字符集來進行解碼(這里與get方式就不同了),這就是通過POST表單提交的參數一般而言都不會出現亂碼問題。當然這個字符集編碼我們是可以自己設定的:request.setCharacterEncoding(charset)
1.8 解決URL中文亂碼問題
1.8.1 javascript
使用javascript編碼不給瀏覽器插手的機會,編碼之后再向服務器發送請求,然后在服務器中解碼。在掌握該方法的時候,我們需要料及javascript編碼的三個方法:escape()、encodeURI()、encodeURIComponent()。
1.8.1.1 escape
采用SIO Latin字符集對指定的字符串進行編碼。所有非ASCII字符都會被編碼為%xx格式的字符串,其中xx表示該字符在字符集中所對應的16進制數字。例如,格式對應的編碼為%20。它對應的解碼方法為unescape()。
事實上escape()不能直接用于URL編碼,它的真正作用是返回一個字符的Unicode編碼值。比如上面我是cm的結果為%u6211%u662Fcm,其中我對應的編碼為6211,是的編碼為662F,cm編碼為cm。
注意,escape()不對+編碼。但是我們知道,網頁在提交表單的時候,如果有空格,則會被轉化為+字符。服務器處理數據的時候,會把+號處理成空格。所以,使用的時候要小心
1.8.1.2 encodeURI
對整個URL進行編碼,它采用的是UTF-8格式輸出編碼后的字符串。不過encodeURI除了ASCII編碼外對于一些特殊的字符也不會進行編碼如:! @ # $& * ( ) = : / ; ? + ‘
1.8.1.3 encodeURIComponent()
把URI字符串采用UTF-8編碼格式轉化成escape格式的字符串。相對于encodeURI,encodeURIComponent會更加強大,它會對那些在encodeURI()中不被編碼的符號(; / ? : @ & = + $ , #)統統會被編碼。但是encodeURIComponent只會對URL的組成部分進行個別編碼,而不用于對整個URL進行編碼。對應解碼函數方法decodeURIComponent。
當然我們一般都是使用encodeURI方來進行編碼操作。所謂的javascript兩次編碼后臺兩次解碼就是使用該方法。javascript解決該問題有一次轉碼、兩次轉碼兩種解決方法
1.8.1.4 一次轉碼
javascript轉碼:
var url = '<s:property value="webPath" />/ShowMoblieQRCode.servlet?name=我是cm'; window.location.href = encodeURI(url);轉碼后的URL:http://127.0.0.1:8080/perbank/ShowMoblieQRCode.servlet?name=%E6%88%91%E6%98%AFcm 后臺處理: String name = request.getParameter("name");System.out.println("前臺傳入參數:" + name);name = new String(name.getBytes("ISO-8859-1"),"UTF-8");System.out.println("經過解碼后參數:" + name);輸出結果: 前臺傳入參數:??????cm 經過解碼后參數:我是cm1.8.1.5 二次轉碼
var url = '<s:property value="webPath" />/ShowMoblieQRCode.servlet?name=我是cm'; window.location.href = encodeURI(encodeURI(url));轉碼后的url:http://127.0.0.1:8080/perbank/ShowMoblieQRCode.servlet?name=%25E6%2588%2591%25E6%2598%25AFcm 后臺處理:String name = request.getParameter("name");System.out.println("前臺傳入參數:" + name);name = URLDecoder.decode(name,"UTF-8");System.out.println("經過解碼后參數:" + name);輸出結果: 前臺傳入參數:E68891E698AFcm 經過解碼后參數:我是cm2 計算機編碼歷史
2.1 認識字符集
2.1.1 問題起源
對于計算機而言,它僅認識兩個0和1,不管是在內存中還是外部存儲設備上,我們所看到的文字、圖片、視頻等等數據在計算機中都是已二進制形式存在的。不同字符對應二進制數的規則,就是字符的編碼。字符編碼的集合稱為字符集。
在早期的計算機系統中,使用的字符是非常少的,他們只包括26個英文字母、數字符號和一些常用符號,對于這些字符進行編碼,用1個字節就足夠了,但是隨著計算機的不斷發展,為了適應全世界其他各國民族的語言,這些少得可憐的字符編碼肯定是不夠的。于是人們提出了UNICODE編碼,它采用雙字節編碼,兼容英文字符和其他國家民族的雙字節字符編碼。
每個國家為了統一編碼都會規定該國家/地區計算機信息交換用的字符集編碼,為了解決本地字符信息的計算機處理,于是出現了各種本地化版本,引進LANG, Codepage 等概念。
現在大部分具有國際化特征的軟件核心字符處理都是以 Unicode 為基礎的,在軟件運行時根據當時的 Locale/Lang/Codepage 設置確定相應的本地字符編碼設置,并依此處理本地字符。在處理過程中需要實現 Unicode 和本地字符集的相互轉換。
java內部采用的就是Unicode編碼,所以在java運行的過程中就必然存在從Unicode編碼與相應的計算機操作系統或者瀏覽器支持的編碼格式相互轉化的過程,這個轉換的過程有一系列的步驟,如果某個步驟出現錯誤,則輸出的文字就會是亂碼。所以產生java亂碼的問題就在于JVM與對應的操作系統/瀏覽器進行編碼格式轉換時出現了錯誤。
其實解決 JAVA 程序中的漢字編碼問題的方法往往很簡單,但理解其背后的原因,定位問題,還需要了解現有的漢字編碼和編碼轉換。
2.1.2 常見字符編碼
計算機要準確的處理各種字符集文字,需要進行字符編碼,以便計算機能夠識別和存儲各種文字。常見的字符編碼主要包括:ASCII編碼、GBK編碼、Unicode
2.2 字符編碼詳解;基礎知識 + ASCII + GB*
2.2.1 基礎知識
在了解各種字符集之前我們需要了解一些最基礎的知識,如:編碼、字符、字符集、字符編碼基礎知識。
2.2.1.1 編碼
計算機中存儲的信息都是用二進制表示的,我們在屏幕上所看到文字、圖片等都是通過二進制轉換的結果。編碼是信息從一種形式或格式轉換為另一種形式的過程,通俗點講就是就是將我們看到的文字、圖片等信息按照某種規則存儲在計算機中,例如‘c’在計算機中怎么表達,‘陳’在計算機中怎么表達,這個過程就稱之為編碼。解碼是編碼的逆過程,它是將存儲在計算機的二進制轉換為我們可以看到的文字、圖片等信息,它體現的是視覺上的刺激。
n位二進制數可以組合成2的n次方個不同的信息,給每個信息規定一個具體碼組,這種過程也叫編碼。
在編碼和解碼中,他們就如加密、解密一般,他們一定會遵循某個規則,即y = f(x),那么x = f(y);否則在解密過程就會導致‘a’解析成‘b’或者亂碼。
2.2.1.2 字符
字符是可使用多種不同字符方案或代碼頁來表示的抽象實體,它是一個單位的字形、類字形單位或符號的基本信息,也是各種文字和符號的總稱,包括各國家文字、標點符號、圖形符號、數字等。
字符是指計算機中使用的字母、數字、字和符號,包括:1、2、3、A、B、C、~!·#¥%……—*()——+等等。在 ASCII 編碼中,一個英文字母字符存儲需要1個字節。在 GB 2312 編碼或 GBK 編碼中,一個漢字字符存儲需要2個字節。在UTF-8編碼中,一個英文字母字符存儲需要1個字節,一個漢字字符儲存需要3到4個字節。在UTF-16編碼中,一個英文字母字符或一個漢字字符存儲都需要2個字節(Unicode擴展區的一些漢字存儲需要4個字節)。在UTF-32編碼中,世界上任何字符的存儲都需要4個字節
2.2.1.3 字符集
字符是各種文字和符號的總稱,而字符集則是多個字符的集合,字符集種類較多,每個字符集包含的字符個數不同。而計算機要準確的處理各種字符集文字,需要進行字符編碼,以便計算機能夠識別和存儲各種文字。
常見字符集名稱:ASCII字符集、GB2312字符集、BIG5字符集、 GB18030字符集、Unicode字符集等。
2.2.1.4 字符編碼
計算機中的信息包括數據信息和控制信息,然而不管是那種信息,他們都是以二進制編碼的方式存入計算機中,但是他們是怎么展示在屏幕上的呢?同時在展現過程中如何才能保證他們不出錯?這個時候字符編碼就起到了重要作用!字符編碼是一套規則,一套建立在符合集合與數字系統之間的對應關系之上的規則,它是信息處理的基本技術。
使用字符編碼這套規則能夠對自然語言的字符的一個集合(如字母表或音節表),與其他東西的一個集合(如號碼或電脈沖)進行配對
2.2.2 ASCII
2.2.2.1 標準ASCII碼
ASCII,American Standard Code for Information Interchange,是基于拉丁字母的一套電腦編碼系統,主要用于顯示現代英語和其他西歐語言。它是現今最通用的單字節編碼系統。
ASCII碼使用指定的7位或者8位二進制數字組合表示128或者256種可能的字符。標準的ASCII編碼使用的是7(2^7 = 128)位二進制數來表示所有的大小寫字母、數字和標點符號已經一些特殊的控制字符,最前面的一位統一規定為0。其中0~31及127(共33個)是控制字符或通信專用字符,32~126(共95個)是字符(32是空格),其中48~57為0到9十個阿拉伯數字,65~90為26個大寫英文字母,97~122號為26個小寫英文字母,其余為一些標點符號、運算符號等
前面提過標準的ASCII碼是使用七位來表示字符的,而最高位(b7)則是用作奇偶校驗的。所謂奇偶校驗,是指在代碼傳送過程中用來檢驗是否出現錯誤的一種方法,一般分奇校驗和偶校驗兩種。
奇校驗規定:正確的代碼一個字節中1的個數必須是奇數,若非奇數,則在最高位b7添1;
偶校驗規定:正確的代碼一個字節中1的個數必須是偶數,若非偶數,則在最高位b7添1。
2.2.2.2 擴展ASCII碼
標準的ASCII是用七位來表示的,那么它的缺陷就非常明顯了:只能顯示26個基本拉丁字母、阿拉伯數目字和英式標點符號,基本上只能應用于現代美國英語,對于其他國家,128個字符肯定不夠。于是,這些歐洲國家決定利用字節中閑置的最高位編入新的符號,這樣一來,可以表達的字符數最多就為256個,但是隨著產生的問題也就來了:不同的國家有不同的字母,可能同一個編碼在不同的國家所表示的字符不同。但是不管怎么樣,在這些編碼中0~127所表示的字符肯定是一樣的,不一樣的也只是128~255這一段。
8位ASCII在歐洲國家表現的不盡人意,那么在其他國家就更加不用說了,我們擁有五千年歷史文化的中華名族所包含的漢字多大10多萬,不知道是多少個256。所以一個字節8位表示的256個字符肯定是不夠的,那么兩個字節呢?可能夠了吧!我們常見的漢字就是用兩個字節表示的,如GB2312。
2.2.3 GB*編碼
為了顯示中文,我們必須設計一套編碼規則用于將漢字轉換為計算機可以接受的數字系統的數。顯示中文的常用字符編碼有:GB2312、GBK、GB18030
2.2.3.1 GB2312
GB2312,用于漢字處理、漢字通信等系統之間的信息交換,通行于中國大陸。它的編碼規則是:小于127的字符的意義與原來相同,但兩個大于127的字符連在一起時,就表示一個漢字,前面的一個字節(他稱之為高字節)從0xA1用到 0xF7,后面一個字節(低字節)從0xA1到0xFE,這樣我們就可以組合出大約7000多個簡體漢字了。在這些編碼里,還把數學符號、羅馬希臘的 字母、日文的假名們都編進去了,連在ASCII里本來就有的數字、標點、字母都統統重新編了兩個字節長的編碼,這就是常說的全角字符,而原來在127 號以下的那些就叫半角字符了
雖然GB2312收錄了這么多漢子,他所覆蓋的使用率可以達到99%,但是對于那些不常見的漢字,例如人名、地名、古漢語,它就不能處理了,于是就有下面的GBK、GB 18030的出現。
2.2.3.2 GBK
GBK,全稱《漢字內碼擴展規范》,由中華人民共和國全國信息技術標準化技術委員會1995年12月1日制訂,也是漢字編碼的標準之一。
GBK是GB2312的擴展,他向下與GB2312兼容,,向上支持ISO 10646.1國際標準,是前者向后者過渡過程中的一個承上啟下的標準。同時它是使用雙字節編碼方案,其編碼范圍從8140至FEFE(剔除xx7F),首字節在 81-FE 之間,尾字節在 40-FE 之間,共23940個碼位,共收錄了21003個漢字。
2.2.3.3 GB18030
GB18030,全稱:國家標準GB 18030-2005《信息技術 中文編碼字符集》,是我國計算機系統必須遵循的基礎性標準之一,GB18030有兩個版本:GB18030-2000和GB18030-2005。
GB18030-2000是GBK的取代版本,僅規定了常用非漢字符號和27533個漢字(包括部首、部件等)的編碼,它的主要特點是在GBK基礎上增加了CJK統一漢字擴充A的漢字。
而GB18030-2005是全文強制性標準,市場上銷售的產品必須符合,它是GB18030-2000的基礎上增加了42711個漢字和多種我國少數民族文字的編碼。
GB18030標準采用單字節、雙字節和四字節三種方式對字符編碼
與UTF-8相同,采用多字節編碼,每個字可以由1個、2個或4個字節組成。
編碼空間龐大,最多可定義161萬個字符。
支持中國國內少數民族的文字,不需要動用造字區。
漢字收錄范圍包含繁體漢字以及日韓漢字
2.2.4 Unicode編碼
正如前面前面所提到的一樣,世界存在這么多國家,也存在著多種編碼風格,像中文的GB232、GBK、GB18030,這樣亂搞一套,雖然在本地運行沒有問題,但是一旦出現在網絡上,由于互不兼容,訪問則會出現亂碼。為了解決這個問題,偉大的Unicode編碼騰空出世。
2.2.4.1 Unicode
Unicode又稱為統一碼、萬國碼、單一碼,它是為了解決傳統的字符編碼方案的局限而產生的,它為每種語言中的每個字符設定了統一并且唯一的二進制編碼,以滿足跨語言、跨平臺進行文本轉換、處理的要求。可以想象Unicode作為一個字符大容器,它將世界上所有的符號都包含其中,并且每一個符號都有自己獨一無二的編碼,這樣就從根本上解決了亂碼的問題。所以Unicode是一種所有符號的編碼。
Unicode伴隨著通用字符集的標準而發展,同時也以書本的形式對外發表,它是業界的標準,對世界上大部分的文字系統進行了整理、編碼,使得電腦可以用更為簡單的方式來呈現和處理文字。Unicode至今仍在不斷增修,迄今而至已收入超過十萬個字符,它備受業界認可,并廣泛地應用于電腦軟件的國際化與本地化過程。
我們知道Unicode是為了解決傳統的字符編碼方案的局限而產生的,對于傳統的編碼方式而言,他們都存在一個共同的問題:無法支持多語言環境,這對于互聯網這個開放的環境是不允許的。而目前幾乎所有的電腦系統都支持基本拉丁字母,并各自支持不同的其他編碼方式。Unicode為了和它們相互兼容,其首256字符保留給ISO 8859-1所定義的字符,使既有的西歐語系文字的轉換不需特別考量;并且把大量相同的字符重復編到不同的字符碼中去,使得舊有紛雜的編碼方式得以和Unicode編碼間互相直接轉換,而不會丟失任何信息
一個字符的Unicode編碼是確定的,但是在實際傳輸過程中,由于不同系統平臺的設計不一定一致,以及出于節省空間的目的,對Unicode編碼的實現方式有所不同。Unicode的實現方式稱為Unicode轉換格式(Unicode Transformation Format,簡稱為UTF)。
Unicode編碼是現在大部分計算機內部使用的編碼格式
Unicode是字符集,它主要有UTF-8、UTF-16、UTF-32三種實現方式。由于UTF-8是目前主流的實現方式,UTF-16、UTF-32相對而言使用較少,所以下面就主要介紹UTF-8
2.2.4.2 UCS
提到Unicode可能有必要了解下,UCS。UCS(Universal Character Set,通用字符集),是由ISO制定的ISO 10646(或稱ISO/IEC 10646)標準所定義的標準字符集。它包括了其他所有字符集,保證了與其他字符集的雙向兼容,即,如果你將任何文本字符串翻譯到UCS格式,然后再翻譯回原編碼,你不會丟失任何信息。
UCS不僅給每個字符分配一個代碼,而且賦予了一個正式的名字。表示一個UCS或Unicode值的十六進制數通常在前面加上U+,例如U+0041代表字符A。
2.2.4.3 Little endian & Big endian
由于各個系統平臺的設計不同,可能會導致某些平臺對字符的理解不同(比如字節順序的理解)。這時將會導致同意字節流可能會被解釋為不同的內容。如某個字符的十六進制為4E59,拆分為4E、59,在MAC上讀取時是歐諾個低位開始的,那么MAC在遇到該字節流時會被解析為594E,找到的字符為奎,但是在Windows平臺是從高字節開始讀取,為4E59,找到的字符為乙。也就是說在Windows平臺保存的“乙”跑到MAC平臺上就變成了“奎”。這樣勢必會引起混亂,于是在Unicode編碼中采用了大頭(Big endian)、小頭(Little endian)兩種方式來進行區分。即第一個字節在前,就是大頭方式,第二個字節在前就是小頭方式。那么這個時候就出現了一個問題:計算機怎么知道某個文件到底是采用哪種編碼方式的呢?
Unicode規范中定義,每一個文件的最前面分別加入一個表示編碼順序的字符,這個字符的名字叫做 零寬度非換行空格(ZERO WIDTH NO-BREAK SPACE),用FEFF表示。這正好是兩個字節,而且FF比FE大1。
如果一個文本文件的頭兩個字節是FE FF,就表示該文件采用大頭方式;如果頭兩個字節是FF FE,就表示該文件采用小頭方式。
2.2.4.4 BOM
既然底層存儲分為了大端和小端兩種模式,那么假如我們現在有一個文件,計算機又是怎么知道當前是采用的大端模式還是小端模式呢?
BOM 即 byte order mark(字節順序標記),出現在文本文件頭部。BOM 就是用來標記當前文件采用的是大端模式還是小端模式存儲。我想這個大家應該都見過,平常在使用記事本保存文檔的時候,需要選擇采用的是大端還是小端:
在 UCS 編碼中有一個叫做 Zero Width No-Break Space(零寬無間斷間隔)的字符,對應的編碼是 FEFF。FEFF 是不存在的字符,正常來說不應該出現在實際數據傳輸中。
但是為了區分大端模式和小端模式,UCS 規范建議在傳輸字節流前,先傳輸字符 Zero Width No-Break Space。而且根據這個字符的順序來區分大端模式和小端模式。
下表就是不同編碼的 BOM:
有了這個規范,解析文件的時候就可以知道當前編碼以及其存儲模式了。注意這里 UTF-8 編碼比較特殊,因為本身 UTF-8 編碼有特殊的順序格式規定,所以 UTF-8 本身并沒有什么大端模式和小端模式的區別.
根據 UTF-8 本身的特殊編碼格式,在沒有 BOM 的情況下也能被推斷出來,但是因為微軟是建議都加上 BOM,所以目前存在了帶 BOM 的 UTF-8 文件和不帶 BOM 的 UTF-8 文件,這兩種格式在某些場景可能會出現不兼容的問題,所以在平常使用中也可以稍微留意這個問題
2.2.4.5 UTF-8
UTF-8是一種針對Unicode的可變長度字符編碼,可以使用1~4個字節表示一個符號,根據不同的符號而變化字節長度。它可以用來表示Unicode標準中的任何字符,且其編碼中的第一個字節仍與ASCII兼容,這使得原來處理ASCII字符的系統無須或只須做少部份修改,即可繼續使用。因此,它逐漸成為電子郵件、網頁及其他存儲或傳送文字的應用中,優先采用的編碼。
UTF-8使用一到四個字節為每個字符編碼,編碼規則如下:
- 對于單字節的符號,字節的第一位設為0,后面7位為這個符號的unicode碼。因此對于英語字母,UTF-8編碼和ASCII碼是相同的。
- 對于n字節的符號(n>1),第一個字節的前n位都設為1,第n+1位設為0,后面字節的前兩位一律設為10。剩下的沒有提及的二進制位,全部為這個符號的unicode碼
| 0000 ~007F | 0XXX XXXX |
| 0080 ~07FF | 110X XXXX 10XX XXXX |
| 0800 ~FFFF | 1110XXXX 10XX XXXX 10XX XXXX |
| 1 0000 ~1F FFFF | 1111 0XXX 10XX XXXX 10XX XXXX 10XX XXXX |
| 20 0000 ~3FF FFFF | 1111 10XX 10XX XXXX 10XX XXXX 10XX XXXX 10XX XXXX |
| 400 0000 ~7FFF FFFF | 1111 110X 10XX XXXX 10XX XXXX 10XX XXXX 10XX XXXX 10XX XXXX |
根據上面的轉換表,理解UTF-8的轉換編碼規則就變得非常簡單了:第一個字節的第一位如果為0,則表示這個字節單獨就是一個字符;如果為1,連續多少個1就表示該字符占有多少個字節。
以漢字嚴為例,演示如何實現UTF-8編碼。
已知嚴的unicode是4E25(100 1110 0010 0101),根據上表,可以發現4E25處在第三行的范圍內(0000 0800-0000 FFFF),因此嚴的UTF-8編碼需要三個字節,即格式是1110xxxx 10xxxxxx 10xxxxxx。然后,從嚴的最后一個二進制位開始,依次從后向前填入格式中的x,多出的位補0。這樣就得到了,嚴的UTF-8編碼是11100100 10111000 10100101,轉換成十六進制就是E4B8A5
2.2.4.6 Unicode與UTF-8之間的轉換
通過上面的例子我們可以看到”嚴”的Unicode碼為4E25,UTF-8編碼為E4B8A5,他們兩者是不一樣的,需要通過程序的轉換來實現,在Window平臺最簡單的直觀的方法就是記事本
在最下面的編碼(E)處有四個選項:ANSI、Unicode、Unicode big endian、UTF-8。
- ANSI:記事本的默認的編碼方式,對于英文文件是ASCII編碼,對于簡體中文文件是GB2312編碼。注意:不同 ANSI 編碼之間互不兼容,當信息在國際間交流時,無法將屬于兩種語言的文字,存儲在同一段ANSI編碼的文本中
- Unicode:UCS-2編碼方式,即直接用兩個字節存入字符的Unicode碼。該方式是小頭little endian方式。
- Unicode big endian:UCS-2編碼方式,大頭方式。
- UTF-8:閱讀上面(UTF-8)。
實例:在記事本中輸入”嚴”字,依次選擇ANSI、Unicode、Unicode big endian、UTF-8四種編碼風格,然后另存為,使用EditPlus文本工具使用”16進制查看器”進行查看,得到如下結果:
- ANSI:兩個字節D1 CF正是”嚴”的GB2312編碼。
- Unicode:四個字節”FF FE 25 4E”,其中FF FE表示小頭存儲方式,真正的編碼為”25 4E”。
- Unicode big endian:四個字節”FE FF 4E 25″,”FE FF”表示大頭存儲方式,真正編碼為”4E 25″。
- UTF-8:編碼是六個字節EF BB BF E4 B8 A5,前三個字節EF BB BF表示這是UTF-8編碼,后三個E4B8A5就是嚴的具體編碼,它的存儲順序與編碼順序是一致的。
點擊了解源碼反碼補碼和漢字與十六進制互轉
參考文獻地址:
Unicode維基百科:http://zh.wikipedia.org/wiki/Unicode
Unicode百度百科:http://baike.baidu.com/view/40801.html
字符編碼筆記:ASCII,Unicode和UTF-8:http://www.ruanyifeng.com/blog/2007/10/ascii_unicode_and_utf-8.html
UTF-8百度百科:http://baike.baidu.com/view/25412.html
編碼:http://baike.baidu.com/subview/237708/11062012.html(百度百科)
字符:http://baike.baidu.com/view/263416.html(百度百科)
字符集:http://baike.baidu.com/view/51987.html(百度百科)
字符編碼:http://baike.baidu.com/view/1204863.html(百度百科)
字符集和字符編碼:http://www.cnblogs.com/skynet/archive/2011/05/03/2035105.html(吳秦)
ASCII:http://baike.baidu.com/view/15482.html
GB2312:http://baike.baidu.com/view/443268.html
GBK:http://baike.baidu.com/view/931619.html
GB18030:http://baike.baidu.com/view/889058.html
總結
以上是生活随笔為你收集整理的Java中文乱码详解的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: mongoose 通过账号密码连接 Er
- 下一篇: Javasript设计模式之链式调用