什么是編碼?
對(duì)于普通人來說,編碼總是與一些秘密的東西相關(guān)聯(lián)(加密與解密);對(duì)于程序員們來說,編碼大多數(shù)是指一種用來在機(jī)器與人之間傳遞信息的方式.
但從廣義上來講,編碼是從一種信息格式轉(zhuǎn)換為另一種信息格式的過程,解碼則是編碼的逆向過程.接下來舉幾個(gè)使用到編碼的例子:
當(dāng)我們要把想表達(dá)的意思通過一種語(yǔ)言表達(dá)出來,其實(shí)就是在腦海中對(duì)信息進(jìn)行了一次編碼,而對(duì)方如果也懂得這門語(yǔ)言,那么就可以用這門語(yǔ)言的解碼方法(語(yǔ)法規(guī)則)來獲得信息(日常的說話交流其實(shí)就是在編碼與解碼).
程序員寫程序時(shí),其實(shí)就是在將自己的想法通過計(jì)算機(jī)語(yǔ)言進(jìn)行編碼,而編譯器則通過生成抽象語(yǔ)法樹,詞義分析等操作進(jìn)行解碼,最終交給計(jì)算機(jī)執(zhí)行程序(編譯器產(chǎn)生的解碼結(jié)果并不是最終結(jié)果,一般為匯編語(yǔ)言,但匯編語(yǔ)言只是CPU指令集的助記符,還需要再進(jìn)行解碼).
- 計(jì)算機(jī)只有兩種狀態(tài)(0和1),要想存儲(chǔ)和傳輸多媒體信息,就需要用到編碼和解碼.
- 對(duì)數(shù)據(jù)進(jìn)行壓縮,其本質(zhì)就是以減少自身占用的空間為前提進(jìn)行重新編碼.
了解了編碼的含義,我們接下來重點(diǎn)探究Java
中的字符編碼.
本文作者為: SylvanasSun.轉(zhuǎn)載請(qǐng)務(wù)必將下面這段話置于文章開頭處(保留超鏈接).
本文首發(fā)自SylvanasSun Blog,原文鏈接: https://sylvanassun.github.io/2017/08/20/2017-08-20-Encode/
常見的字符集
字符集就是字符與二進(jìn)制的映射表,每一個(gè)字符集都有自己的編碼規(guī)則,每個(gè)字符所占用的字節(jié)也不同(支持的字符越多每個(gè)字符占用的字節(jié)也就越多).
-
ASCII : 美國(guó)信息交換標(biāo)準(zhǔn)碼(American Standard Code for Information Interchange).學(xué)過計(jì)算機(jī)的都知道大名鼎鼎的
ASCII
碼,它是基于拉丁字母的字符集,總共記有128個(gè)字符,主要目的是顯示英語(yǔ).其中每個(gè)字符占用一個(gè)字節(jié)(只用到了低7位).
-
ISO-8859-1 : 它是由國(guó)際標(biāo)準(zhǔn)化組織(International Standardization Organization)在
ASCII
基礎(chǔ)上制定的8位字符集(仍然是單字節(jié)編碼).它在ASCII
空置的0xA0-0xFF
范圍內(nèi)加入了96個(gè)字母與符號(hào),支持了歐洲部分國(guó)家的語(yǔ)言.
-
GBK : 如果我們想要讓電腦上顯示漢字就必須要有支持漢字的字符集,GBK就是這樣一個(gè)支持漢字的字符集,全稱為<<漢字內(nèi)碼擴(kuò)展規(guī)范>>,它的編碼方式分為單字節(jié)與雙字節(jié):
00–7F
范圍內(nèi)是第一個(gè)字節(jié),與ASCII
保持一致,之后的雙字節(jié)中,前一字節(jié)是雙字節(jié)的第一位(范圍在81–FE
,不包含80
和FF
),第二字節(jié)的一部分在40–7E
,其他部分在80–FE
.(這里不再介紹GB2313
與GB18030
,它們都是互相兼容的.) -
UTF-16 :
UTF-16
是Unicode(統(tǒng)一碼,一種以支持世界上多國(guó)語(yǔ)言為目的的通用字符集)
的一種實(shí)現(xiàn)方式,它把Unicode
的抽象碼位映射為2~4
個(gè)字節(jié)來表示,UTF-16
是變長(zhǎng)編碼(UTF-32是真正的定長(zhǎng)編碼
),但在最開始以前UTF-16
是用來配合UCS-2(UTF-16的子集,它是定長(zhǎng)編碼,用2個(gè)字節(jié)表示所有Unicode字符)
使用的,主要原因還是因?yàn)楫?dāng)時(shí)Unicode
只有不到65536個(gè)字符,2個(gè)字節(jié)就足以應(yīng)對(duì)一切了.后來,Unicode
支持的字符不斷膨脹,2個(gè)字節(jié)已經(jīng)不夠用了,導(dǎo)致一些只支持UCS-2
當(dāng)做內(nèi)碼的產(chǎn)品很尷尬(Java
就是其中之一). -
UTF-8 :
UTF-8
也是基于Unicode
的變長(zhǎng)編碼表,它使用1~6
個(gè)字節(jié)來為每個(gè)字符進(jìn)行編碼(RFC 3629
對(duì)UTF-8
進(jìn)行了重新規(guī)范,只能使用原來Unicode
定義的區(qū)域,U+0000~U+10FFFF
,也就是說最多只有4個(gè)字節(jié)),UTF-8
完全兼容ASCII
,它的編碼規(guī)則如下:在
U+0000~U+007F
范圍內(nèi),只需要一個(gè)字節(jié)(也就是ASCII
字符集中的字符).在
U+0080~U+07FF
范圍內(nèi),需要兩個(gè)字節(jié)(希臘文膀钠、阿拉伯文特愿、希伯來文等).在
U+0800~U+FFFF
范圍內(nèi),需要三個(gè)字節(jié)(亞洲漢字等).其他的字符使用四個(gè)字節(jié).
Java中字符的編解碼
Java
提供了Charset
類來完成對(duì)字符的編碼與解碼,主要使用以下函數(shù):
-
public static Charset forName(String charsetName)
: 這是一個(gè)靜態(tài)工廠函數(shù),它根據(jù)傳入的字符集名稱來返回對(duì)應(yīng)字符集的Charset
類.
-
public final ByteBuffer encode(CharBuffer cb) / public final ByteBuffer encode(String str)
: 編碼函數(shù),它將傳入的字符串或者字符序列進(jìn)行編碼,返回的ByteBuffer
是一個(gè)字節(jié)緩沖區(qū).
-
public final CharBuffer decode(ByteBuffer bb)
: 解碼函數(shù),將傳入的字節(jié)序列解碼為字符序列.
示例代碼
private static final String text = "Hello,編碼!";
private static final Charset ASCII = Charset.forName("ASCII");
private static final Charset ISO_8859_1 = Charset.forName("ISO-8859-1");
private static final Charset GBK = Charset.forName("GBK");
private static final Charset UTF_16 = Charset.forName("UTF-16");
private static final Charset UTF_8 = Charset.forName("UTF-8");
private static void encodeAndPrint(Charset charset) {
System.out.println(charset.name() + ": ");
printHex(text.toCharArray(), charset);
System.out.println("----------------------------------");
}
private static void printHex(char[] chars, Charset charset) {
System.out.println("ForEach: ");
ByteBuffer byteBuffer;
byte[] bytes;
if (chars != null) {
for (char c : chars) {
System.out.print("char: " + Integer.toHexString(c) + " ");
// 打印出字符編碼后對(duì)應(yīng)的字節(jié)
byteBuffer = charset.encode(String.valueOf(c));
bytes = byteBuffer.array();
System.out.print("byte: ");
if (bytes != null) {
for (byte b : bytes)
System.out.print(Integer.toHexString(b & 0xFF) + " ");
}
System.out.println();
}
}
System.out.println();
}
有的讀者可能會(huì)對(duì)以上代碼中的b & 0xFF
產(chǎn)生疑惑,這是為了解決符號(hào)擴(kuò)展問題.在Java
中,如果一個(gè)窄類型強(qiáng)轉(zhuǎn)為一個(gè)寬類型時(shí),會(huì)對(duì)多出來的空位進(jìn)行符號(hào)擴(kuò)展(如果符號(hào)位為1,就補(bǔ)1,為0則補(bǔ)0).只有char
類型除外,char
是沒有符號(hào)位的,所以它永遠(yuǎn)都是補(bǔ)0.
代碼中調(diào)用了函數(shù)Integer.toHexString()
,變量b
在運(yùn)算之前就已經(jīng)被強(qiáng)轉(zhuǎn)為了int
類型,為了讓數(shù)值不受到破壞,我們讓b
對(duì)0xFF
進(jìn)行了與運(yùn)算,0xFF
是一個(gè)低八位都為1的值(其他位都為0),而byte
的有效范圍只在低八位,所以結(jié)果為前24位(除符號(hào)位)都變?yōu)榱?,低八位保留了原有的值.
如果不做這項(xiàng)操作,那么b
又恰好是個(gè)負(fù)數(shù)的話,那這個(gè)強(qiáng)轉(zhuǎn)后的int
的前24位都會(huì)變?yōu)?,這個(gè)結(jié)果顯然已經(jīng)破壞了原有的值.
IO中的字符編碼
Reader
與Writer
是Java
中負(fù)責(zé)字符輸入與輸出的抽象基類,它們的子類實(shí)現(xiàn)了在各種場(chǎng)景中的字符輸入輸出功能.
在使用Reader
與Writer
進(jìn)行IO
操作時(shí),需要指定字符集,如果不顯式指定的話會(huì)默認(rèn)使用當(dāng)前環(huán)境的字符集,但我還是推薦顯式指定一致的字符集,這樣才不會(huì)出現(xiàn)亂碼問題(Reader
與Writer
指定的字符集不一致或更改了環(huán)境導(dǎo)致字符集不一致等).
public static void writeChar(String content, String filename, String charset) {
OutputStreamWriter writer = null;
try {
FileOutputStream outputStream = new FileOutputStream(filename);
writer = new OutputStreamWriter(outputStream, charset);
writer.write(content);
} catch (IOException e) {
e.printStackTrace();
} finally {
try {
if (writer != null)
writer.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
public static String readChar(String filename, String charset) {
InputStreamReader reader = null;
StringBuilder sb = null;
try {
FileInputStream inputStream = new FileInputStream(filename);
reader = new InputStreamReader(inputStream, charset);
char[] buf = new char[64];
int count = 0;
sb = new StringBuilder();
while ((count = reader.read(buf)) != -1)
sb.append(buf, 0, count);
} catch (IOException e) {
e.printStackTrace();
} finally {
try {
if (reader != null)
reader.close();
} catch (IOException e) {
e.printStackTrace();
}
}
return sb.toString();
}
Web中的字符編碼
在Web
開發(fā)中,亂碼也是經(jīng)常存在的一個(gè)問題,主要體現(xiàn)在請(qǐng)求的參數(shù)和返回的響應(yīng)結(jié)果,最頭疼的是不同的瀏覽器的默認(rèn)編碼甚至還不一致.
Java
以Http
的請(qǐng)求與響應(yīng)抽象出了Request
和Response
兩個(gè)對(duì)象,只要保持請(qǐng)求與響應(yīng)的編碼一致就能避免亂碼問題.
Request
提供了setCharacterEncoding(String encode)
函數(shù)來改變請(qǐng)求體的編碼,一般通過寫一個(gè)過濾器來統(tǒng)一對(duì)所有請(qǐng)求設(shè)置編碼.
request.setCharacterEncoding("UTF-8");
Response
提供了setCharacterEncoding(String encode)
與setHeader(String name,String value)
兩個(gè)函數(shù),它們都可以設(shè)置響應(yīng)的編碼.
response.setCharacterEncoding("UTF-8");
// 設(shè)置響應(yīng)頭的編碼信息,同時(shí)也告知了瀏覽器該如何解碼
response.setHeader("Content-Type","text/html;charset=UTF-8");
還有一種更簡(jiǎn)便的方式,直接使用Spring
提供的CharacterEncodingFilter
,該過濾器就是用來統(tǒng)一編碼的.
<filter>
<filter-name>charsetFilter</filter-name>
<filter-class>org.springframework.web.filter.CharacterEncodingFilter</filter-class>
<init-param>
<param-name>encoding</param-name>
<param-value>UTF-8</param-value>
</init-param>
<init-param>
<param-name>forceEncoding</param-name>
<param-value>true</param-value>
</init-param>
</filter>
<filter-mapping>
<filter-name>charsetFilter</filter-name>
<url-pattern>*</url-pattern>
</filter-mapping>
CharacterEncodingFilter
的實(shí)現(xiàn)如下:
public class CharacterEncodingFilter extends OncePerRequestFilter {
private String encoding;
private boolean forceEncoding = false;
public CharacterEncodingFilter() {
}
public void setEncoding(String encoding) {
this.encoding = encoding;
}
public void setForceEncoding(boolean forceEncoding) {
this.forceEncoding = forceEncoding;
}
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
if(this.encoding != null && (this.forceEncoding || request.getCharacterEncoding() == null)) {
request.setCharacterEncoding(this.encoding);
if(this.forceEncoding) {
response.setCharacterEncoding(this.encoding);
}
}
filterChain.doFilter(request, response);
}
}
為什么Char在Java中占用兩個(gè)字節(jié)?
眾所周知,在Java
中一個(gè)char
類型占用兩個(gè)字節(jié),那么這是為什么呢?這是因?yàn)?code>Java使用了UTF-16
當(dāng)作內(nèi)碼.
內(nèi)碼(Internal Encoding
)就是程序內(nèi)部所使用的編碼,主要在于編程語(yǔ)言實(shí)現(xiàn)其char
和String
類型在內(nèi)存中使用的內(nèi)部編碼.與之相對(duì)的就是外碼(External Encoding
),它是程序與外部交互時(shí)使用的字符編碼.
值得一提的是,當(dāng)初UTF-16
是配合UCS-2
使用的,后來Unicode
支持的字符不斷增多,UTF-16
也不再只當(dāng)作一個(gè)定長(zhǎng)的2字節(jié)編碼使用了,也就是說,Java
中的一個(gè)char
其實(shí)并不一定能代表一個(gè)完整的UTF-16
字符.
String.getBytes()
可以將該String的內(nèi)碼轉(zhuǎn)換為指定的外碼并返回這個(gè)編完碼的字節(jié)數(shù)組(無參數(shù)版使用當(dāng)前平臺(tái)的默認(rèn)編碼).
public static void main(String[] args) throws UnsupportedEncodingException {
String text = "碼";
byte[] bytes = text.getBytes("UTF-8");
System.out.println(bytes.length); // 輸出3
}
Java
還規(guī)定char
與String
類型的序列化是使用UTF-8
當(dāng)作外碼的,Java
中的Class
文件中的字符串常量與符號(hào)名也都規(guī)定使用UTF-8
.這種設(shè)計(jì)是為了平衡運(yùn)行時(shí)的時(shí)間效率與外部存儲(chǔ)的空間效率所做的取舍.
在SUN JDK6
中,有一條命令-XX:+UseCompressedString
.該命令可以讓String
內(nèi)部存儲(chǔ)字符內(nèi)容可能用byte[]
也可能用char[]
: 當(dāng)整個(gè)字符串所有字符處于ASCII
字符集范圍內(nèi)時(shí),就使用byte[]
(使用了ASCII
編碼)來存儲(chǔ),如果有任一字符超過了ASCII
的范圍,就退回到使用char[]
(UTF-16
編碼)來存儲(chǔ).但是這個(gè)功能實(shí)現(xiàn)的并不理想,所以沒有包含在Open JDK6
/Open JDK7
/Oracle JDK7
等后續(xù)版本中.
JavaScript
也使用了UTF-16
作為內(nèi)碼,其實(shí)現(xiàn)也廣泛應(yīng)用了CompressedString
的思想,主流的JavaScript
引擎中都會(huì)盡可能使用ASCII
內(nèi)碼的字符串,不過這些細(xì)節(jié)都是對(duì)外隱藏的..