Java字符编码

基本概念

字符集

字符(Character)是各种文字和符号的总称,包括各国家文字、标点符号、图形符号、数字等。字符集(Character set)是多个字符的集合,字符集种类较多,每个字符集包含的字符个数不同,常见字符集名称:ASCII字符集、GB2312字符集、BIG5字符集、 GB18030字符集、Unicode字符集等。计算机要准确的处理各种字符集文字,就需要进行字符编码,以便计算机能够识别和存储各种文字。

字符编码

字符编码(英语:Character encoding)也称字集码,是把字符集中的字符编码为指定集合中某一对象(例如:比特模式、自然数序列、8位组或者电脉冲),以便文本在计算机中存储和通过通信网络的传递。

为了理解字符集和字符编码的关系,这里举个简单的例子,我们可以把字符集当成接口,把字符编码当成接口的实现。Unicode是接口(字符集),UTF-8 / UTF-16 / UTF-32则是不同的实现(字符编码)。

ANSI

ANSI全称(American National Standard Institite)美国国家标准学会(美国的一个非营利组织),首先ANSI不是指的一种特定的编码,而是不同地区扩展编码方式的统称,各个国家和地区所独立制定的兼容ASCII但互相不兼容的字符编码,微软统称为ANSI编码

在这里插入图片描述
在简体中文windows下使用文本文件保存”联通“,则再次打开会显示乱码。
这是因为windows下的文本文件默认使用ansi字符集,而简体中文windows下的ansi字符集为GB2312,”联通“两个字的GB2312编码看起来和UTF-8非常相似,又因为我们没有在文件开头设置字符集标记(BOM),所以当我们再次打开该文件时,被识别为UTF-8,因此出现乱码

Unicode

Unicode(统一码、万国码、单一码)是计算机科学领域里的一项业界标准,包括字符集、编码方案等。Unicode 是为了解决传统的字符编码方案的局限而产生的,它为每种语言中的每个字符设定了统一并且唯一的二进制编码,以满足跨语言、跨平台进行文本转换、处理的要求。

  • Code Unit:代码单元/编码单元,是Unicode编码里一个Code Point需要的最少字节数。例如:UTF-8是一个字节,UTF-16是两个字节,UTF-32是四个字节
  • Code Point:代码点,Unicode规定的每一个字符就是一个Code Point
  • Code Space:代码空间,所有的代码点构成一个代码空间,根据 Unicode 定义,总共有 1,114,112 个代码点,编号从 0x0-0x10FFFF,也就是大概 110 多万个字符
  • Code Plane:代码平面,Unicode 标准把代码点分成了17 个代码平面,编号为 #0-#16。每个代码平面包含 65,536(2^16)个代码点(17*65,536=1,114,112)。#0叫做基本多语言平面(BMP:大部分常用的字符都坐落在这个平面内,比如 ASCII 字符,汉字等。代码点范围:0x0000-0xFFFF),其余平面叫做补充平面
  • Surrogate Pair:代理对,由一个High-surrogate(高代理代码点:0xD800-0xDBFF)和一个 Low-surrogate(低代理代码点:0xDC00-0xDFFF)组成。这 2048 个代码点位于BMP内,并且不是有效的字符代码点,它们是为 UTF 编码保留的。在UTF-16中它可以编码BMP之外的代码点

UTF-8

UTF-8的特点是对不同范围的字符使用不同长度的编码。对于0x00-0x7F之间的字符,UTF-8编码与ASCII编码完全相同。UTF-8编码的最大长度是4个字节。

  • 对于单个字节的字符,第一位设为 0,后面的 7 位对应这个字符的 Unicode 码点。
  • 对于需要使用 N 个字节来表示的字符(N > 1),第一个字节的前 N 位都设为 1,第 N + 1 位设为0,剩余的 N - 1 个字节的前两位都设位 10,剩下的二进制位则使用这个字符的 Unicode 码点来填充。
Unicode编码(十六进制)UTF-8 字节流(二进制)
000000-00007F0xxxxxxx
000080-0007FF110xxxxx 10xxxxxx
000800-00FFFF1110xxxx 10xxxxxx 10xxxxxx
010000-10FFFF11110xxx 10xxxxxx 10xxxxxx 10xxxxxx

UTF-8 编码的最大长度是 4 个字节。从上表可以看出,4 字节模板有 21 个x,即可以容纳 21 位二进制数字。Unicode 的最大码位 0x10FFFF 也只有 21 位。

例1:“汉”字的Unicode编码是0x6C49。0x6C49在0x0800-0xFFFF之间,使用3字节模板:1110xxxx 10xxxxxx 10xxxxxx。将0x6C49写成二进制是:0110 1100 0100 1001, 用这个比特流依次代替模板中的x,得到:11100110 10110001 10001001,即E6 B1 89。

例2:Unicode编码0x20C30在0x010000-0x10FFFF之间,使用4字节模板 11110xxx 10xxxxxx 10xxxxxx 10xxxxxx。将0x20C30写成21位二进制数字(不足21位就在前面补0):0 0010 0000 1100 0011 0000,用这个比特流依次代替模板中的x,得到:11110000 10100000 10110000 10110000,即F0 A0 B0 B0。

UTF-16

UTF-16 是 Unicode 的一种编码方式,它用两个字节来编码 BMP 里的代码点,用四个字节编码其余平面里的代码点。
为了书写方便,我们把 Unicode 编码记作 U

  • 如果 U < 0x10000,U的 UTF-16 编码就是 U 对应的 16 位无符号整数。
  • 如果 U ≥ 0x10000,我们先计算 U’=U-0x10000,然后将 U’ 写成二进制形式:yyyy yyyy yyxx xxxx xxxx,U 的 UTF-16 编码(二进制)就是:110110yyyyyyyyyy 110111xxxxxxxxxx

为什么 U’可以被写成 20 个二进制位?

Unicode 的最大码位是 0x10FFFF,减去 0x10000 后,U 的最大值是 0xFFFFF,所以肯定可以用 20 个二进制位表示。

为什么把 0x10000-0x10FFFF编码为110110yyyyyyyyyy 110111xxxxxxxxxx

110110yyy yyyyyyy 的取值范围为11011000 00000000-11011011 11111111,即 0xD800-0xDBFF(High-surrogate
110111xxx xxxxxxx 的取值范围为11011100 00000000-11011111 11111111,即 0xDC00-0xDFFF(Low-surrogate
所以110110yyyyyyyyyy 110111xxxxxxxxxx正好是一个Surrogate Pair,也就是4个字节

例如:Unicode 编码 0x20C30,减去 0x10000 后,得到 0x10C30,写成二进制是:0001 0000 1100 0011 0000。用前 10 位依次替代模板中的y,用后 10 位依次替代模板中的x,就 得到:1101100001000011 1101110000110000,即 0xD843 0xDC30。

UTF-32

UTF-32 编码以 32 位无符号整数为单位。Unicode 的 UTF-32 编码就是其对应的 32 位无符号整数。

大/小端

大小端是CPU处理多字节数的不同方式,其主要特点是字节序在内存中存储位置不同

  • 大端(Big-Endian):高字节序存储在低地址,低字节序存储在高地址
  • 小端(Little-Endian):高字节序存储在高地址,低字节序存储在低地址

在这里插入图片描述
对于任何字符编码,编码单元的顺序是由编码方案指定的,与endian无关。例如GBK的编码单元是字节,用两个字节表示一个汉字。 这两个字节的顺序是固定的,不受CPU字节序的影响。UTF-16的编码单元是word(双字节),word之间的顺序是编码方案指定的,word内部的字节排列才会受到endian的影响。

在网络上传输数据时,由于数据传输的两端对应不同的硬件平台,采用的存储字节顺序可能不一致。所以在TCP/IP协议规定了在网络上必须采用网络字节顺序,也就是大端模式。

JVM屏蔽了大小端问题,默认为大端,并且可使用ByteOrder.nativeOrder()查询处理器和内存系统的大小端,在使用ByteBuffer时,也可以使用ByteBuffer.order()进行设置。

BOM

Unicode的学名是"Universal Multiple-Octet Coded Character Set",简称为UCS。UCS可以看作是"Unicode Character Set"的缩写。在UCS 编码中有一个叫做 “Zero Width No-Break Space”,中文译名作“零宽无间断间隔”的字符,它的编码是 FEFF。而 FFFE 在 UCS 中是不存在的字符,所以不应该出现在实际传输中。UCS 规范建议我们在传输字节流前,先传输字符 “Zero Width No-Break Space”。这样如果接收者收到 FEFF,就表明这个字节流是 Big-Endian 的;如果收到FFFE,就表明这个字节流是 Little- Endian 的。因此字符 “Zero Width No-Break Space” (“零宽无间断间隔”)又被称作 BOM(即Byte Order Mark)。

UTF-8 BOM: UTF-8以字节为编码单元,没有字节序的问题,但可以用 BOM 来表明编码方式。字符 “Zero Width No-Break Space” 的 UTF-8 编码是 EF BB BF。(windows系统默认使用UTF-8 BOM编码,需要注意

文件:D:\bom.txt,编码:UTF-8-BOM
在这里插入图片描述
代码:

public static void main(String[] args) throws Exception {
	File file = new File("D:\\\\bom.txt");
	 FileInputStream inputStream = new FileInputStream(file);
	 int i = 0;
	 while ((i = inputStream.read()) != -1) {
	     System.out.printf("0x"+Integer.toHexString(i) + " ");
	
	 }
}

结果

0xef 0xbb 0xbf 0xe6 0xb5 0x8b 0xe8 0xaf 0x95 0x55 0x54 0x46 0x38 0x20 0x42 0x4f 0x4d 

常见BOM

BOM Encoding字符编码
EF BB BFUTF-8 BOM
FE FFUTF-16 (big-endian)
FF FEUTF-16 (little-endian)
00 00 FE FFUTF-32 (big-endian)
FF FE 00 00UTF-32 (little-endian)

Java字符编码

Java使用Unicode字符集并且使用UTF-16字符编码。
Java语言规范规定,Java的char类型是UTF-16的code unit,也就是一定是16位(2字节)。

一个字符到底占用多少个字节?

对于Java中的char类型来说的话,固定占用2字节,但是为什么使用new String("字").getBytes().length返回的是3,这是因为getBytes实际是做了编码转换(内码转外码),你可以显式传入一个参数来指定编码,否则它会使用缺省编码来转换。
对于肉眼可见的字符来说,这个取决于字符编码,同一个字符在不同的编码下占用不同的字节。例如:汉字的"字","字"在GBK编码下占2字节,在UTF-8编码下占3字节,在UTF-32编码下占4字节

内码 & 外码

  • 内码:程序内部使用的字符编码,特别是某种语言实现其char或String类型在内存里用的内部编码。Java的内码就是UTF-16。
  • 外码:程序与外部交互时外部使用的字符编码。简单的来说就是除了内码都可以认为是“外码”(包括class文件的编码)。

内码转外码:

  • String.getBytes(String charsetName):将内存中的字符串用UTF-16编码转换为指定编码的 byte 序列
  • String.getBytes():将内存中的字符串用UTF-16编码转换为缺省编码的 byte 序列

外码转内码:

  • new String(byte[] bytes, String charset):就是把字节流以指定的编码转换为UTF-16编码的字节流存入内存中
  • Java的class文件是以UTF-8的方式来编码的。JVM读取class文件时需要把UTF-8编码转换为UTF-16编码读入内存

注意:编码和解码的“字符编码”必须要一致才能解码成想要的字符串。

缺省编码

可以在启动 JVM 时通过-Dfile.encoding=UTF-8来设置,否则使用操作系统环境下的缺省编码,可通过Charset.defaultCharset()获取编码
通常,Windows 系统下是 GBK,Linux 和 Mac 是 UTF-8。
如果使用IDE,则会使用工程的缺省编码,具体编码是什么,需要看具体使用的IDE,可以百度来修改IDE的编码。(有可能会遇到某IDE在启动项目时控制台打印的日志是乱码,这就是编码搞得鬼)
因为getBytes受缺省编码的影响而得到的结果不同,所以在使用该方法时,建议显示指定编码

String#length()获取的是真正的字符串长度吗?

不是,它获取的仅仅是代码单元的数量,而真正的字符串长度是代码点数量,可使用String#codePointCount()来获取。
原因:我们在上面介绍过,某字符的代码点>=0x10000时UTF-16编码会占用4个字节,又因为Java的char固定就是2字节,所以我们需要使用2个char来表示该字符,那么在使用String#length()获取字符串长度时,就会出错。
为什么我们平时都是使用该方法获取字符串长度呢?因为BMP里定义了我们使用的大部分字符,并且我们基本使用不到BMP之外的字符

为什么Java不使用定长编码呢?

Java设计之初UTF-16确实是定长编码,只不过后来Unicode的字符变多了之后,UTF-16变成了变长编码。
Java 5.0 版本既要支持 Unicode 4.0 同时要保证向后兼容性,不得不开始使用 UTF-16 作为内部编码方式,

代码点/代码单元分析

代码

// 16进制,10进制,2进制
// 0xd801, 55297, 1101 1000 0000 0001
// 0xdc01, 56321, 1101 1100 0000 0001
// UTF-16编码: 0xDB01 0xDC01, 3624000513, 110110 00 0000 0001 110111 00 0000 0001
// U': 0x401, 1025, 00 0000 0001 00 0000 0001
// Unicode码点 = U'+0x10000: 0x10401, 66561, 0000 0000 0000 0001 0000 0100 0000 0001
String str = "\ud801\udc01";
System.out.println("str = " + str);
// 代码单元(char)
System.out.println("str.length() = " + str.length());
// 代码点
System.out.println("str.codePointCount(0, str.length()) = " + str.codePointCount(0, str.length()));
// 代码点的int值,如果当前位置是高代理,则返回高代理+低代理对应代码点的int值,否则,会返回当前位置的int值
// 高代理,返回高代理+低代理对应代码点的int值
System.out.println("str.codePointAt(0) = "+str.codePointAt(0));
// 低代理,返回该位置的int值
System.out.println("str.codePointAt(1) = " + str.codePointAt(1));
// char,高代理
System.out.println("str.charAt(0) = " + str.charAt(0));
// char, 低代理
System.out.println("str.charAt(1) = " + str.charAt(1));

结果

str = 𐐁
str.length() = 2
str.codePointCount(0, str.length()) = 1
str.codePointAt(0) = 66561
str.codePointAt(1) = 56321
str.charAt(0) = ?
str.charAt(1) = ?

编解码

可通过Charset.availableCharsets().keySet()来查看Java到底支持哪些字符编码

  • 编码:字符集的字符 => 字节数组
  • 解码:字节数组 => 字符集的字符
  • 编码转换:字符集1的字符 => 字符集2的字符,通过这里可以看出如果2个字符集的某个字符没有对应关系,那么就会导致乱码

其他来源的字符保存到JVM 内存需要经历:字节数组 => 指定编码的字符 => Unicode字符 => UTF-16编码后的字节数组
JVM 内存保存到其他地方需要经历:UTF-16编码后的字节数组 => Unicode字符 => 指定编码的字符 => 字节数组
由此可以看出,系统内部是做了一步字符集转换。如果两个字符集没有转换规则,那么就会使用Codepage(代码页),有兴趣的同学可以自行去了解代码页。
代码

public static void main(String[] args) throws Exception {
	
	byte[] utf8Bytes = "编码转换".getBytes("UTF-8");
	System.out.println("UTF-8 = " + new String(utf8Bytes, "UTF-8"));
	System.out.printf("utf8Bytes = ");
	print(utf8Bytes); 
	 
	String gbk = new String(utf8Bytes, "GBK");
	System.out.println("GBK = " + gbk);
	byte[] gbkBytes = gbk.getBytes("GBK");
	System.out.printf("gbkBytes = ");
	print(gbkBytes);
	 
	byte[] isoBytes = gbk.getBytes("ISO-8859-1");
    System.out.printf("isoBytes = ");
    print(isoBytes);
     
	System.out.println("UTF-8 = " + new String(gbkBytes, "UTF-8"));
}
private static void print(byte[] bytes) {
	for (int i = 0; i < bytes.length; i++) {
		System.out.printf(bytes[i] + " ");
	}
	System.out.println();
}

结果

UTF-8 = 编码转换
utf8Bytes = -25 -68 -106 -25 -96 -127 -24 -67 -84 -26 -115 -94 
GBK = 缂栫爜杞崲
gbkBytes = -25 -68 -106 -25 -96 -127 -24 -67 -84 -26 -115 -94 
isoBytes = 63 63 63 63 63 63 
UTF-8 = 编码转换

在这里插入图片描述

  1. "编码转换"以UTF-8编码存储在class文件
  2. JVM加载class文件,从常量池中读取”编码转换“,并使用UTF-8解码为Unicode字符,然后使用UTF-16编码保存到JVM 内存
  3. “编码转换”.getBytes(“UTF-8”),直接从JVM内存获取"编码转换"的字节数组,然后使用UTF-16解码为Unicode字符,最后使用UTF-8编码为新的字节数组。(UTF-16 => UTF-8)
  4. new String(utf8Bytes, “GBK”),使用GBK把bytes解码为GBK字符,然后把GBK字符转换为Unicode字符,最后使用UTF-16编码保存到JVM内存(GBK和Unicode无联系,所以通过代码页来完成转换)

浏览器/Tomcat/Mysql

在这里插入图片描述

浏览器编码

URI:不同浏览器采用的编码方案不同。例如chrome使用UTF-8
header:ISO-8859-1
body:根据Content-Type来进行编码

chrome浏览器
在这里插入图片描述

浏览器解码

header:ISO-8859-1
body:首先查看Content-Type中是否存在编码方案,其次若返回的是html格式,则查看meta中是否指定字符集,最后使用浏览器默认字符集解码

在chrome中可以使用插件手动修改默认的字符集。例如:Set Character Encoding

tomcat编码

依赖于应用程序

tomcat解码

依赖于server.xml的Connector的配置
在这里插入图片描述
UTIEncoding:URI的编码,tomcat7默认ISO-8859-1,tomcat8默认UTF-8。
useBodyEncodingForURI:使URI编码等于request.setCharacterEncoding()设置的编码。
request.setCharacterEncoding():指定body体的编码方式,必须在第一次获取body体内容之前设置。

Spring

如果使用Spring MVC,则需要配置CharacterEncodingFilter为第一个过滤器,并且指定编码
如果使用Spring Boot,则无需配置,默认配置了OrderedCharacterEncodingFilter,默认编码为UTF-8
这个过滤器只是针对的body体,至于get请求还依赖于使用的tomcat版本,如使用Tomcat7,则需要去server.xml配置UTIEncoding/useBodyEncodingForURI,若使用Tomcat8,无需配置

Mysql

在这里插入图片描述
character_set_client:客户端数据解析、编码的字符集
character_set_connection:连接层字符集
character_set_database:当前数据库的字符集
character_set_server:服务器内部操作字符集
character_set_results:查询结果字符集
character_set_system:系统源数据(字段名等)字符集

set names utf8mb4等同于同时设置character_set_clientcharacter_set_connectioncharacter_set_results这三个字符集

  1. 客户端使用特定字符集编码SQL发送到服务端
  2. 服务端接受到字节流后,使用character_set_client进行解码
  3. 服务端解码后,会使用character_set_connection进行编码,然后传给存储引擎。若character_set_connectioncharacter_set_client不同,则发生字符集转换操作
  4. 存储引擎查询表时,若表字符集和character_set_connection不同,则发生字符集转换操作
  5. 存储引擎查询到结果之后,会使用character_set_results进行编码返回给客户端,若表字符集和character_set_results不同,则发生字符集转换操作
  6. 客户端接受到结果后,使用特定字符集进行解码
客户端character_set_clientcharacter_set_connection表字符集character_set_results结果
utf8utf8utf8/gbkutf8/gbkutf8正常
gbkgbkutf8/gbkutf8/gbkgbk正常
gbkutf8utf8/gbkutf8/gbkgbk乱码
gbkgbkutf8/gbkutf8/gbkutf8乱码
gbkutf8utf8/gbkutf8/gbkutf8乱码

由此表可以看出,必须保证客户端character_set_clientcharacter_set_results一致才可以保证数据正常。如果使character_set_connection表字符集和上面3个保持一致,可以减少字符集转换

Connector / J

在使用Connector / J时,创建连接语句jdbc:mysql://127.0.0.1:3306/test?useUnicode=true&characterEncoding=utf8基本都会配置useUnicodecharacterEncoding。否则创建连接时,驱动程序会自动检测character_set_server并使用该字符集。

要覆盖客户端自动检测到的编码,请使用characterEncoding服务器连接URL中的属性。指定字符编码时,请使用Java样式的名称。
在这里插入图片描述
对于Connector / J 5.1.46及更早版本:为了使用 utf8mb4 字符集进行连接,服务器必须配置为 character_set_server=utf8mb4;。如果不是这种情况,UTF-8则characterEncoding在连接字符串中使用时,它将映射到MySQL字符集名称 utf8。

对于Connector / J 5.1.47及更高版本:当UTF-8用于 characterEncoding连接字符串中,它映射到MySQL的字符集的名字 utf8mb4。

参考资料

https://www.cnblogs.com/binarylei/p/10760233.html
https://baike.baidu.com/item/Unicode/750500?fr=aladdin
https://baike.baidu.com/item/%E5%AD%97%E7%AC%A6%E9%9B%86/946585?fr=aladdin
https://baike.baidu.com/item/%E5%AD%97%E7%AC%A6%E7%BC%96%E7%A0%81/8446880
https://www.zhihu.com/question/27562173/answer/76208352
http://www.imooc.com/article/26166
http://www.fmddlmyy.cn/text6.html
https://blog.csdn.net/duduniao999/article/details/80872701
https://www.cnblogs.com/lanhaicode/p/11214827.html
https://www.zhihu.com/question/30945431/answer/50046808
https://www.cnblogs.com/jave1ove/p/7454966.html
https://jingyan.baidu.com/article/148a1921189b234d71c3b1df.html
https://dev.mysql.com/doc/connector-j/5.1/en/connector-j-reference-charsets.html

  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值