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;
		}
	}
}
  • 2
    点赞
  • 5
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值