Java 常見編碼格式及編碼轉換場景介紹

一.常見的編碼格式介紹

人類的語言種類很多,因而表示這些語言的符號就很多。計算機無法用一個基本的存儲單元— byte 來表示這些語言,所以必須要經過拆分或一些翻譯工作,才能讓計算機能理解。假設計算機能夠理解的語言是英語,其它語言要能夠在計算機中使用必須經過翻譯,把自己的語言翻譯成英語。這個翻譯的過程就是編碼。這樣就可以想象只要不是說英語的國家要能夠使用計算機就必須要經過編碼。

總結編碼的原因:

  1. 計算機中存儲信息的最小單元是一個字節即 8 個 bit,所以能表示的字符範圍是 0~255 個
  2. 人類要表示的符號太多,無法用一個字節來完全表示。那麼就需要一個新的數據結構 char,而從 char 到 byte 必須編碼

計算機中提拱了多種翻譯方式,常見的有 ASCII、ISO-8859-1、GB2312、GBK、UTF-8、UTF-16 等。它們都可以被看作爲字典,它們規定了轉化的規則,按照這個規則就可以讓計算機正確的表示需要的字符。例如 GB2312、GBK、UTF-8、UTF-16 這些格式都可以表示一個漢字,如何選擇哪種編碼格式來存儲漢字呢?存儲空間、編碼效率等因素影響我們正確選擇編碼的格式,下面簡要介紹一下這幾種編碼格式。

1.ASCII碼

ASCII碼基於拉丁字母的一套電腦編碼系統,主要用於顯示現代英語和其他西歐語言。總共有 128 個,用一個字節的第 7 位表示,0~31 是控制字符如換行回車刪除等;32~126 是打印字符,可以通過鍵盤輸入並且能夠顯示出來。

2.UTF-8

UTF-8是在互聯網上使用最廣的一種unicode的實現方式。其他實現方式還包括UTF-16和UTF-32。

UTF-8(8-bit Unicode Transformation Format)是一種針對Unicode的可變長度字符編碼,又稱萬國碼。由Ken Thompson於1992年創建。現在已經標準化爲RFC 3629。UTF-8用1到4個字節編碼Unicode字符。用在網頁上可以統一頁面顯示中文簡體繁體及其它語言(如英文,日文,韓文)。

2.Unicode編碼集

ISO 試圖想創建一個全新的超語言字典,世界上所有的語言都可以通過這本字典來相互翻譯。Unicode 是 Java 和 XML 的基礎, Unicode 在計算機中的存儲形式有以下幾種:

  • UTF-16

UTF-16 具體定義了 Unicode 字符在計算機中存取方法。UTF-16 用兩個字節來表示 Unicode 轉化格式,這個是定長的表示方法,不論什麼字符都可以用兩個字節表示,兩個字節是 16 個 bit,所以叫 UTF-16。UTF-16 表示字符非常方便,每兩個字節表示一個字符,這個在字符串操作時就大大簡化了操作,這也是 Java 以 UTF-16 作爲內存的字符存儲格式的一個很重要的原因。

  • UTF-8

UTF-16 統一採用兩個字節表示一個字符,雖然在表示上非常簡單方便,但是也有其缺點,有很大一部分字符用一個字節就可以表示的現在要兩個字節表示,存儲空間放大了一倍,這會增大網絡傳輸的流量。而 UTF-8 採用了一種變長技術,每個編碼區域有不同的字碼長度。不同類型的字符可以是由 1~6 個字節組成。

UTF-8 有以下編碼規則:

  1. 如果一個字節,最高位(第 8 位)爲 0,表示這是一個 ASCII 字符(00 - 7F)。可見,所有 ASCII 編碼已經是 UTF-8 了。
  2. 如果一個字節,以 11 開頭,連續的 1 的個數暗示這個字符的字節數,例如:110xxxxx 代表它是雙字節 UTF-8 字符的首字節。
  3. 如果一個字節,以 10 開始,表示它不是首字節,需要向前查找才能得到當前字符的首字節 

3.GBK編碼

GBK的出現是爲了擴展 GB2312,加入更多的漢字,它的編碼範圍是 8140~FEFE(去掉 XX7F)總共有 23940 個碼位,它能表示 21003 個漢字,它的編碼是和 GB2312 兼容的,也就是說用 GB2312 編碼的漢字可以用 GBK 來解碼,並且不會有亂碼。

GBK和UTF-8區別:GBK包含全部中文字符;UTF-8則包含全世界所有國家需要用到的字符。

 

 4.ISO8859-1編碼

ISO-8859-1編碼是單字節編碼,是ASCII編碼的擴展,它們是 ISO-8859-1~ISO-8859-15,其中 ISO-8859-1 涵蓋了大多數西歐語言字符,所有應用的最廣泛。ISO-8859-1 仍然是單字節編碼,它總共能表示 256 個字符。此字符集支持部分於歐洲使用的語言。Latin1(或Latin-1)是ISO-8859-1的別名。Tomcat7默認就是ISO8859-1編碼。

 

二.Java 中編碼和解碼流程

在用戶創建JAVA文件時,由於用戶環境不同,所以編碼是不同的;

Java 文件通常建議使用utf-8的編碼格式;

輸出到控制檯時,使用的是系統編碼,這就屬於系統內部規範的範疇了。

三.  Java 中常見編碼的場景   

1. I/O 操作中存在的編碼

讀取文件

Reader 類是 Java 的 I/O 中讀字符的父類,而 InputStream 類是讀字節的父類,InputStreamReader 類就是關聯字節到字符的橋樑,它負責在 I/O 過程中處理讀取字節到字符的轉換,它使用的字符集可以由名稱指定或顯式給定,否則可能接受平臺默認的字符集。

InputStreamReader在底層採用字節流來讀取字節,讀取字節後它需要一個編碼格式來解碼讀取的字節,如果在構造InputStreamReader沒有傳入編碼方式,那麼會採用操作系統默認的GBK來解碼讀取的字節。

例如:有一個test.txt的文件,假設test.txt編碼方式爲GBK,則可以使用如下代碼來讀取文件:

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

那麼讀取不會產生亂碼,因爲文件採用GBK編碼,所以讀出的字節也是GBK編碼的,而InputStreamReader默認採用解碼也是GBK。如果把test.txt編碼方式換成UTF-8,那麼採用這種方式讀取就會產生亂碼。因爲字節編碼(UTF-8)和當前解碼編碼(GBK)不一致造成的。所以指定字符集即可:

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

 下面是一個具體的例子:

	/*
	 *  轉換流,InputSteamReader讀取文本
	 *  採用UTF-8編碼表,讀取文件utf
	 */
	public static void readUTF()throws IOException{
		//創建自己輸入流,傳遞文本文件
		FileInputStream fis = new FileInputStream("c:\\utf.txt");
		//創建轉換流對象,構造方法中,包裝字節輸入流,同時寫編碼表名
		InputStreamReader isr = new InputStreamReader(fis,"UTF-8");
		char[] ch = new char[1024];
		int len = isr.read(ch);
		System.out.println(new String(ch,0,len));
		isr.close();
	}
	/*
	 *  轉換流,InputSteamReader讀取文本
	 *  採用系統默認編碼表,讀取GBK文件
	 */
	public static void readGBK()throws IOException{
		//創建自己輸入流,傳遞文本文件
		FileInputStream fis = new FileInputStream("c:\\gbk.txt");
		//創建轉換流對象,構造方法,包裝字節輸入流
		InputStreamReader isr = new InputStreamReader(fis);
		char[] ch = new char[1024];
		int len = isr.read(ch);
		System.out.println(new String(ch,0,len));
		
		isr.close();
	}

寫入文件

Writer類是 Java 的 I/O 中寫字符的父類,而 OutputStream類是寫字節的父類。通過 OutputStreamWriter 轉換字符到字節。

OutputStreamWriter在底層採用字節輸出流來寫文件。只是字符輸出流根據指定的編碼將字符轉換爲字節的。

OutputStreamWriter是字符流通向字節流的橋樑:使用指定的 charset 將要向其寫入的字符編碼爲字節。它使用的字符集可以由名稱指定或顯式給定,否則可能接受平臺默認的字符集。它需要一個編碼將寫入的字符轉換爲字節,如果沒有指定則採用GBK編碼,則輸出的字節都將是GBK編碼,生成的文件也是GBK編碼的。

同讀取文件,如果採用以下方式構造OutputStreamWriter,則寫入的字符將被編碼爲UTF-8的字節,生成的文件即爲UTF-8格式。

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

 下面是一個具體的例子:

	    /*
		 * 轉換流對象OutputStreamWriter寫文本
		 * 採用UTF-8編碼表寫入
		 */
		public static void writeUTF()throws IOException{
			//創建字節輸出流,綁定文件
			FileOutputStream fos = new FileOutputStream("c:\\utf.txt");
			//創建轉換流對象,構造方法保證字節輸出流,並指定編碼表是UTF-8
			OutputStreamWriter osw = new OutputStreamWriter(fos,"UTF-8");
			osw.write("你好");
			osw.close();
		}
		
		/*
		 * 轉換流對象 OutputStreamWriter寫文本
		 * 文本採用GBK的形式寫入
		 */
		public static void writeGBK()throws IOException{
			//創建字節輸出流,綁定數據文件
			FileOutputStream fos = new FileOutputStream("c:\\gbk.txt");
			//創建轉換流對象,構造方法,綁定字節輸出流,使用GBK編碼表
			OutputStreamWriter osw = new OutputStreamWriter(fos);
			//轉換流寫數據
			osw.write("你好");
			
			osw.close();
		}

2.內存中操作中的編碼

在內存中進行字符到字節的數據類型的轉換,Java 中用 String 表示字符串,所以 String 類就提供轉換到字節的方法,也支持將字節轉換爲字符串的構造函數。

  • getBytes(charset)

 這是String處理的一個標準函數,其作用是將字符串所表示的字符按照charset編碼,並以字節方式表示。

  • new String(charset)

這是java字符串處理的另一個標準函數,和上一個函數的作用相反,將字節數組按照charset編碼進行組合識別,最後轉換爲unicode存儲。

如下代碼示例:

        String str = "中文";
        byte[] gbks = str.getBytes("GBK");
        //[-42, -48, -50, -60]
        System.out.println(Arrays.toString(gbks));
        String newStr = new String(gbks,"GBK");
        //中文
        System.out.println(newStr);

既然讀文件要使用和文件編碼一致的編碼,那麼javac編譯文件也需要讀取文件,它使用的是什麼編碼呢?

以下三種情況是常用的編譯java源文件的方法

1.javac在控制檯編譯java類文件。

當在本地手動創建一個java文件Hello.java並保存。此時Hello.java文件的編碼爲ANSI,在中文操作系統下該文件的編碼即爲GBK.

然後使用javac命令來編譯該源文件。”javac Hello.java”。Javac需要讀取java文件,此時javac採用了操作系統默認的GBK編碼解碼讀取的字節的編碼也是Hello.java文件的編碼,二者一致,所以不會出現亂碼情況。

如果保存Hello.java文件時,選擇UTF-8保存。此時Hello.java文件編碼即爲UTF-8。再使用”javac Demo.java”來編譯,就會出現亂碼。這是因爲javac採用了GBK編碼解碼讀取的字節。保存的文件是UTF-8編碼的,所以會出現亂碼。

解決辦法就是使用javac的encoding參數來制定解碼編碼。“javac -encoding UTF-8 Demo.java” 這裏指定使用UTF-8來解碼讀取的字節,由於這個編碼和Hello.java文件編碼一致,所以就不會出現亂碼了。

2.IDE中編譯java文件。

  使用IDE開發過程中,通常將編碼設置成UTF-8。則每個項目中的java源文件的編碼就是UTF-8。這避免了使用javac可能出現的亂碼。IDE自動識別了工程中java源文件的文件編碼,然後採取了正確的encoding參數來編譯java源文件。

3.使用Ant來編譯java文件。

Ant在後臺其實是採用javac來編譯java源文件的,那麼1會出現的問題在Ant中也會存在。如果使用Ant來編譯UTF-8編碼的java源文件,並且不指定如何編碼,也會出現亂碼的情況。所以Ant的編譯命令<javac>有一個屬性” encoding”允許我們指定編碼,如果要編譯源文件編碼爲UTF-8的java文件,那麼命令應該如下:

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

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

4.tomcat中編譯jsp文件。

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會採用contentType中charset編碼來讀取jsp文件,jsp文件編碼通常設置爲UTF-8,contentType的charset也設置爲UTF-8,這樣tomcat使用UTF-8編碼來解碼讀取的jsp文件,二者編碼一致也不會出現亂碼。

那麼既不設置pageEncoding屬性,也不設置contentType的charset屬性,tomcat會採取什麼編碼來解碼讀取的jsp文件呢?

答案是:iso-8859-1,tomcat讀取文件採用的默認的編碼是iso-8859-1,如果用這種編碼來讀取文件顯然會出現亂碼。

四.JVM輸出時編碼轉換場景

上述分析的過程是源文件class轉碼過程。最終的class文件都是以unicode編碼的,上述是把各種不同的編碼轉換爲unicode編碼,比如從GBK轉換爲unicode,從UTF-8轉換爲unicode。只有採用正確的編碼來轉碼才能保證不出現亂碼。Jvm在運行時其內部都是採用unicode編碼的,在輸出時,會做一次編碼的轉換。分以下兩種情況:

1.採用System.out.println輸出。

比如:System.out.println(“測試”)。經過正確的解碼後”測試”是unicode保存在內存中的,但是在向標準輸出(控制檯)輸出時,jvm又做了一次轉碼,它會採用操作系統默認編碼(中文操作系統是GBK),將內存中的unicode編碼轉換爲GBK編碼,然後輸出到控制檯。因爲操作系統是中文系統,所以往終端顯示設備上打印字符時使用的也是GBK編碼。因爲終端的編碼無法手動改變,所以只要編譯時能正確轉碼,最終的輸出不會出現亂碼。

 

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

Jsp編譯成class後,如果輸出到客戶端,有個轉碼的過程。Java會採用操作系統默認的編碼來轉碼,那麼tomcat採用什麼編碼來轉碼呢?其實tomcat是根據<%@ page language="java" contentType="text/html; charset=utf-8" pageEncoding="utf-8"%>中contentType的charset參數來轉碼的,contentType用來設置tomcat往瀏覽器發送HTML內容所使用的編碼。

Tomcat根據這個編碼來轉碼內存中的unicode。經過轉碼後tomcat輸出到客戶端的字符編碼就是utf-8了。那麼瀏覽器怎麼知道採取什麼編碼格式來顯示接收到的內容呢?contentType的charset屬性會在HTTP響應頭中指定以通知瀏覽器。瀏覽器使用http響應頭的contentType的charset屬性來顯示接收到的內容。

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

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

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

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

爲了能更好的理解上面所說的解碼和轉碼過程,可以新建一個index.jsp文件,該文件編碼爲GBK,在jsp開頭寫上如下代碼:

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

這裏的charset和pageEncoding不同,但是也不會出現亂碼。

首先tomcat讀取jsp內容,並根據pageEncoding指定的GBK編碼將讀取的GBK字節解碼並轉換爲unicode字節碼保存在class文件中。然後tomcat在輸出時(out.println())使用charset屬性將內存中的unicode轉換爲utf-8編碼,並在響應頭中通知瀏覽器,瀏覽器以utf-8顯示接收到的內容。整個過程沒有一次轉碼錯誤,所以就不會出現亂碼情況。

 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章