java基础之字符串编码知识点总结
⽬录
⼀、为什么要编码
⼆、如何“翻译”
三、Java 中需要编码的场景
3.1 I/O 操作中存在的编码
3.2 内存中操作中的编码
四、Java 中如何编解码
4.1 按照 ISO-8859-1 编码
4.2 按照 GB2312 编码
4.3 按照 GBK 编码
4.4 按照 UTF-16 编码
4.5 按照 UTF-8 编码
五、⼏种编码格式的⽐较
⼀、为什么要编码
不知道⼤家有没有想过⼀个问题,那就是为什么要编码?我们能不能不编码?要回答这个问题必须要回到计算机是如何表⽰我们⼈类能够理解的符号的,这些符号也就是我们⼈类使⽤的语⾔。由于⼈类的语⾔有太多,因⽽表⽰这些语⾔的符号太多,⽆法⽤计算机中⼀个基本的存储单元—— byte 来表⽰,因⽽必须要经过拆分或⼀些翻译⼯作,才能让计算机能理解。我们可以把计算机能够理解的语⾔假定为英语,其它语⾔要能够在计算机中使⽤必须经过⼀次翻译,把它翻译成英语。这个翻译的过程就是编码。所以可以想象只要不是说英语的国家要能够使⽤计算机就必须要经过编码。这看起来有些霸道,但是这就是现状,这也和我们国家现在在⼤⼒推⼴汉语⼀样,希望其它国家都会说汉语,以后其它的语⾔都翻译成汉语,我们可以把计算机中存储信息的最⼩单位改成汉字,这样我们就不存在编码问题了。
所以总的来说,编码的原因可以总结为:
计算机中存储信息的最⼩单元是⼀个字节即 8 个 bit,所以能表⽰的字符范围是 0~255 个
⼈类要表⽰的符号太多,⽆法⽤⼀个字节来完全表⽰,要解决这个⽭盾必须需要⼀个新的数据结构 char,从 char 到byte 必须编码
⼆、如何“翻译”
明⽩了各种语⾔需要交流,经过翻译是必要的,那⼜如何来翻译呢?计算中提拱了多种翻译⽅式,常见的有 ASCII、ISO-8859-1、GB2312、GBK、UTF-8、UTF-16 等。它们都可以被看作为字典,它们规定了转化的规则,按照这个规则就可以让计算机正确的表⽰我们的字符。⽬前的编码格式很多,例如 GB2312、GBK、UTF-8、UTF-16 这⼏种格式都可以表⽰⼀个汉字,那我们到底选择哪种编码格式来存储汉字呢?这就要考虑到其它因素了,是存储空间重要还是编码的效率重要。根据这些因素来正确选择编码格式,下⾯简要介绍⼀下这⼏种编码格式。
ASCII 码
学过计算机的⼈都知道 ASCII 码,总共有 128 个(0-127),⽤⼀个字节的低 7 位表⽰,0~31 是控制字符如换⾏回车删除等;32~126 是打印字符,可以通过键盘输⼊并且能够显⽰出来。
其中48~57为0到9⼗个阿拉伯数字
65~90为26个⼤写英⽂字母
97~122号为26个⼩写英⽂字母
ISO-8859-1
128 个字符显然是不够⽤的,于是 ISO 组织在 ASCII 码基础上⼜制定了⼀些列标准⽤来扩展 ASCII 编码,它们是 ISO-8859-1~ISO-8859-15,其中 ISO-8859-1 涵盖了⼤多数西欧语⾔字符,所有应⽤的最⼴泛。ISO-8859-1 仍然是单字节编码,它总共能表⽰ 256 个字符。
GB2312
它的全称是《信息交换⽤汉字编码字符集基本集》,它是双字节编码,总的编码范围是 A1-F7,其中从 A1-A9 是符号区,总共包含 682 个符号,从 B0-F7 是汉字区,包含 6763 个汉字。
GBK
全称叫《汉字内码扩展规范》,是国家技术监督局为 windows95 所制定的新的汉字内码规范,它的出现是为了扩展
GB2312,加⼊更多的汉字,它的编码范围是 8140~FEFE(去掉 XX7F)总共有 23940 个码位,它能表⽰ 21003 个汉字,它的编码是和 GB2312 兼容的,也就是说⽤ GB2312 编码的汉字可以⽤ GBK 来解码,并且不会有乱码。
GB18030
全称是《信息交换⽤汉字编码字符集》,是我国的强制标准,它可能是单字节、双字节或者四字节编码,它的编码与 GB2312编码兼容,这个虽然是国家标准,但是实际应⽤系统中使⽤的并不⼴泛。
UTF-16
说到 UTF 必须要提到 Unicode(Universal Code 统⼀码),ISO 试图想创建⼀个全新的超语⾔字典,世界上所有的语⾔都可以通过这本字典来相互翻译。可想⽽知这个字典是多么的复杂,关于 Unicode 的详细规范可以参考相应⽂档。Unicode 是Java 和 XML 的基础,下⾯详细介绍 Unicode 在计算机中的存储形式。
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 开始,表⽰它不是⾸字节,需要向前查才能得到当前字符的⾸字节
三、Java 中需要编码的场景
前⾯描述了常见的⼏种编码格式,下⾯将介绍 Java 中如何处理对编码的⽀持,什么场合中需要编码。
3.1 I/O 操作中存在的编码
我们知道涉及到编码的地⽅⼀般都在字符到字节或者字节到字符的转换上,⽽需要这种转换的场景主要是在 I/O 的时候,这个I/O 包括磁盘 I/O 和⽹络 I/O,关于⽹络 I/O 部分在后⾯将主要以 Web 应⽤为例介绍。下图是 Java 中处理 I/O 问题的接⼝:
Reader 类是 Java 的 I/O 中读字符的⽗类,⽽ InputStream 类是读字节的⽗类,InputStreamReader 类就是关联字节到字符的桥梁,它负责在 I/O 过程中处理读取字节到字符的转换,⽽具体字节到字符的解码实现它由 StreamDecoder 去实现,在StreamDecoder 解码过程中必须由⽤户指定 Charset 编码格式。值得注意的是如果你没有指定 Charset,将使⽤本地环境中的默认字符集,例如在中⽂环境中将使⽤ GBK 编码。
写的情况也是类似,字符的⽗类是 Writer,字节的⽗类是 OutputStream,通过 OutputStreamWriter 转换字符到字节。如下图所⽰:
同样 StreamEncoder 类负责将字符编码成字节,编码格式和默认编码规则与解码是⼀致的。
如下⾯⼀段代码,实现了⽂件的读写功能:
String file = "c:/";
String charset = "UTF-8";
// 写字符换转成字节流
FileOutputStream outputStream = new FileOutputStream(file);
OutputStreamWriter writer = new OutputStreamWriter(
outputStream, charset);
try {
writer.write("这是要保存的中⽂字符");
} finally {
writer.close();
}
// 读取字节转换成字符
FileInputStream inputStream = new FileInputStream(file);
InputStreamReader reader = new InputStreamReader(
inputStream, charset);
StringBuffer buffer = new StringBuffer();
char[] buf = new char[64];
int count = 0;
try {
while ((count = ad(buf)) != -1) {
buffer.append(buffer, 0, count);
}
} finally {
reader.close();
}
在我们的应⽤程序中涉及到 I/O 操作时只要注意指定统⼀的编解码 Charset 字符集,⼀般不会出现乱码问题,有些应⽤程序如果不注意指定字符编码,中⽂环境中取操作系统默认编码,如果编解码都在中⽂环境中,通常也没问题,但是还是强烈的不建议使⽤操作系统的默认编码,因为这样,你的应⽤程序的编码格式就和运⾏环境绑定起来了,在跨环境下很可能出现乱码问题。
3.2 内存中操作中的编码
在 Java 开发中除了 I/O 涉及到编码外,最常⽤的应该就是在内存中进⾏字符到字节的数据类型的转换,Java 中⽤ String 表⽰字符串,所以 String 类就提供转换到字节的⽅法,也⽀持将字节转换为字符串的构造函数。如下代码⽰例:
String s = "这是⼀段中⽂字符串";
byte[] b = s.getBytes("UTF-8");
String n = new String(b,"UTF-8");
另外⼀个是已经被被废弃的 ByteToCharConverter 和 CharToByteConverter 类,它们分别提供了 convertAll ⽅法可以实现byte[] 和 char[] 的互转。如下代码所⽰:
ByteToCharConverter charConverter = Converter("UTF-8");
char c[] = vertAll(byteArray);
CharToByteConverter byteConverter = Converter("UTF-8");
byte[] b = vertAll(c);
这两个类已经被 Charset 类取代,Charset 提供 encode 与 decode 分别对应 char[] 到 byte[] 的编码和 byte[] 到 char[] 的解码。如下代码所⽰:
Charset charset = Charset.forName("UTF-8");
ByteBuffer byteBuffer = de(string);
CharBuffer charBuffer = charset.decode(byteBuffer);
编码与解码都在⼀个类中完成,通过 forName 设置编解码字符集,这样更容易统⼀编码格式,⽐ ByteToCharConverter 和CharToByteConverter 类更⽅便。
Java 中还有⼀个 ByteBuffer 类,它提供⼀种 char 和 byte 之间的软转换,它们之间转换不需要编码与解码,只是把⼀个 16bit 的 char 格式,拆分成为 2 个 8bit 的 byte 表⽰,它们的实际值并没有被修改,仅仅是数据的类型做了转换。如下代码所以:
ByteBuffer heapByteBuffer = ByteBuffer.allocate(1024);
ByteBuffer byteBuffer = heapByteBuffer.putChar(c);
以上这些提供字符和字节之间的相互转换只要我们设置编解码格式统⼀⼀般都不会出现问题。
四、Java 中如何编解码
前⾯介绍了⼏种常见的编码格式,这⾥将以实际例⼦介绍 Java 中如何实现编码及解码,下⾯我们以“I
am 君⼭”这个字符串为例介绍 Java 中如何把它以 ISO-8859-1、GB2312、GBK、UTF-16、UTF-8 编码格式进⾏编码的。
public static void encode() {
String name = "I am 君⼭";
CharArray());
try {
byte[] iso8859 = Bytes("ISO-8859-1");
toHex(iso8859);
byte[] gb2312 = Bytes("GB2312");
toHex(gb2312);
byte[] gbk = Bytes("GBK");
toHex(gbk);
byte[] utf16 = Bytes("UTF-16");
toHex(utf16);
byte[] utf8 = Bytes("UTF-8");
toHex(utf8);
} catch (UnsupportedEncodingException e) {
e.printStackTrace();
}
}
我们把 name 字符串按照前⾯说的⼏种编码格式进⾏编码转化成 byte 数组,然后以 16 进制输出,我们先看⼀下 Java 是如何进⾏编码的。
下⾯是 Java 中编码需要⽤到的类图
⾸先根据指定的 charsetName 通过 Charset.forName(charsetName) 设置 Charset 类,然后根据 Charset 创建CharsetEncoder 对象,再调⽤ de 对字符串进⾏编码,不同的编码类型都会对应到⼀个类中,实际的编码过程是在这些类中完成的。
如字符串“I am 君⼭”的 char 数组为 49 20 61 6d 20 541b 5c71,下⾯把它按照不同的编码格式转化成相应的字节。
4.1 按照 ISO-8859-1 编码
字符串“I am 君⼭”⽤ ISO-8859-1 编码,下⾯是编码结果:
从上图看出 7 个 char 字符经过 ISO-8859-1 编码转变成 7 个 byte 数组,ISO-8859-1 是单字节编码,中⽂“君⼭”被转化成值是3f 的 byte。3f 也就是“?”字符,所以经常会出现中⽂变成“?”很可能就是错误的使⽤了 ISO-8859-1 这个编码导致的。中⽂字符经过 ISO-8859-1 编码会丢失信息,通常我们称之为“⿊洞”,它会把不认识的字符吸收掉。由于现在⼤部分基础的 Java 框架或系统默认的字符集编码都是 ISO-8859-1,所以很容易出现乱码问题,后⾯将会分析不同的乱码形式是怎么出现的。
4.2 按照 GB2312 编码
字符串“I am 君⼭”⽤ GB2312 编码,下⾯是编码结果:
GB2312 对应的 Charset 是 sun. EUC_CN ⽽对应的 CharsetDecoder 编码类是 sun. DoubleByte,
GB2312 字符集有⼀个 char 到 byte 的码表,不同的字符编码就是查这个码表到与每个字符的对应的字节,然后拼装成 byte 数组。查表的规则如下:
c2b[c2bIndex[char >> 8] + (char & 0xff)]
如果查到的码位值⼤于 oxff 则是双字节,否则是单字节。双字节⾼ 8 位作为第⼀个字节,低 8 位作为第⼆个字节,如下代码所⽰:
if (bb > 0xff) { // DoubleByte
if (dl - dp < 2)
return CoderResult.OVERFLOW;
da[dp++] = (byte) (bb >> 8);中文字符unicode查询
da[dp++] = (byte) bb;
} else { // SingleByte
if (dl - dp < 1)
return CoderResult.OVERFLOW;
da[dp++] = (byte) bb;
}
从上图可以看出前 5 个字符经过编码后仍然是 5 个字节,⽽汉字被编码成双字节,在第⼀节中介绍到 GB2312 只⽀持 6763个汉字,所以并不是所有汉字都能够⽤ GB2312 编码。
4.3 按照 GBK 编码
字符串“I am 君⼭”⽤ GBK 编码,下⾯是编码结果:
你可能已经发现上图与 GB2312 编码的结果是⼀样的,没错 GBK 与 GB2312 编码结果是⼀样的,由此可以得出 GBK 编码是兼容 GB2312 编码的,它们的编码算法也是⼀样的。不同的是它们的码表长度不⼀样,GBK 包含的汉字字符更多。所以只要是经过 GB2312 编码的汉字都可以⽤ GBK 进⾏解码,反过来则不然。
4.4 按照 UTF-16 编码
字符串“I am 君⼭”⽤ UTF-16 编码,下⾯是编码结果:
⽤ UTF-16 编码将 char 数组放⼤了⼀倍,单字节范围内的字符,在⾼位补 0 变成两个字节,中⽂字符也变成两个字节。从UTF-16 编码规则来看,仅仅将字符的⾼位和地位进⾏拆分变成两个字节。特点是编码效率⾮常⾼,规则很简单,由于不同处
版权声明:本站内容均来自互联网,仅供演示用,请勿用于商业和其他非法用途。如果侵犯了您的权益请与我们联系QQ:729038198,我们将在24小时内删除。
发表评论