Javac和Java是JDK自帶的工具,其中Javac是編譯工具,Java工具啟動JVM虛擬機並執行java程序。這兩個工具都帶有設置字符編碼的選項。本文討論字符編碼選項的使用場景,和出現亂碼的原因。先把結論寫在這里,如不想閱讀后面的章節,可只看這里的結論。
注:文中的字符編碼和字符集是同一概念。我之前有篇博客專門闡述這個問題:https://www.cnblogs.com/jayson-jamaica/p/12652873.html
結論:Javac 的字符編碼選項
- 形式:javac -encoding CharSet XXXX.java //CharSet為XXXX.java文件的字符編碼。
- javac編譯器根據-encoding后跟隨的字符編碼,解析.java文件。-encoding不設置的時候,使用系統默認字符集解析.java文件。Windows的默認字符集是GBK。
- 無論之前的.java文件采用什么編碼,編譯后的.class文件都使用utf8編碼。
- 如果-encoding指定的字符編碼與.java文件的字符編碼不一致,不一定編譯失敗,會給后續亂碼埋下隱患。
結論:JVM的字符編碼選項
- 形式:java -Dfile.encoding= CharSet XXXX //XXXX為class文件,CharSet是本地設備支持的字符集。
- -Dfile.encoding並不是設置JVM虛擬機內存字符編碼的。JVM虛擬機內存的字符編碼是無法設置的,都是UTF-16.
- JVM加載.class文件中的字符,轉化為UTF-16存在內存中。在字符需要與本地設備交互時才根據encoding選項后的字符集編碼或解碼。
- -Dfile.encoding不設置的時候,使用系統默認字符集,Windows的默認字符集是GBK。
JVM的字符編碼選項不太好理解,舉個簡單的例子,以下面這段代碼來說明:
String str = "腦袋里有一盆醬"; OutputStream outputStream = new FileOutputStream("D:\\test\\t.txt"); outputStream.write(str.getBytes());
上面的代碼就是把幾個中文字符寫到t.txt文件中,程序執行完成后,t.txt文件的字符編碼是GBK還是UTF-8,這取決於-Dfile.encoding選項,如果選項指定為GBK,那么t.txt文件的字符編碼就是GBK;如果指定為UTF-8,那么t.txt文件的字符編碼就是UTF-8.
詳解:Javac與JVM的字符編碼選項
這一篇博客寫的非常好,https://blog.csdn.net/lgh1992314/article/details/77482046。很多細節我是看了這篇博客才理清楚的。后面很多地方也會引用這篇博客的圖以及觀點,先表示感謝。先上圖吧。
上圖是.java文件從編譯到執行的幾個過程,這幾個過程涉及到字符編碼,具體解釋摘抄如下:
①、A.java就是一個文本文件(以某種編碼格式來存儲:UTF-8、GBK、ISO-8859-1等),java編譯器要解析這個文本文件並編譯生成.class文件。而要想解析它,就必須知道它的編碼方式。(javac - encoding charset)如果encoding指定的編碼與文件的編碼不一致,要么編譯失敗,要么導致class文件中的字符亂碼。
②:以不同編碼方式編碼的A.java經過Java編譯器編譯生成了同一個相同的A.class。(字符串以UTF-8格式存儲) 字節碼解讀見:http://blog.csdn.net/x_iya/article/details/77073112
③:java虛擬機以二進制字節流的形式加載A.class,A.class中的字符編碼是utf8,加載到JVM虛擬機內存中后,字符編碼是utf16。
④:輸出結果,如代碼中指定了字符集,則按照代碼中指定的字符集輸出到設備中。如果代碼中未指定字符集,則按照JVM啟動時 -Dfile.encoding指定的字符集輸出到設備中。如設備不支持該字符集,則顯示亂碼。
Javac的字符編碼選項
上圖的過程1,就是java的編譯過程,JDK自帶的編譯工具javac支持設定encoding選項,形式如下:
javac -encoding CharSet xxxx.java //CharSet應為xxxx.java文件的字符集
javac根據encoding指定的字符集解析xxxx.java中的字符,如果encoding指定的字符集與.java文件的字符集一致,則能正常解析和編譯。如果encoding指定的字符集與.java文件的字符集不一致,解析的結果有兩種:一種是解析失敗,編譯失敗;另一種是解析為其他字符,編譯成功,但是class文件中的字符編碼有誤(也就是亂碼)。
javac編譯實驗1
String str = "腦袋里有一盆醬"; OutputStream outputStream = new FileOutputStream("D:\\test\\t.txt"); outputStream.write(str.getBytes());
根據上面的代碼,創建兩個Java類,分別為GbkCode和Utf8Code,GbkCode使用gbk字符集,Utf8Code使用utf-8字符集,把上面的代碼整理到兩個類中。分別使用javac,javac -encoding gbk, javac -encoding utf8 編譯兩個java文件。然后查看class文件的字符編碼。得到以下表格。
編譯命令 | 編譯結果 | class文件字符編碼 |
---|---|---|
javac GbkCode.java | 成功,生成GBKCode.class文件 | utf-8編碼 |
javac -encoding gbk GbkCode.java | 成功,生成GBKCode.class文件 | utf-8編碼 |
javac -encoding utf8 GbkCode.java | 失敗,utf8不可映射字符xxxx | \ |
javac Utf8Code.java | 失敗,gbk不可映射字符xxxx | \ |
javac -encoding gbk Utf8Code.java | 失敗,gbk不可映射字符xxxx | \ |
javac -encoding utf8 Utf8Code.java | 成功,生成Utf8Code.class文件 | utf-8編碼 |
注:判斷class文件字符編碼的方法有很多,我是使用Notepad++來確認class文件的編碼的。使用notepad++打開class文件,查看二進制,搜索“腦袋里有一盆醬”的utf8對應的二進制序列,如果搜到,就說明是utf-8編碼。Notepad++查看二進制的方法請看我另一篇博客:https://www.cnblogs.com/jayson-jamaica/p/12659229.html
實驗1測試了-encoding選項指定的字符集與.java文件字符集各種匹配情況。得出的結論是encoding選項指定的字符集與.java文件字符集一致,則編譯成功;不一致時,則編譯失敗。但是這個實驗不夠充分,因為如果不一致就編譯失敗,那編譯環節就永遠不會存在亂碼問題了。因此繼續實驗2.
javac編譯實驗2
我試着編造個字符串,看能不能騙過編譯器。我通過Notepad++發現了一對字符串,“人民” 的utf8編碼 與“浜烘皯” 的gbk編碼一致,於是我把上面的代碼做如下修改:
String str = "人民";// 用utf8編碼,保存在Utf8Code.java文件中,使用encoding gbk編譯。 String str = "浜烘皯";//用gbk編碼,保存在GbkCode.java文件中,使用encoding utf8編譯。
果然騙過了編譯器,class文件中的編碼,就出現了亂碼,結果如下:
編譯命令 | 編譯結果 | class文件字符編碼 |
---|---|---|
javac -encoding gbk Utf8Code.java | 成功,生成Utf8Code.class文件 | 本應是“人民”的utf8編碼,變成了“浜烘皯”的utf8編碼 |
javac -encoding utf8 GbkCode.java | 成功,生成GBKCode.class文件 | 本應是“浜烘皯”的utf8編碼,變成了“人民”的utf8編碼。 |
實驗二證明了,在有些時候,encoding選項指定的字符集與.java文件的字符集不一致,但是也能編譯成功,但是class文件中的編碼有問題。這會導致執行過程中出現亂碼。
JVM的字符編碼選項
使用java XXXX命令,啟動JVM虛擬機,並執行XXXX.class文件。前面已經說過,class文件中的字符編碼是utf8. JVM虛擬機中存儲字符使用的是utf16字符編碼,且不能設置。那java -Dfile.encoding=CharSet xxxx
`的作用是什么呢?
JVM虛擬機有時與本地設備進行IO操作,因此需要知道設備兼容的字符編碼。-Dfile.encoding就是告知虛擬機本地設備的字符編碼。有些設備支持多種字符編碼,比如說文件;有些設備可能僅支持一種字符編碼,比如說終端/Terminal/Console等。下面做兩個實驗分別驗證下。
Java encoding實驗1
String str = "腦袋里有一盆醬"; OutputStream outputStream = new FileOutputStream("D:\\test\\t.txt"); outputStream.write(str.getBytes());
依然是這段代碼,分別使用utf8/gbk編碼,使用正確的-encoding選項編譯;對class文件使用不同的-Dfile.encoding選項執行,得到如下結果:
編譯命令 | 執行命令 | t.txt文件字符編碼 |
---|---|---|
javac -encoding utf8 Utf8Code.java |
java -Dfile.encoding=UTF-8 Utf8Code | UTF-8 |
java -Dfile.encoding=GBK Utf8Code | GBK | |
javac -encoding gbk GBKCode.java |
java -Dfile.encoding=UTF-8 GBKCode | UTF-8 |
java -Dfile.encoding=GBK GBKCode | GBK |
注意:每次使用java命令執行程序前,需要把上次執行得到的t.txt文件刪除。不然得到的結果可能會異常。
通過這個實驗,可以得到-Dfile.encoding的用途:適配本地設備兼容的字符集(這個實驗是適配文件系統的字符集,估計網絡IO的字符集適配也是同樣的)。大家寫測試代碼的時候,似乎更喜歡使用console打印的形式,而非把結果打印到文件。下面的實驗將測試把字符打印到console。
Java encoding 實驗2
String str = "腦袋里有一盆醬";
System.out.println(str);
System.out.println(Arrays.toString(str.getBytes()));//此行代碼的目的是驗證str是確實受-Dfile.encoding影響。
這段代碼把結果打印到控制台。分別使用utf8/gbk編碼編寫.java文件,使用正確的-encoding選項編譯;對class文件使用不同的-Dfile.encoding選項執行。為了讓測試結果更有說服力,使用三種終端來執行。一種是windows自帶的cmd命令行,一種是Cygwin64 Terminal,一種是在Ubuntu的Terminal中。
這里先說明一下終端的默認字符集:我的電腦是win10系統,cmd命令行和Cygwin64 Terminal都默認使用GBK字符編碼;Ubuntu的Terminal默認的使用utf8字符編碼。
對於終端默認的字符集,可以簡單測試驗證下,分別使用命令查看utf8的文本文件和gbk的文本文件,若gbk的文本文件顯示正常,utf8的文本文件出現亂碼,則說明終端的默認字符集是gbk。cmd查看文件的命令是type 文件名
, Cygwin64 Terminal的命令與Ubuntu命令一樣,查看文件的命令是cat 文件名
。具體做法可以參照下面這篇博客:https://blog.csdn.net/lgh1992314/article/details/77482046
實驗的結果如下:
編譯命令 | 執行命令 | cmd控制台輸出 | Cygwin64 Terminal輸出 | Ubuntu Ternimal輸出 |
---|---|---|---|---|
javac -encoding utf8 Utf8Code.java |
java -Dfile.encoding=GBK Utf8Code | 正常 | 正常 | 亂碼 |
java -Dfile.encoding=UTF-8 Utf8Code | 正常 | 亂碼 | 正常 | |
javac -encoding gbk GBKCode.java |
java -Dfile.encoding=GBK GBKCode | 正常 | 正常 | 亂碼 |
java -Dfile.encoding=UTF-8 Utf8Code | 正常 | 亂碼 | 正常 |
這個實驗結果比較有意思,win10的cmd控制台的結果都正常,結果似乎不能推斷出什么,這里后面再討論。 Cygwin64 Terminal和Ubuntu Terminal的結果在意料之中, Cygwin64 Terminal默認的編碼格式是GBK,因此Dfile.encoding=UTF-8執行文件會亂碼,在 Dfile.encoding=GBK是顯示正常。Ubuntu Terminal的默認編碼格式是utf8,因此Dfile.encoding=UTF-8顯示正常,在 Dfile.encoding=GBK顯示亂碼。
UbuntuTerminal 設置字符集非常方便,我們做更多測試試一下,測試結果如下:
執行命令 | Terminal字符集utf8 | Terminal字符集gbk | Terminal字符集gb18030 |
---|---|---|---|
-Dfile.encoding=UTF-8 | 正常 | 亂碼 | 亂碼 |
-Dfile.encoding=GBK | 亂碼 | 正常 | 正常 |
-Dfile.encoding=gb18030 | 亂碼 | 正常 | 正常 |
由於gbk字符集和gb18030兼容,因此Terminal字符集為gb18030時,-Dfile.encoding=GBK執行文件沒有亂碼。因此實驗2,可以得出結論:-Dfile.encoding設置字符編碼要與終端/console/Terminal的字符編碼兼容。
小結:
關於Javac的encoding問題和java -Dfile.encoding問題,已經清楚了。
- javac 的encoding選項跟隨的字符集,必須與.java文件使用的字符集一致,不然會在class文件中產生亂碼。
- java -Dfile.encoding跟隨的字符集,必須與設備支持的字符集兼容,這里的設備可能是文件系統,可能是終端,也有可能是網絡。網絡部分我還沒學到,暫時不討論。
寫在最后:
為了搞清楚javac和JVM字符編碼問題,我花了一個多星期時間,看了很多博客,做了很多實驗,甚至還專門安裝了虛擬機和Ubuntu。但現在仍然有兩個問題遺留。一個是Win10自帶的cmd終端,執行class文件時,無論 -Dfile.encoding跟隨的字符集是什么,都不會出現打印亂碼;第二個問題是Cygwin64 Terminal的字符集改為utf8時, -Dfile.encoding=UTF-8,依然打印亂碼。
對於這兩個遺留問題,我的猜測是Win10系統的終端與JVM虛機存在互優化,JVM執行class文件時,會讀到Win10的默認字符集,打印輸出的時候,無論-Dfile.encoding設置為什么字符集,都會轉化為GBK輸出。Cygwin64 Terminal修改字符集為utf8后,JVM執行class文件時,仍然讀到Win10的默認字符集是GBK,因此無論-Dfile.encoding設置為什么字符集,輸出都是GBK,而此時Cygwin64 Terminal只能顯示UTF8,因此產生了亂碼。
對於這兩個遺留問題,不好驗證,需要一台默認字符集是utf8的win電腦,或者把win10的系統字符集修改為utf8.暫時先遺留在這里吧。
下面的參考博客,寫的都不錯,可以作為參考。
https://www.qqxiuzi.cn/bianma/zifuji.php
https://www.qqxiuzi.cn/bianma/Unicode-UTF.php
https://blog.csdn.net/PacosonSWJTU/article/details/79118928
https://www.ibm.com/developerworks/cn/java/j-lo-chinesecoding/