文字与编码的奥秘

文字的发明

文字是社会发展到一定阶段的产物。原始社会时期,人群、部落之间交流较少,有声语言已能满足需要。后来,随着社会生产的发展,人们的社会交际日趋频繁,而有声语言一发即逝,既不能传诸远方,亦不能留诸异日,用它作为传递思想信息的唯一手段,已越来越不能满足需要了。

在旧石器时代,社会组织和社会关系都很原始、很简单。人类社会的最基本的交际工具语言已经可以满足人们之间的交际需要了,不会发生创造文字的迫切要求。

到了新石器时代,社会组织逐渐复杂起来,生产范围也开始扩大,出现了初步的社会分工,交换也在这个基础上萌芽了;人口的”密集“也使社会生活大大”复杂“起来,氏族、部落间的联系和冲突也大大增加了。口耳相传的语言已经不完全适用了,人们产生了异时异地进行交际的要求。

文字正是应这种要求而诞生的。克服语言与时间、空间的矛盾,是当时社会遇到的社会性的问题,解决这个问题是全社会努力的结果。因此,文字是社会发展到特定阶段的社会需要的产物,也是全社会的发明。

各种各样的文字

不同国家,不同地区的人,他们的语言往往是不一样的,并且他们的文字也是各种各样的。近现代的世界文字,大概有5种最重要的文字系统:汉字、拉丁字母、斯拉夫字母、阿拉伯字母、印度字母。其中除了汉字是表意文字外,其余4种是表音文字,它们分别形成了五大文字流通圈。

汉字主要流通于东亚;拉丁字母占据美洲、大洋洲的全部,欧洲、非洲的大部分,亚洲的小部分;阿拉伯字母主要流通于北非、西亚的阿拉伯国家;斯拉夫字母主要流通于俄罗斯、白俄罗斯、乌克兰、保加利亚、塞尔维亚等国家;印度字母主要流通于南亚和东南亚。

各种各样的数字

虽然现代世界文字百花齐放,各种各样的文字都有其特殊性。对于数字来说,其种类虽然没有文字那么多,但也不少了,主要有:古埃及数字、罗马数字、玛雅数字、古印度数字(阿拉伯数字的前身)、阿拉伯数字、中文数字。目前还在使用的数字大概是以下这些:

古埃及数字

古埃及的数字系统是十进制的,他们有以下这些基础数字:

egypt-numbers.png

如果我想表达一个898的数字,则需要8个表示100的数字,9个表示10的数字,8个表示1的数字,加起来总共需要25个数字。用这么多数字表示一个比较大的数,这是非常可怕的,当数字越来越大时,所需要的基础数字也就越来越多。这种计数方式是非常复杂的,一方面是基础数字太多,另一方面是想表示一个数字时,需要画出所有的所需的基础数字。

罗马数字

罗马数字 没有进位制的概念,他没有表示零的数字。

rome-numbers.png

从上图中我们可以看出来,罗马数字中也是有一些基础数字的:

罗马数字阿拉伯数字罗马数字阿拉伯数字
1V5
X10L50
C100D500
M1000

大约在两千五百年前,罗马人还处在文化发展的初期,当时他们用手指作为计算工具。为了表示一、二、三、四个物体,就分别伸出一、二、三、四个手指;表示五个物体就伸出一只手;表示十个物体就伸出两只手。这种习惯人类一直沿用到今天。人们在交谈中,往往就是运用这样的手势来表示数字的。当时,罗马人为了记录这些数字,便在羊皮上画出Ⅰ、Ⅱ、Ⅲ来代替手指的数;要表示一只手时,就写成“Ⅴ”形,表示大指与食指张开的形状;表示两只手时,就画成“ⅤⅤ”形,后来又写成一只手向上,一只手向下的“Ⅹ”,这就是罗马数字的雏形。

rome-base-numbers.png

现在我们能在许多手表的表盘上看到罗马数字的身影:

rome-numbers-watch.png

玛雅数字

玛雅数字 是玛雅文明所使用的 二十进制 记数系统。

玛雅数字由3个符号的组合构成:0(贝形符号)、1(点)、5(横线)。

maya-numbers.png

小于等于19的数字,如19写作3根横线上另加4个点。

大于19的数字以20为进制累进。如30写作一个点(代表20),下面是一个贝形符号作为进制的标志,再下面加一个10(两道横线)。

特别是数系中“0”这个符号的发明和应用,无疑具有重要意义。前人栽树,后人乘凉,现代人均接受了玛雅人的独特创造,并称玛雅的数系为“人类最伟大的成就之一”。

阿拉伯数字

阿拉伯数字 的前身是印度数字,他是现今国际通用的数字。最初由 古印度人 发明,后由 阿拉伯人 传向欧洲,之后再经 欧洲人 将其现代化。正因阿拉伯人的传播,成为该种数字最终被国际通用的关键节点,所以人们称其为“阿拉伯数字”。阿拉伯数字由0,1,2,3,4,5,6,7,8,9共10个计数符号组成。采取位值法,高位在左,低位在右,从左往右书写。

公元3世纪,古印度的一位科学家 巴格达 发明了阿拉伯数字。最古的计数目大概至多到3,为了要设想“4”这个数字,就必须把2和2加起来,5是2+2+1,3这个数字是2+1得来的,大概较晚才出现了用手写的五指表示5这个数字和用双手的十指表示10这个数字。

两百年后,团结在伊斯兰教下的阿拉伯人征服了周围的民族,建立了东起印度,西从非洲到西班牙的阿拉伯帝国。后来,这个伊斯兰大帝国分裂成东、西两个国家。由于这两个国家的各代君王都奖励文化和艺术,所以两国的首都都非常繁荣,而其中特别繁华的是东都—巴格达,西来的希腊文化和东来的印度文化都汇集到这里来了。阿拉伯人将两种文化理解消化,从而创造了独特的阿拉伯文化。

大约700年前后,阿拉伯人征服了旁遮普地区,他们吃惊地发现:被征服地区的数学比他们先进。于是设法吸收这些数字。771年,印度北部的数学家被抓到了阿拉伯的巴格达,被迫给当地人传授新的数学符号和体系,以及印度式的计算方法(用的计算法)。由于印度数字和印度计数法既简单又方便,其优点远远超过了其他的计算法,阿拉伯的学者们很愿意学习这些先进知识,商人们也乐于采用这种方法去做生意。

后来,阿拉伯人把这种数字传入西班牙。公元10世纪,又由教皇 热尔贝·奥里亚克 传到欧洲其他国家。公元1200年左右,欧洲的学者正式采用了这些符号和体系。至13世纪,在意大利比萨的数学家 费婆拿契 的倡导下,普通欧洲人也开始采用阿拉伯数字,15世纪时这种现象已相当普遍。那时的阿拉伯数字的形状与现代的阿拉伯数字尚不完全相同,只是比较接近而已,为使它们变成今天的1、2、3、4、5、6、7、8、9、0的书写方式,又有许多数学家花费了不少心血。

巴格达发明的数字的演变图:

baghdad-numbers-evolution.png

阿拉伯数字演变史

最早人们用线条的多少来表示数字,但是当数字慢慢变多时,这种计数方式很不方便,画的线条会非常多。

arabic-numbers-evolution-1.png

就跟二进制一样,当要表示一个非常大的数字时,写出来的二进制会很长,所以迫切需要一种更先进的计数方式。

苏美尔人 发明了一种更先进的计数符号,但这些符号很复杂,估计科学家们都很难搞懂,并不适合日常的使用。

arabic-numbers-evolution-2.png

数学家 Abu Ja'far Muhammad ibn Musa AI-Khwarizmi (我晕,这名字真TM长,一看就是数学家)的出现,推动了数字发展的进程,进而推动了人类发展的进程。这个名字巨长的数学家(下面我们将简称他为 阿布 数学家),他需要做大量的演算,首先需要一套简单的计数符号。像上面 苏美尔人 发明的计数符号太复杂了,又是横着又是竖着,很可能一不小心就算错了。

为了解决自身的需求,阿布 教授发明了一套简单的计数符号,如下图所示:

arabic-numbers-evolution-3.png

从图中我们可以看出来有几个熟悉的身影:1、2、3、6、9。嘿嘿,跟我们现在的数字符号很接近,感觉有戏。

其实 阿布 教授想到的计数方法很简单,就是数每个符号上面角的个数。

arabic-numbers-evolution-4.png

可是我们现代数字中的4,明明不止4个角啊,有可能有5个甚至6个角,这怎么解释啊。

arabic-numbers-evolution-5.png

arabic-numbers-evolution-6.png

额,我没办法解释,在古人看来我们现代的4是错的,但是他现在就是这样了,我也没办法啊,我也管不了他。

arabic-numbers-evolution-7.png

5-10的符号要负责一点,需要借助一些辅助,在看之前,先画一条水平的线,横穿整个符号。

arabic-numbers-evolution-8.png

画完水平线之后,就把这些符号分成了两部分了,有的在水平线之上,有的在水平线之下。

那个圆圈,把他比作是一个握起来的拳头,而一个拳头是有5个手指吧,把他记作数字5很合理吧?

arabic-numbers-evolution-9.png

有了5之后,10就很自然而然的得到了,我把两个握紧的拳头举起来,超过水平线,就得到了10,哈哈,太机智了。

有了5和10之后,其他几个符号就很好创造了呀。5+1个角就是6,5+2个角就是7,相应的,10-1个角就是9,10-2个角就是8,完美!

arabic-numbers-evolution-10.png

arabic-numbers-evolution-11.png

后来为了书写方便,8的符号在水平线下方的两个直线也关闭起来了,就变成了现在8的样子。

arabic-numbers-evolution-12.png

5和7也慢慢的发生了变化,不过那条水平线一直伴随着他们。最终5只取了圆圈的右半部分,用来和6进行区分。

arabic-numbers-evolution-13.png

其中5的演变是这样的:

arabic-numbers-evolution-14.png

7的演变是这样的:

arabic-numbers-evolution-15.png

随着时间的推移,古代的数字从最初的样子慢慢的演变,最终成为了现代的模样:

arabic-numbers-evolution-16.png

arabic-numbers-evolution-17.png

进制的发明

进位制 (Positional notation),即用基数和基数的整数幂的和来表示数字的方法,是从 符值相加记数法 (Sign-value notation)演化而来的。

例如十进制中有10个基数,要计算整个数字的大小,需要从左往右计算每一位数字的值,然后将每一位上的数字的值相加后的和作为最终的结果。

在现代社会中,最常用的进制是十进制,但是我们仍然能看到很多其他的进制。十进制并不是一开始就确立了它的地位的。在阿拉伯数字成为国际计数符号之前,各国的计数方式中大量存在各种非十进制的计数系统。

列举一些常见的非十进制的应用:

应用场景包含的进制
罗马数字五进制 I,II,III,IV,V,VI
中国古代的算筹五进制
算盘五进制,十进制
唱票画的“正”字五进制
计算机二进制

计算机的发明

社会发展到另一个阶段产生了另一种伟大的发明:计算机。

世界上第一台电子数字式计算机于1946年2月15日在美国宾夕法尼亚大学正式投入运行,它的名称叫ENIAC(埃尼阿克),是电子数值积分计算机(The Electronic Numberical Intergrator and Computer)的缩写。

它使用了17468个真空电子管,耗电174千瓦,占地170平方米,重达30吨,每秒钟可进行5000次加法运算。虽然它的功能还比不上今天最普通的一台微型计算机,但在当时它已是运算速度的绝对冠军,并且其运算的精确度和准确度也是史无前例的。

圆周率(π)的计算为例,中国的古代科学家 祖冲之 利用算筹,耗费15年心血,才把圆周率计算到小数点后7位数。一千多年后,英国人香克斯以毕生精力计算圆周率,才计算到小数点后707位。而使用ENIAC进行计算,仅用了40秒就达到了这个记录,还发现香克斯的计算中,第528位是错误的。

ENIAC奠定了电子计算机的发展基础,开辟了一个计算机科学技术的新纪元。有人将其称为人类第三次产业革命开始的标志。

ENIAC诞生后,数学家 冯•诺依曼 提出了重大的改进理论,主要有两点:

  • 一是电子计算机应该以二进制为运算基础
  • 二是电子计算机应采用存储程序方式工作

并且进一步明确指出了整个计算机的结构应由五个部分组成:运算器、控制器、存储器、输入装置和输出装置。

冯•诺依曼 的这些理论的提出,解决了计算机的运算自动化的问题和速度配合问题,对后来计算机的发展起到了决定性的作用。直至今天,绝大部分的计算机还是采用 冯•诺依曼 方式工作。

至今为止,计算机中都是使用的二进制。那么为什么要用二进制,而不是十进制呢。这要从计算机的物理层面考虑,因为计算机是用数字电路构成的,而数字电路基本的构成是逻辑门电路,我们知道逻辑门电路的理论基础是布尔运算,而布尔运算的结果只有两种。而电子元器件最容易确定的状态就是导通与断开,正好对应布尔中的true和false,从硬件的角度看就是,电信号在0-2伏低电压用来表示0,2-5伏高电压用来表示1。所以这些都决定了计算机使用二进制,而不是十进制或者八进制。

其实八进制或十六进制也只是为了我们人类的方便而采用的,在C/C++的代码中不能直接写二进制,而是采用八进制或十六进制。可是为什么是八进制或十六进制呢?因为8或16都是2的次方,他们之间可以直接进行转换,并且通过左移右移的方式就可以快速转换。

计算机内部是采用的二进制进行运算和存储的。通过计算机来代替我们进行日常的工作,必然会遇到如何进行运算以及数据如何进行存储的问题,本篇文章我将和大家一起来了解下文字是如何在计算机中存储的。

说到文字,我们通常联想到的是各种各样的字符:中文、英文、日文、韩文等等。除此之外,就是数字了,这里的数字通常就是指全世界通用的阿拉伯数字。

数字编码

为了简单起见,假设我们定义了一个 无符号 的整型: int i=5; 那计算机拿到这个i之后,他是怎么知道这个变量的值是多少的呢?他又是如何存储的呢?

因为计算机是采用的二进制,而十进制的整数要存储时,首先要先转换成二进制。那么自然而然的就得到了数字编码的过程是:

十进制数字--->二进制数字

例如:数字 5 ,在计算机中的形态就是:0000 0101

上面说的是 无符号 的情况, 有符号 的情况更为复杂一些,二进制数字的最高位用作符号位。这时就涉及到另外一种情况了,即:原码,反码和补码。

正数的原码=反码=补码

负数的补码=反码+1

因为用补码存储时既能保证整数又能保证负数的值,所以计算机内部实际是用补码来表示一个数字的。

字符编码

数字编码比较简单,直接将十进制转换成二进制就可以了。但是字符就做不到了,但是我们可以把字符也想象成是一个虚拟的数字,然后再把这个虚拟的数字转换成二进制,不就可以让计算机去处理了吗?

所以字符编码的过程是:

字符------>虚拟数字
虚拟数字--->二进制数字

那怎么确定字符和虚拟数字之间的关系呢?其实这就是一个 编码 的过程,将每一个单独的字符映射为一个虚拟的数字。当我们把字符映射为数字之后,我们就得到了一个 字符集(Character Set)

我们可以这个字符集想象为一个包含字符与数字之间映射关系的表,这个表有一个名字,叫做 Code Page(码表) ,表中的每一个数字叫做 Code Point(码点) ,但是这个码点并不是最小的单元,他可能是由一个或多个 Code Unit(码元) 所组成的。

此外,字符集和字符编码是两个不同的概念,大家需要注意区分,举一个比较容易理解的例子,字符集相当于接口,字符编码相当于实现类。

charset-and-encoding.png

ASCII字符集

因为计算机是美国人发明的,最初设计的码表叫ASCII表。ASCII是American Standard Code for Information Interchange的缩写,他是美国人制定的一套字符编码方案。因为英文中只有52个字母(区分大小写),再加上数字和一些特殊符号和控制字符,总的来说需要编码的字符很少,所以最初的ASCII表中只有128个码点。具体的码表如下图所示:

ascii-chart.png

PS:ASCII表中的数字0-9是字符形式的数字,即:"1","2"..."9",和数字的1,2…9是不一样的。

ASCII表中的"1",对应十进制的数字是:49,转换成二进制是 0011 0001

EASCII字符集

计算机普及后,除了美国人使用之外,很多其他国家的人也开始使用起来。但是原本的ASCII码表已经太小了,所以需要重新找一张大表。最初的ASCII表中只用了一个字节中的7位,最高位是没有使用的,如果把最高位也利用起来的话,就可以多出来128个字符。后来,用人真的把这剩下的128个字符利用了起来,解决了部分西欧语言中的字符的映射。因为这个表是在ASCII表的基础上扩展出来的,所以被称为 EASCII字符集 ,我们经常看到的 ISO 8859-1 的编码方式就是 EASCII字符集 的一种实现。

GB XX字符集

再到后来,计算机传到中国之后,在ASCII码表的基础上,即便预留了128个码点可供选择,重新设计码点。但是对于汉字来说,128实在是太少了,所以我们需要重新造一张表。

GB2312字符集

最先被造出来的表是 GB2312 ,这张表中包含了7445个字符,其中汉字6763个。我们知道一个字节最大表示的范围(不考虑符号位)是0~255,共256个空间,2个字节的最大可表示的范围(不考虑符号位)是0~65535,共65536个空间,显然GB2312用一个字节是表示不全的,至少要用两个字节来表示。

为了与ASCII表兼容,码点在0~127(对应的十六进制是0x00~0x7F)之间的字符与ASCII中保持一致。然后用两个连在一起的字节来表示一个汉字,但是规定第一个字节的范围是0xA1~0xF7,第二个字节的范围是0xA1~0xFE。

说到这里我们就需要了解另外一个概念了:码元

首先记住一点:码元是组成码点的最小单位。一个码点可能由一个码元组成,也可能由多个码元组成。这取决于不同的编码方式中对码点值的处理方式,稍后我们将在Unicode字符集的编码实现中具体说明这个问题。

GB2312字符集对应的实现方式就是GB2312编码。

GBK字符集

由于GB2312字符集,只收录了6763个汉字,还有好多汉字并未收录,于是微软基于GB2312扩展出了GBK字符集。GBK字符集也是采用的两个字节,第一个字节在0x81~0xFE之间,第二个字节在0x40~0xFE之间,一共收录了两万多个码点,其中汉字有21003个,GBK与GB2312完全兼容。

GBK字符集对应的实现方式就是GBK编码。

GB18030字符集

GB18030字符集与GB2312和GBK基本兼容,但是不同的是GB18030采用变长字节的编码方式,这一点与UTF-8相同。

  • 单字节,从0到0x7F,与ASCII字符集兼容
  • 双字节,第一个字节范围是0x81~0xFE,第二个字节范围是0x40~0xFE,与GBK字符集兼容
  • 四字节,第一个字节范围是0x81~0xFE,第二个字节范围是0x30~0x39,第三个字节范围是0x81~0xFE,第四个字节范围是0x30~0x39

GB18030共收录了70244个汉字。

Unicode字符集

从ASCII字符集开始,后面由不同国家陆续推出了很多不同的字符集,也有各种各样的编码方案。但是,这带来另外一个问题,张三用A字符集编码的结果,李四用B字符集可能解码出来就会出现乱码了,甚至根本解码不出来。因为他们两个所用的码表是不一样的,码点也可能不一样,即便运气好,找到了相同的码点,也有可能解码出来是不同的字符。

那为了解决这种问题,我们就需要一个全世界都认同的大而全的码表,于是Unicode字符集就应运而生了。

由于Unicode字符集太大了,一下子管理不过来,所以在目前Unicode标准中,将字符按照一定的类别划分到0~16这17个平面(Plane层面)中,每个平面中拥有2^16 = 65536个码点。所以,目前Unicode字符集所拥有的码点总数为17*65536=1114112。

Unicode的平面划分,如下图所示:

unicode-charts-plane.png

Unicode的码点非常多,但是每个码点最少也需要4个字节,那和传统的ASCII码表就存在不兼容的问题了, 除此以外,如果每个码点都用4个字节来表示的话,就会造成空间的浪费。

UTF-XX编码

为了解决这些问题,就出现了 UTF-XX 这些编码方式,即Unicode码点转换方式(Unicode Transformation Format),一共有三种UTF编码方式,分别是:

  • UTF-8(8-bit Unicode/UCS Transformation Format)
  • UTF-16(16-bit Unicode/UCS Transformation Format)
  • UTF-32(32-bit Unicode/UCS Transformation Format)

其中最简单粗暴的就是UTF-32编码方式,他直接用4个字节来编码每个码点。而UTF-16是用2个字节或4个字节来表示码点的,这将取决于码点在Unicode中哪个Plane中,如果码点在最基本的BMP平面中,那么UTF-16将使用2个字节来编码,否则将使用4个字节来编码。最复杂,最灵活,用的最多的就是UTF-8编码方式了。他可以根据码点的范围使用1到4个字节来编码。

码元和码点

前面我们已经知道了,码点是由一个或多个码元组成的,我们用一个简单的例子来了解下。

utf-encoding.png

上图中每一个方框内的都是一个字符,字符下方的是该字符对应的 码点 ,用竖线分隔出来的每个独立的部分是该码点所对应的 码元

  • UTF-32

最简单的就是UTF-32编码方式,他是定长字节的,每个字符都是4个字节,这种方式下的码元是4字节的,每个码点由1个码元组成,并且码点是定长字节的。那么4个字节的码元就可能存在字节序的问题,例如 00 00 03 A9 变换字节序之后可能就变成了: 03 A9 00 00 ,这时解码就会出现问题。

  • UTF-16

UTF-16编码方式是变长字节的,可以看到有的码点只需要2个字节,有的码点需要4个字节。这种方式下码元是2字节的,每个码点可能由1个码元组成,也可能由2个码元组成,但是不管由几个码元组成,也都会出现字节序的问题。

  • UTF-8

UTF-8编码方式也是变长字节的,从1个字节到4个字节都有,但是他的码元是1个字节。也就意味着UTF-8编码方式不需要考虑字节序的问题。

unicode-utf.png

PS:好多人说Unicode编码,这种说法是不准确的,Unicode只是一个字符集,UTF-XX才是他具体的编码方式的实现,不过目前说Unicode编码的说法比较多,通常都把他默认为是UTF-16编码。

字节序

由于UTF-16是2字节码元,一个码点是由两个字节组成的,所以就存在字节序的问题。为了解决这个问题,Unicode规范中引入了一个叫BOM(Byte Order Mark)的东西,即指定这种编码使用哪种字节序来编码,一共有两种BOM:BE和LE,即我们所熟悉的大端序和小端序。

  • 大端序:高位字节在前,低位字节在后
  • 小端序:低位字节在前,高位字节在后

举个例子,汉字“语”用UTF-16编码,大端序的结果是: 8A 9E ,小端序的结果是: 9E 8A

为什么会有字节序这种奇怪的问题存在呢?这跟计算机的实现有关,我们人类阅读的习惯是大端序的,但是计算机先处理低位字节再处理高位字节时效率比较高,所以计算机更喜欢小端序。

bom.png

java中的编码

java中用来存储字符的类型有char和String,java规范中指出,char是由UTF-16编码格式的二字节码元来存储字符的。一个char占2个字节,即一个码元的大小,那么对于那些需要2个以上的字节存储的字符,是不能用char来保存的。String也是使用的UTF-16编码方式进行存储数据的,String可以用char[]数组进行存储,也可以用byte[]数组进行存储,这取决于字符串内字符的编码范围。

在Sun JDK6中有一个“压缩字符串”(-XX:+UseCompressedString)的功能。启用后,String内部存储字符串内容可能用byte[],也可能用char[]。当整个字符串所有字符都在ASCII编码范围内时,就使用byte[]来存储,此时字符串就处于“压缩”状态;反之,只要有任何一个字符超出了ASCII的编码范围,就退回到用char[]来存储。

下面我们来用一个简单的例子来看java中的字符编码,具体的代码如下:

private static String getHex(String str, Charset charset){
    byte[] bytes = str.getBytes(charset);
    StringBuilder sb = new StringBuilder();
    for(int i=0,s=bytes.length;i<s;i++){
        byte b = bytes[i];
        sb.append(byte2Hex(b));
        if(i<s-1){
            sb.append(" ");
        }
    }
    return sb.toString();
}

private static String byte2Hex(byte b){
    // byte(8位)转int(32位)时,高24位会被自动补齐1,而byte原本高24位是0,
    // 补齐之后二进制的补码值就变了,为了保持byte的值不变,与上0xff,
    // 这样高24位变为0,低8位保持不变
    String hexStr = Integer.toHexString(b & 0xff);
    if(hexStr.length()==1){
        hexStr = "0"+hexStr;
    }
    return hexStr;
}

private static void encode(){
    // 编码的过程
    String cn = "语";
    String en = "A";
    System.out.println(cn+"--encode with ASCII=======>"+getHex(cn,US_ASCII)); 
    System.out.println(en+"--encode with ASCII=======>"+getHex(en,US_ASCII));
    System.out.println(cn+"--encode with UTF-8=======>"+getHex(cn,UTF_8));
    System.out.println(cn+"--encode with UTF-16======>"+getHex(cn,UTF_16)); 
    System.out.println(cn+"--encode with UTF-32======>"+getHex(cn,UTF_32));
}

上面的 encode 方法先执行 String.getBytes() 来获取字符串的字节数组,然后转成十六进制的结果输出。执行完将打印出下面的信息:

语--encode with ASCII=======>3f
A--encode with ASCII=======>41
语--encode with UTF-8=======>e8 af ad
语--encode with UTF-16======>fe ff 8b ed
语--encode with UTF-32======>00 00 8b ed

首先我们需要知道 String.getBytes() 方法是获取指定字符的 外码 的过程,说到 外码 ,就需要知道与他对应的内码内码 是char或String在内存中存储时采用的编码方式,而 外码 则是字符在文件中存储,网络中传输时采用的编码方式。

第一行打印出来的 3f ,表示字符 ”语“ 在ASCII码表中没有找到对应的码点,所以编码的结果是返回了一个 ?

第二行打印出来的 41 ,就是字符 ”A“ 在ASCII码表中的码点,转换成十六进制后的结果。

第三行打印了三个字节,这与汉字 ”语“ 在UTF-8下的编码方式相符。

第四行就比较奇怪了,按照UTF-16编码方式,”语“ 的编码结果应该是 8b 4d ,开头多出来的两个字节是什么情况呢?

其实上面,我们已经了解到UTF-16编码方式会有字节序的问题,如果不指定字节序的话,UTF-16编码会在结果的字节流开头加上两个字节表示字节序: fe ff 表示大端序, ff fe 表示小端序。

第五行打印的也和预期相符。

如果我们指定UTF-16编码的字节序,那么输出的结果就不会再多出两个用来表示字节序的字节了,如下代码所示:

private static void encode(){
    // 编码的过程
    String cn = "语";
    String en = "A";
    System.out.println(cn+"--encode with UTF-16BE====>"+getHex(cn,UTF_16BE));
    System.out.println(cn+"--encode with UTF-16LE====>"+getHex(cn,UTF_16LE));
}

执行完之后,打印出如下的结果:

语--encode with UTF-16BE====>8b ed
语--encode with UTF-16LE====>ed 8b

乱码

private static void decode(){
    // 解码的过程
    String cn = "语";
    String en = "A";
    byte[] cnUtf8Bytes = cn.getBytes(UTF_8);
    byte[] cnUtf16Bytes = cn.getBytes(UTF_16);
    System.out.println(cn+"--encode with UTF-8,decode with UTF-8========>"+new String(cnUtf8Bytes,UTF_8));
    System.out.println(cn+"--encode with UTF-8,decode with UTF-16=======>"+new String(cnUtf8Bytes,UTF_16));
    System.out.println(cn+"--encode with UTF-8,decode with UTF-16BE=====>"+new String(cnUtf8Bytes,UTF_16BE));
    System.out.println(cn+"--encode with UTF-8,decode with UTF-16LE=====>"+new String(cnUtf8Bytes,UTF_16LE));
    System.out.println(cn+"--encode with UTF-16,decode with UTF-8=======>"+new String(cnUtf16Bytes,UTF_8));
    System.out.println(cn+"--encode with UTF-16,decode with UTF-16======>"+new String(cnUtf16Bytes,UTF_16));
    System.out.println(cn+"--encode with UTF-16,decode with UTF-16BE====>"+new String(cnUtf16Bytes,UTF_16BE));
    System.out.println(cn+"--encode with UTF-16,decode with UTF-16LE====>"+new String(cnUtf16Bytes,UTF_16LE));

    byte[] enUtf8Bytes = en.getBytes(UTF_8);
    byte[] enUtf16Bytes = en.getBytes(UTF_16);
    System.out.println(en+"--encode with UTF-8,decode with UTF-8========>"+new String(enUtf8Bytes,UTF_8));
    System.out.println(en+"--encode with UTF-8,decode with UTF-16=======>"+new String(enUtf8Bytes,UTF_16));
    System.out.println(en+"--encode with UTF-8,decode with UTF-16BE=====>"+new String(enUtf8Bytes,UTF_16BE));
    System.out.println(en+"--encode with UTF-8,decode with UTF-16LE=====>"+new String(enUtf8Bytes,UTF_16LE));
    System.out.println(en+"--encode with UTF-16,decode with UTF-8=======>"+new String(enUtf16Bytes,UTF_8));
    System.out.println(en+"--encode with UTF-16,decode with UTF-16======>"+new String(enUtf16Bytes,UTF_16));
    System.out.println(en+"--encode with UTF-16,decode with UTF-16BE====>"+new String(enUtf16Bytes,UTF_16BE));
    System.out.println(en+"--encode with UTF-16,decode with UTF-16LE====>"+new String(enUtf16Bytes,UTF_16LE));
}

执行完,将打印出如下结果:

语--encode with UTF-8,decode with UTF-8========>语
语--encode with UTF-8,decode with UTF-16=======>�
语--encode with UTF-8,decode with UTF-16BE=====>�
语--encode with UTF-8,decode with UTF-16LE=====>꿨�
语--encode with UTF-16,decode with UTF-8=======>����
语--encode with UTF-16,decode with UTF-16======>语
语--encode with UTF-16,decode with UTF-16BE====>语
语--encode with UTF-16,decode with UTF-16LE====>�
A--encode with UTF-8,decode with UTF-8========>A
A--encode with UTF-8,decode with UTF-16=======>�
A--encode with UTF-8,decode with UTF-16BE=====>�
A--encode with UTF-8,decode with UTF-16LE=====>�
A--encode with UTF-16,decode with UTF-8=======>��A
A--encode with UTF-16,decode with UTF-16======>A
A--encode with UTF-16,decode with UTF-16BE====>A
A--encode with UTF-16,decode with UTF-16LE====>�䄀

可以看到,用一种编码方式编码出来的结果,用另一种编码方式去解码,就会出现乱码的情况。甚至用相同的编码方式,解码时指定的字节序不同也会出现乱码的情况。

实用工具介绍

我们在处理自定义协议,或者抓包到一段报文时,常常需要进行协议的解析,而这时通常需要进行字符的解码。但是码流是用什么格式编码的我们是不知道的,为此笔者自己写了一个实用的工具,可以将一段字符编码成不同格式,也可以将一段码流用不同的编码方式进行解码。话不多说,直接看图:

Text2Hex

将字符用不同编码方式进行编码,并转成十六进制:

text-to-hex.png

Hex2Text

将十六进制的码流用不同的编码方式进行解码:

hex-to-text.png

Socket client

一个tcp客户端,连接上服务端后,可以发送数据,并将接收到的结果,转换成十六进制码流,然后自动用不同的编码方式进行解码,一眼就可以看出对方采用的何种编码方式:

socket-client.png

时间戳转换和md5计算

另外两个常用的工具是时间戳转换和md5计算

time-parser.png

md5-calculator.png

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值