从Properties乱码来学习编码
最近使用到java中的Properties
来获取一些变量信息,但如果变量值中有中文,那么最终录入到内存中的字符将会变乱码,那么是什么原因使得中文变成乱码呢?
1 字节与字符
为了了解中文乱码,我们先捋清字节和字符的概念。
我们平常说的字节(byte
),实际上是在计算机信息技术中计量存储容量的一种计量单位。在计算机中,能够识别和处理的数据都是二进制,而字节就是二进制位数的单位,一般情况下,一个字节表示8位二进制。
而字符(char
),则是计算机中使用的字母、数字、字和符号。由于计算机识别的数据都是是二进制,所以字符在计算机中的处理也是由二进制来进行。为了方便人们统计处理,一般用字节来衡量字符所占用的二进制位数。
需要注意的是,字符的长度在不同的系统中不同的语言中是不一样的,例如在C语言中一个字符(char
)就是一个字节(byte
)大小,而在Java语言中一个字符(char
)占用两个字节(byte
)大小。
2 字符集
在第一节中,我们缕清了字节和字符的关系,那么很多人接下来就想知道为什么字符0
在序号上是48(编码:0x30)、字符A
在序号上是65(编码:0x41)。
这里就要引入一个字符集的概念(编码的问题在下一章节细讲),1967年,为了统一不同计算机在相互通信时的字符编码标准,美国国家标准协会定制了一套字符集。ASCII ((American Standard Code for Information Interchange): 美国信息交换标准代码),这套字符集中包含了英语和一些其他小部分欧洲语言,共计128个字符。
随着计算机信息技术的发展,计算机系统需要处理的字符越来越多,像中文、日文和韩文等等。这时,ASCII字符集就已经不能满足各个国家的需求了,于是各个国家均开始了本国文字字符集标准的建设,像日文的字符集标准有:Shift_JIS、EUC-JP、ISO-2022-JP,中文的字符集标准有:GBK、BIG-5、GB2312等等,欧洲国家也制定了ISO-8859-1标准,扩展ASCII字符集到256位,其中包含了大部分欧洲语言。
在这些各国的文字字符集标准中,大多都对ASCII字符集做了兼容,所以在各个标准中,大家看到的字符0
、A
的序号都是48、65。
与此同时,国际组织随之制定了能够将全球字符纳入的字符集:Unicode。Java中的字符(char
)就采用的Unicode,之前我们说Java中的字符占两个字节,而Unicode中包含的字符远超65535,因此在Unicode中序号超过65535的字符就用Java中的两个字符(char
)表示。
很多人在这里就有疑问了,如果有一个Unicode字符占了两个Java的字符(char
),那么String.length()
方法岂不是有问题了?实际上,这个问题的确存在,但我们平常处理大部分的字符都在65535以内,所以平时也不用纠结String.length()
是否有问题。那如何得到真正的字符长度呢?String类提供了int codePointCount(int beginIndex, int endIndex)
方法。
下面是实际演示的例子:
3 字符编码集
第二节我们讨论了字符集,但我有意忽略了字符编码集,因为字符集和字符编码集很容易搞混。为了沟通顺畅,在这里我们对这两个概念做一解释。字符集中只有字符与序号的对应关系,例如字符0
在ASCII字符集中序号是48。字符编码集指的是为了在计算机中进行处理,字符与在计算机中的二进制编码的对应关系。
为什么二者容易搞混?原因是序号和编码在计算机中都是用二进制,而在很多数字符集中,字符集和字符编码集的关系就是一一对应的。例如字符0
在ASCII字符集中序号是48,它的16进制编码就是0x30。
但还有一些字符集的编码方式却和序号不是意义对应的,例如GB2312,举例来说,"啊"字是GB2312编码中的第一个汉字,它位于16区的01位,所以它的区位码(序号)就是1601。GB2312规定对收录的每个字符采用两个字节表示,第一个字节为“高字节”,对应94个区;第二个字节为“低字节”,对应94个位。所以它的区位码范围是:0101-9494。区号和位号分别加上0xA0就是GB2312编码。例如最后一个码位是9494,区号和位号分别转换成十六进制是5E5E,0x5E+0xA0=0xFE,所以'啊'字在GB2312字符集中的编码是0xFEFE。
需要注意的是,有些字符集的计算机编码规范只有一种,例如GB2312。但还有一些字符集的计算机编码方式却有多种,例如我们平常使用最多的UTF8编码,它是Unicode字符集中的一种编码方式,Unicode的编码方式还有UTF16、UTF32等等。为什么Unicode字符集会有多种编码方式?其中一个重要的原因就是Unicode字符集包含的字符太多,如果直接一一映射,那么每个字符需要占用4字节。为了减少字节占用,于是出现了UTF8编码。UTF8编码针对Unicode的一种可变长度字符编码,对于常用的英文字符只占用一个字节,对于中文常用的字符,只占用两个字节,这样做的好处是在IO时,需要传输的字节长度将会大大降低。
由于在Java中,字符集只支持Unicode,所以在Java的编码函数中,只有Unicode字符到各个字符集对应编码的映射关系,不存在各个字符集对应编码再映射回各个字符集中的序号的能力。例如"啊",在Java中可以编码为GB2312的字符编码0xFEFE,也可以从0xFEFE映射回Unicode的序号u554a
,但就是没有映射回GB2312序号1601的需要和能力。
4 Properties的问题
在捋清楚字节、字符集和字符编码集后,我们来看看Properties为什么会中文乱码。Java从文件读取字符串的流程如下:
- 获取文件对象
- 读取其中的字节(现在的文件编码大多是UTF-8)
- 将字节按照字符集的编码规范翻译成Unicode序号并产生字符(
char
) - 将字符组成字符串
而JDK中Properties的load方法有这样的一个注释。
Reads a property list (key and element pairs) from the input byte stream. The input stream is in a simple line-oriented format as specified in load(Reader) and is assumed to use the ISO 8859-1 character encoding; that is each byte is one Latin1 character. Characters not in Latin1, and certain special characters, are represented in keys and elements using Unicode escapes as defined in section 3.3 of The Java™ Language Specification.
The specified stream remains open after this method returns.
也就是说使用Properties加载文件数据时,并没有默认以UTF-8的编码规范来翻译字符到Unicode,而是以ISO-8859-1的编码规范来翻译字符到Unicode。由于ISO-8859-1编码规范中并不包含汉字,因此UTF8编码的字节将会变成ISO-8859-1字符集中的英文或拉丁文字,从而让人感觉是乱码。
4 Properties乱码解决
那么如何解决这个问题?该注释说明了解决办法,就是说如果要用到ISO-8859-1字符集以外的字符,就要使用Unicode转义,而Properties内部会将转义字符串再转回Unicode字符。
另外,可以使用Properties的synchronized void load(Reader reader)
方法来加载文件数据。Reader接口使用FileReader即可。因为Reader接口返回的是Unicode序号(也就是char
),而如果使用别的load方法,Properties内部将使用内部的LineReader来获取char,这个LineReader则默认以ISO-8859-1编码来处理生成Unicode序号(也就是char
)。
5 小结
字符集和字符编码集之前没有细究,这次趁排查Properties的机会,再次梳理了字符集和字符编码集的关系。
Java中全局使用的字符集是Unicode,而各类Charset的编码与解码,均不牵扯其他字符集,只是Unicode字符序号和各字符集计算机编码的映射。
- 使用Symfony的Console组件构建命令行程序
- 微软编程教育都在搞什么?从code.org到makecode,从Minecraft到Micro:bit
- 谷歌:通往完全自动驾驶之路
- 随时随地部署Kubernetes
- 使用CoreOs,Docker和Nirmata来部署微服务风格的应用程序
- 使用ACS和Kubernetes部署Red Hat JBoss Fuse
- 教你快速安装OpenShift容器平台3.6
- 面向开发者的Cloud Foundry
- 云数据库安全与农场和餐馆:知道来源的重要性
- 云数据库安全,农场和餐馆:知道你的来源的重要性
- NO.32 不堪重负:线程池拒绝策略
- 工厂模式进阶之Android中工厂模式源码分析
- C加加游戏编程,大神十年的绝技,正确的入门,这才叫学习
- 我们应该担心吗?人工智能现在可以通过交谈来学习新单词!
- JavaScript 教程
- JavaScript 编辑工具
- JavaScript 与HTML
- JavaScript 与Java
- JavaScript 数据结构
- JavaScript 基本数据类型
- JavaScript 特殊数据类型
- JavaScript 运算符
- JavaScript typeof 运算符
- JavaScript 表达式
- JavaScript 类型转换
- JavaScript 基本语法
- JavaScript 注释
- Javascript 基本处理流程
- Javascript 选择结构
- Javascript if 语句
- Javascript if 语句的嵌套
- Javascript switch 语句
- Javascript 循环结构
- Javascript 循环结构实例
- Javascript 跳转语句
- Javascript 控制语句总结
- Javascript 函数介绍
- Javascript 函数的定义
- Javascript 函数调用
- Javascript 几种特殊的函数
- JavaScript 内置函数简介
- Javascript eval() 函数
- Javascript isFinite() 函数
- Javascript isNaN() 函数
- parseInt() 与 parseFloat()
- escape() 与 unescape()
- Javascript 字符串介绍
- Javascript length属性
- javascript 字符串函数
- Javascript 日期对象简介
- Javascript 日期对象用途
- Date 对象属性和方法
- Javascript 数组是什么
- Javascript 创建数组
- Javascript 数组赋值与取值
- Javascript 数组属性和方法
- 在网页中动态的生成一个gif图片
- 在 Visual Basic .NET 或 JScript 代码中使用早期绑定
- 腾讯云TKE-GPU案例: TensorFlow 在TKE中的使用
- 使用pyppeteer 下载chromium 报错 或速度慢
- layui数据表格自定义每页条数limit设置
- dotnet OpenXML 幻灯片 PPTX 的 Slide Id 和页面序号的关系
- springboot 国际化
- Windows/Android/iOS全平台支持的视频播放器EasyPlayerPro,iOS版播放无音频问题如何解决?
- java之springboot之快速入门(一)- maven方式创建项目
- java之springboot之快速入门-Spring Initializr方式创建项目
- springboot之Web综合开发
- springboot之mybatis
- springboot之mybatis多数据源最简解决方案
- 蓝桥杯突击复习准备——部分算法汇总
- 小解c# foreach原理