Java中String使用及分析(UTF-8簡單編碼/解碼器實現)

0. Java中的字符串(String)
  • 在 Java 語言中,字符串即 字符序列(這裏的字符可以是一個英文字母例如 ‘A’,也可以是一個漢字例如 ‘楠’,也可以是一個韓語文字例如 ‘남’,也可以是一個 emoji 表情符號例如 ‘?’ 或 ‘?’)。原生類型 char 用來定義一個字符變量,char 類型字符變量用於保存一個字符。String 類型用來表示一個字符串,Java 中所有字符串字面量都是 String 類型的對象實例,而 String 類內部使用 char 類型的數組保存組成該字符串的字符序列,String 並沒有提供修改字符串即 字符序列的方法,但這並不因爲不能做到。下面是一個 String 類的構造器方法,如下:
public String(String original) {
        this.value = original.value;
        this.hash = original.hash;
    }

可見該方法使用一個 String 類型的實例對象初始化/創建另一個 String 對象,它們內部都指向同一個字符序列,這是通常我們創建一個 String 類型對象實例的方式之一:

String str = new String("this is string");

可見,下面的直接賦值的方法更簡潔/高效一點:

String str = "this is string";
  • Java中的字符串並非不能修改,下面的例子使用反射來修改字符串對象的值:
import java.lang.reflect.Field;

public class Test3 {
	public static void main(String[] args) throws NoSuchFieldException, 
	SecurityException, IllegalArgumentException, IllegalAccessException {
		String str1 = "真楠人";
		String str2 = "真楠人";
		// 打印字符串 str1 和 str2
		System.out.println("str1: " + str1 + ", str2: " + str2);
		// 通過反射更改字符串 str1 的值
		Field valueField = String.class.getDeclaredField("value");
		assert valueField != null;
		valueField.setAccessible(true);
		char[] value = (char[]) valueField.get(str1);
		value[0] = '假';
		// 再次打印字符串 str1
		System.out.println("str1: " + str1 + ", str2: " + str2);
	}
}

代碼執行結果如下:

str1: 真楠人, str2: 真楠人
str1: 假楠人, str2: 假楠人

上述程序中,我們通過反射方法僅僅修改了字符串 str1 的內容,但是最後字符串 str2 的內容也一起改變了,原因在於 str1 和 str2 都是引用同一個字符串對象(字符串字面量 “真楠人” 爲一個 String 類型的對象實例,雖然這個字面量在代碼中出現多次,但是都是引用了同一個 String 實例,可見相同字符序列的字符串共享同一個 String 實例,即 共享字符串,這也解釋了爲什麼 String 類沒有提供修改字符串的公共方法以及String 類被設計爲 final 類,都是爲了共享字符串),這並不難理解。

1. Java中的字符(char)
  • 下面的程序打印一個字符 ‘楠’ 字:
public class Temp_3 {
	public static void main(String[] args) {
		System.out.println('楠');
		System.out.println((char)26976);
		System.out.println('\u6960');
	}
}

運行結果爲:

楠
楠
楠

可見,三次打印輸出的結果都一樣。Java 語言使用 Unicode 字符集,Unicode 爲每一個字符都分配的一個唯一的數字,即 這個數字便代表與之對應的字符,其中 ‘\u6960’ 即爲字符 ‘楠’ 的 Unicode 十六進制編碼,而十進制數 26976 則爲這個十六進制數的十進制數值,它倆是同一個數值的不同表示方式而已。

  • 我們知道 ASCII 字符集中,字符 ‘A’ 的編號爲 65,即 可以對 65 進行強制類型轉換即可得到其對應的字符 ‘A’。數值 65 使用一個字節便可存儲,顯然對於字符 ‘楠’(對應數值爲 26976) 一個字節無法存儲,那麼它需要多少個字節才合適呢?
  • 我們可以看看其它編程語言中的情況,在 C 語言中也有一個 char 數據類型,但是它僅僅表示一個字節的空間,看下面的 C 語言中的例子:
#include <stdio.h>
void main(){
    char c = '楠'; // 一個字節
    printf("%c, %d, size=%ld\n", c, c, sizeof c);
    char c2 = 'A'; // 一個字節
    printf("%c, size=%ld\n", c2, sizeof c2);
    char c3[] = "楠"; // 字符串
    printf("%s, size=%ld\n", c3, sizeof c3);
    char c4[] = "真楠人"; // 字符串
    printf("%s, size=%ld\n", c4, sizeof c4);
}

執行結果如下:

ubuntu@cuname:~/dev/beginning-linux-programming/temp$ gcc -o use-char-string-test use-char-string-test.c
use-char-string-test.c: In function ‘main’:
use-char-string-test.c:4:14: warning: multi-character character constant [-Wmultichar]
     char c = '楠';
              ^
use-char-string-test.c:4:14: warning: overflow in implicit constant conversion [-Woverflow]
ubuntu@cuname:~/dev/beginning-linux-programming/temp$ ./use-char-string-test
�, -96, size=1			// c
A, size=1				// c2
楠, size=4				// c3
真楠人, size=10			// c4

可以看到,將字符 ‘楠’ 賦值給 char 類型的變量時,gcc 編譯器警告提示字符 ’楠‘ 爲多字節字符常量,而變量 c 僅僅存儲了字符 ’楠‘ 的一個字節的數據(且其數值爲 -96,-96 是字符 ‘楠’ 字編碼後的字節序列中的一個字節,變量 c 顯示亂碼),繼續查看後續的輸出結果,發現字符 ’A‘ 的長度爲 1 個字節,而每個漢字佔 3 個字節的存儲空間(C 語言中的字符串字面量的內部實現爲 char 類型的數組,且以一個空字符 ’\0‘表示字符串結尾,所以 sizeof 計算的字符串長度(即 字節數)比實際長度多一個字節)。3 個字節能表示的最大數值爲 ’2的24次方‘ - 1 = 16777215,遠遠大於 26976(即字符 ’楠‘ 對應的數字),其實 2 個字節就足以存儲數值 26976(即字符 ’楠‘),而實際上字符 ’楠‘ 卻佔用了 3 個字節空間,這是爲啥?這與字符(即 數值)的編碼(即 存儲)方式有關,我們知道 Unicode 給每個字符分配一個唯一的數值來代表該字符,例如任一一篇文章很可能會有多個字符,但是在存儲或傳輸該文章時,並不能就直接依次存儲或傳輸與每個字符對應的十進制數字序列,這裏需要考慮 2 個問題,第一如何從數字序列中識別一個字符,即每個字符的 ‘數字表示’ 其自身應當是一個整體,必須與其它的數字即與其相鄰的數字區分開,第二個問題:成本,字符 ‘A’ 使用 1 個字節即可存儲,但是字符 ‘楠’ 卻要使用至少 2 個字節才能滿足,這時,如果要求每個字符都是使用例如 2 個字節存儲,那麼對於英語國家的用戶來說,相當於增加並浪費了 1 倍的成本,這是不能被接受!UTF-8 便是一種可變長度的 Unicode 字符編碼解決方案,且被應用於 Java 語言,於是求助於 UTF-8(即 Unicode Transformation Format 8-bit,)。

2. UTF-8編碼
  • 參考鏈接UTF-8 and Unicode
  • 下面是對UTF-8編碼-規範文檔的解讀(需要注意區分,Unicode 只是一個字符集,它爲每個字符分配一個唯一的數字,從而可以用數字來表達字符,而 UTF-8 是一種編碼方式,描述怎樣實際存儲Unicode 字符對應的數值)
  1. UTF-8 兼容 ASCII 字符集,即 將編號 0 - 127 留給 ASCII 字符集,這裏(ASCII 字符集)總共 128 個數字(即 字符),使用 7 個 bit 的空間即可表示,每個 ASCII 字符佔用一個字節,且該字節的最高位始終爲 0,反過來,在 UTF-8 編碼中,最高位爲 0 的字節始終表示一個 ASCII 字符。
  2. UTF-8 使用 1 - 4 個字節來編碼 Unicode 字符對應的數字編號,規則如下圖
    utf-8編碼規則上圖與 UTF-8 規範文檔中的圖有點不同:即 可編碼的最大值不同,文檔圖如下:
    utf-8規範編碼規則再回到前面說的字符 ‘楠’ 字,該字符的 Unicode 編號爲 26976,而 26976 位於 2048 和 65535 之間,所以它使用 3 個字節進行編碼並存儲。在進行編碼時,只需將 26976 的二進制碼從低位到高位依次填入‘可用編碼位’ 即可得到字符 ‘楠’ 對應的 UTF-8 編碼,操作如下圖:
    utf8編碼一個字符‘楠’
    驗證,使用 notepad++ 新建一個文本文件,並寫入字符 ‘楠’ 字(注意,使用 utf-8編碼),保存文件,然後使用 WinHex 打開該文本文件,結果如下:
    在這裏插入圖片描述可見,結果正確。
  3. 上面是編碼一個字符,下面從 以UTF-8 編碼的字節數據中進行解碼(解碼是編碼的反向操作,編碼是將數值位依次插入到對應的可編碼位,解碼時則從可編碼位提取對應的數值位並將它們拼接在一起,從而還原出原來的數值)。綜上可以發現,以 UTF-8 編碼的數據,其字節類型總共有 5 種,其中只有 4 中類型的字節是可作爲一個 UTF-8 編碼的起始字節(即標識一個字符編碼的開始),如下圖:
    UTF-8編碼起始字節
    思路:將數值 240(即 ‘1111 0000’)與任意字節作 ‘&’ (即 ‘與’)位操作,其結果必定落在上圖中的某個取值範圍中,從而可以決定當前字節的類型(是否爲一個字符編碼的起始字節)。
    簡單 UTF-8 解碼器 Java 實現,代碼如下:
/** 簡單 UTF-8 解碼器(實際應用中,可能必須要注意/處理無效字符 即無效 unicode code-point 的情況) */
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.util.Random;

public class UTF_8_decoder {
	/** 1M,用於緩存數據 */
	private static final int BUFFER_SIZE = 1024 * 1024;
	/** 文本文件 */
	private static final String filePath = "C:/Users/jokee/Desktop/data-for-test.txt";

	public static void main(String[] args) throws IOException {
		File file = new File(filePath);
		FileInputStream fileReader = new FileInputStream(file);
		/** 用於緩存文件字節數據 即 緩存待解碼的數據 */
		byte[] buffer = new byte[BUFFER_SIZE];
		/** 緩存所有數據 */
		final int bytesLength = fileReader.read(buffer, 0, buffer.length);
		String result = doDecoding(buffer, bytesLength);
		// 打印結果
		System.out.println("解碼字符如下:\n" + "---------------------------------------------\n"
				+ result);
		fileReader.close();
	}

	/**
	 * 執行解碼操作,默認大端字節序
	 * <br> bytes : 待解碼字節緩衝區
	 * <br> bytesLength :緩衝區 bytes 中,待解碼的字節總數
	 */
	public static String doDecoding(byte[] bytes, int bytesLength) {
		/** 保存解碼後的數據 */
		char[] charData = null;
		int charData_index = 0;
		String result = null;

		if (bytes != null 
				&& (bytesLength = Math.min(bytesLength, bytes.length)) > 0) {
			// 1 從一個隨機位置開始解碼
//			int startIndex = new Random().nextInt(bytesLength);	
			// 2 解碼所有字節數據
			int startIndex = 0;

			/** 字符類型(參考 getType 方法) */
			int type = 0;
			/** 保存一個字符的 UTF-8 編碼 */
			byte[] encodedCharData = new byte[4];
			int encodedCharData_index = 0;
			// 初始化
			charData = new char[BUFFER_SIZE];

			for (; startIndex < bytesLength; ++startIndex) {
				if (encodedCharData_index == 0 && (type = getType(bytes[startIndex])) < 0) {
					// 直到找到一個 UTF-8 編碼的起始字節爲止
					continue;
				}
				// 讀取一個字符的編碼
				encodedCharData[encodedCharData_index++] = bytes[startIndex];
				if (encodedCharData_index < type) {
					// 繼續讀取該字符剩餘的其它字節
					continue;
				}
				// 解碼一個字符
				if (type == 1) {
					// 1
					// ascii 字符
					charData[charData_index++] = (char) Byte.toUnsignedInt(encodedCharData[0]);
				} else if (type == 2) {
					// 2
					// 提取第一個字節,屏蔽前 3 個 bit
					int b1 = 0b00011111 & Byte.toUnsignedInt(encodedCharData[0]);
					// 提取第二個字節,屏蔽前 2 個 bit
					int b2 = 0b00111111 & Byte.toUnsignedInt(encodedCharData[1]);
					int aChar = (b1 << 6) | b2;
					charData[charData_index++] = (char)aChar;
				} else if (type == 3) {
					// 3
					// 提取第一個字節,屏蔽前 4 個 bit
					int b1 = 0b00001111 & Byte.toUnsignedInt(encodedCharData[0]);
					// 提取第二個字節,屏蔽前 2 個 bit
					int b2 = 0b00111111 & Byte.toUnsignedInt(encodedCharData[1]);
					// 提取第三個字節,屏蔽前 2 個 bit
					int b3 = 0b00111111 & Byte.toUnsignedInt(encodedCharData[2]);
					int aChar = (b1 << 12) | (b2 << 6) | b3;
					charData[charData_index++] = (char)aChar;
				} else if (type == 4) {
					// 4
					// 提取第一個字節,屏蔽前 3 個 bit
					int b1 = 0b00000111 & Byte.toUnsignedInt(encodedCharData[0]);
					// 提取第二個字節,屏蔽前 2 個 bit
					int b2 = 0b00111111 & Byte.toUnsignedInt(encodedCharData[1]);
					// 提取第三個字節,屏蔽前 2 個 bit
					int b3 = 0b00111111 & Byte.toUnsignedInt(encodedCharData[2]);
					// 提取第四個字節,屏蔽前 2 個 bit
					int b4 = 0b00111111 & Byte.toUnsignedInt(encodedCharData[3]);
					int aChar = (b1 << 18) | (b2 << 12) | (b3 << 6) | b4;
					charData[charData_index++] = (char)aChar;
				}
				// 清理工作
				// 清除已緩存且已完成解碼的字符編碼,繼續處理下一個字符編碼
				encodedCharData_index = 0;
			}
			result = new String(charData, 0, charData_index);
		}
		return result;
	}

	/**
	 * 該方法用於確認參數字節 aByte 是否爲一個 UTF-8 編碼的起始字節, 返回值說明, <br>
	 * -1:字節 aByte 非起始字節, <br>
	 * 1:起始字節,類型爲 1,對應字符佔 1 個字節空間(實爲 ASCII 字符) <br>
	 * 2:起始字節,類型爲 2,對應字符佔 2 個字節空間 <br>
	 * 3:起始字節,類型爲 3,對應字符佔 3 個字節空間 <br>
	 * 4:起始字節,類型爲 4,對應字符佔 4 個字節空間
	 */
	public static int getType(byte aByte) {
		int type = 0b11110000 & aByte; // ‘0b11110000’ 爲十進制數 240 的二進制表示
		if (0 <= type && type < 128) {
			return 1;
		} else if (128 <= type && type < 192) {
			return -1;
		} else if (192 <= type && type < 224) {
			return 2;
		} else if (224 <= type && type < 240) {
			return 3;
		} else if (240 <= type && type < 248) {
			return 4;
		}
		return -1;
	}
}
  1. 既然上面有了一個簡單的解碼器,再加一個編碼器也不會多餘,下面是一個簡單的 UTF-8 編碼器實現(其中的位運算必須要細心,容易出錯):
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;

/** 簡單的 UTF-8 編碼器(實際應用中,可能必須要注意/處理無效字符 即無效 unicode code-point 的情況) */
public class UTF_8_encoder {
	/** 輸出文件 */
	private static final String filePath = "C:/Users/jokee/Desktop/data-for-test.txt";
	
	public static void main(String[] args) throws IOException {
		String text = "\u13A0,\u1B93,\u1D83,\u1F00,\u1F10,\u1910\nwo我愛wo家ya,wo愛北京天安門a,\nhei,今天又下雨了ne";
		int[] cs = new int[text.length()];
		for(int i = 0;i < text.length();i++) {
			cs[i] = text.charAt(i);
		}
		// do encoding
		Bytes bytes = doEncoding(cs, cs.length);
		// write to file
		FileOutputStream writer = new FileOutputStream(new File(filePath));
		writer.write(bytes.bytes, 0, bytes.length);
		writer.close();
		System.out.println("done.");
	}
	/** 將字符數組 chars 中的數量爲 charsLength 的字符進行編碼 */
	public static Bytes doEncoding(int[] chars, int charsLength) {
		Bytes bytes = null;
		if (chars != null && (charsLength = Math.min(chars.length, charsLength)) > 0) {
			int aChar = 0;
			int type = 0;
			/** 定義一個足夠容量的字節緩存區:假設每個字符都是佔用 4 個字節 */
			bytes = new Bytes(4 * charsLength);
			for(int i = 0;i < charsLength;i++) {
				aChar = chars[i];
				type = getType(aChar);
				switch(type) {
				case 1:
					bytes.add((byte)aChar);
					break;
				case 2:
					{
						int code = 0b1100000010000000;
						code |= (aChar & 0b111111);
						code |= ((aChar & 0b11111000000) << 2);
						bytes.add((byte)(code >> 8));
						bytes.add((byte)code);
					}
					break;
				case 3:
					{
						int code = 0b111000001000000010000000;
						code |= (aChar & 0b111111);
						code |= ((aChar & 0b111111000000) << 2);
						code |= ((aChar & 0b1111000000000000) << 4);
						bytes.add((byte) (code >> 16));
						bytes.add((byte) (code >> 8));
						bytes.add((byte) code);
					}
					break;
				case 4:
					{
						int code = 0b11100000100000001000000010000000;
						code |= (aChar & 0b111111);
						code |= ((aChar & 0b111111000000) << 2);
						code |= ((aChar & 0b111111000000000000) << 4);
						code |= ((aChar & 0b111000000000000000000) << 6);
						bytes.add((byte) (code >> 24));
						bytes.add((byte) (code >> 16));
						bytes.add((byte) (code >> 8));
						bytes.add((byte) code);
					}
					break;
				default:
					// nothing to do.
				}
			}
		}else {
			// 返回一個空的字節緩存區
			bytes = new Bytes(0);
		}
		return bytes;
	}
	
	/**
	 * 返回值表示一個字符 aChar 的編碼空間(即 需佔用的字節數),返回值如下 4 種:
	 * <br> 1,表示使用 1 個字節編碼字符 aChar 對應的數值
	 * <br> 2,表示使用 2 個字節編碼字符 aChar 對應的數值
	 * <br> 3,表示使用 3 個字節編碼字符 aChar 對應的數值
	 * <br> 4,表示使用 4 個字節編碼字符 aChar 對應的數值
	 * <b4> 如果 aChar 是無效的 UTF-8 字符,可選擇拋出異常:IllegalArgumentException,或忽略
	 */
	public static int getType(int aChar) {
		if (!Character.isValidCodePoint(aChar)) {
//			throw new IllegalArgumentException(aChar + " is invalid code point");			
			return 0;
		}
		if(0 <= aChar && aChar <= 127) {
			// a char of ascii
			return 1;
		}else if(128 <= aChar && aChar <= 2047) {
			return 2;
		}else if(2048 <= aChar && aChar <= 65535) {
			return 3;
		}else if(65536 <= aChar && aChar <= 2097151) {
			return 4;
		}else {			
//			throw new IllegalArgumentException(aChar + " is invalid code point");
			return 0;
		}
	}
	
	/** 組合一個字節緩存區 bytes 及其 長度值 length */
	public static class Bytes {
		private int length;
		private final int capacity;
		private byte[] bytes;
		public Bytes(int capacity) {
			this.length = 0;
			this.capacity = capacity;
			this.bytes = new byte[capacity];
		}
		/** 向該緩存區中添加一個字節 */
		public void add(byte aByte) {
			if (length < capacity) {				
				bytes[length++] = aByte;
			}else {
				throw new ArrayIndexOutOfBoundsException("當前緩存區已滿,capacity = length is " + capacity);
			}
		}
		public int getLength() {
			return length;
		}
		public byte[] getBytes() {
			return bytes;
		}
		public int getCapacity() {
			return capacity;
		}
	}
}
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章