前语:不要为了读文章而读文章,一定要带着问题来读文章,勤思考。
本文链接: http://1t.click/J7E
前言
最近在学习中涉及到计算机储存、传输数字和字符等操作,由于对字节、
2
进制、
10
进制、
16
进制、ASCII码的概念以及它们之间的关系和转换理解的不够透彻,导致在通讯、
MD5
消息摘要算法等时候出现问题,是因为
数据转成
计算机认识的01的这个环节出现问题。由于这个问题并不是那么容易发现,所以我也算是花了挺多时间才解决了这个问题,记录下解决过程,顺便也当复习一下计算机组成原理。
ASCII码
在计算机中,所有的数据在存储和运算时都要使用
二进制数表示(因为计算机用高电平和低电平分别表示1和0),例如,像a、b、c、d这样的52个字母(包括大写)以及0、1等数字还有一些常用的符号(例如*、#、@等)在计算机中存储时也要使用二进制数来表示,而具体用哪些二进制数字表示哪个符号,当然每个人都可以约定自己的一套(这就叫编码),而大家如果要想互相通信而不造成混乱,那么大家就必须使用相同的编码规则,于是美国有关的标准化组织就出台了ASCII编码,统一规定了上述常用符号用哪些二进制数来表示。ASCII 码一共规定了
128
个字符(
0000 0000
-
0111 1111
)的编码,比如空格
SPACE
是32(二进制
0010 0000
),大写的字母
A
是65(二进制
0100 0001
)。这128个符号(包括32个不能打印出来的控制符号),只占用了一个字节的后面7位(低7位),最前面的一位(高1位)统一规定为
0
(不要和数字的符号位搞混)。当然除了ASCII码,还有UTF-8、GBK等。字节字节(Byte)普通计算机系统能读取和定位到最小信息单位,即我们通过计算机储存和传输数据的时候都是先把数据转成字节。字节即
Byte
,一个字节代表
8
个比特(Bit),字节通常缩写为B,比特通常缩写为b。字节的大小是
8
Bit,即字节的范围是
0000 0000
-
1111 1111
,对于
无符号型,它表示的十进制范围是[0,255],对于
有符号型,高一位表示符号位,它表示的十进制范围是[-128,127]。
计算机如何储存数据
计算机只认识
0
和
1
(因为计算机只有高低电平两个状态),数据要想通过计算机储存或者传输,首先是要把数据转成计算机能认识的格式即01数据。我们举个例子,以储存十进制数字
28
和
-28
为例,首先将十进制数转成二进制。
需要注意的是: 数字在计算机中储存的是补码,而字符是在计算机中储存的是字符对应的编码(不要和数字的补码搞混)。
数字
储存十进制数字
28
和
-28
为例,首先将十进制数转成二进制,高
1
位为
0
代表正数,为
1
代表负数
12
| 28(10) = 0001 1100(2)(原码)-28(10) = 1001 1100(2)(原码)
|
然后计算机将二进制数字进行补码运算,运算结果如下
12
| 28(10) = 0001 1100(2)(原码) = 0001 1100(2)(补码)-28(10) = 1001 1100(2)(原码) = 1110 0100(2)(补码)
|
然后计算机保存的就是补码,当要取出数据的时候,就将补码逆运算一下,即可求出原码,再将原码转换一下就可以得到真实的数据了。
下面以Java语言演示这个过程,首先我们要清楚Java的byte、short、int、long都是有符号的(signed)
123456789101112131415161718
| public class test { public static void main(String[] args) throws NoSuchAlgorithmException { int a1 = 28; int a2 = -28;// 转成二进制表示 String b1 = Integer.toBinaryString(a1); String b2 = Integer.toBinaryString(a2);// 转成无无符号表示 String b3 = Integer.toUnsignedString(a1); String b4 = Integer.toUnsignedString(a2); System.out.println("28储存到计算机后为:" + b1); System.out.println("-28储存到计算机后为:" + b2); System.out.println("取出储存的28 以无符号表示:" + b3); System.out.println("取出储存的-28 以无符号表示:" + b4); }}
|
运行输出:
1234
| 28储存到计算机后为:11100-28储存到计算机后为:11111111111111111111111111100100取出储存的28 以无符号表示:28取出储存的-28 以无符号表示:4294967268
|
我们验证一下结果,验证了计算机确实是以补码的方式储存数字。这里有个小问题,就是我们知道
int
型有
4
个字节即
32
个比特,但是28却输出了
11100
5个比特而已,是因为
toBinaryString()
方法把
11100
前面的
0
给忽略了。取出的时候,我们以无符号的标准去处理,导致取出存入的
-28
结果是
4294967268
和我们存入的不一样,这是因为
-28
是负数,负数的补码和原码不一样,而用无符号处理的话就是直接将
11111111111111111111111111100100
转成结果了。而为什么
28
用有无符号处理结果都一样是因为正数的原码和补码一样,这样验证了Java的数据类型都是有符号的。
至于计算机为什么用补码来储存数字,而不是原码,原因是:拿单字节整数来说,无符号型,其表示范围是[0,255],总共表示了
256
个数据。有符号型,其表示范围是[-128,127]。先看无符号,原码和补码都一样,
0
表示为
0000 0000
,
255
表示为
1111 1111
,刚好满足了要求,可以表示
256
个数据。再看有符号的,若是用
原码表示,
0
表示为
0000 000
。因为咱们有符号,所以应该也有个
负0(虽然它还是0)
1000 0000
。这样的话那就有2个0,也就是只能表示
255
个数据,不能够满足我们的要求。而用补码则很好的解决了这个问题。
字符
在计算机中,对
非数值的字符进行处理时,要对字符进行数字化,即用二进制编码来表示字符。其中西文字符最常用到的编码方案有ASCII编码和EBCDIC编码。对于汉字,我国也制定的相应的编码方案,比如 GBK,GB2312等。比如字符
a
的
ASCII
码十进制值为
97
,在计算机中用二进制表示就是 01100001。下面同样用Java来演示计算机是如何储存字符的。1.采用UTF-8和GBK两种编码储存汉字
12345678910111213141516171819202122
| public class test { public static void main(String[] args) throws NoSuchAlgorithmException { String a1 = "中";// 采用两种不同的编码储存"中"这个汉字 比较两种编码 byte[] b1 = a1.getBytes("GBK"); byte[] b2 = a1.getBytes("UTF-8"); String c1 = binary(b1,2); String c2= binary(b2,2); System.out.println("GBK储存对应的二进制:" + c1); System.out.println("UTF-8储存对应的二进制:" +c2); } /** * 将byte[]转为各种进制的字符串 * @param bytes byte[] * @param radix 基数可以转换进制的范围,从Character.MIN_RADIX到Character.MAX_RADIX,超出范围后变为10进制 * @return 转换后的字符串 */ public static String binary(byte[] bytes, int radix){ return new BigInteger(1, bytes).toString(radix);// 这里的1代表正数 }}
|
我们调试看看,发现GBK编码采用
2
个字节储存,储存的数据分别是
10
进制的
-42
和
-48
对应的二进制分别是
11010110
和
11010000
(补码),即汉字
中
对应的二进制为
1101011011010000
,即16进制的
D6D0
,查看GBK对照表,发现16进制编码
D6D0
对应的汉字确实是
中
而UTF-8编码采用
3
个字节储存,同理将对应的二进制
111001001011100010101101
转成16进制,为E4B8AD,通过UTF-8编码查询,发现汉字
中
对应的16进制编码确实是
E4B8AD
2.储存字符
12345678910111213141516171819
| public class test { public static void main(String[] args) throws NoSuchAlgorithmException { String a1 = "EF";// 将字符串转成字节数组 byte[] b1 = a1.getBytes(); String c1 = binary(b1,2); System.out.println("对应的二进制:" + c1); } /** * 将byte[]转为各种进制的字符串 * @param bytes byte[] * @param radix 基数可以转换进制的范围,从Character.MIN_RADIX到Character.MAX_RADIX,超出范围后变为10进制 * @return 转换后的字符串 */ public static String binary(byte[] bytes, int radix){ return new BigInteger(1, bytes).toString(radix);// 这里的1代表正数 }}
|
调试看看,字符串
EF
有
E
和
F
两个字符,它们对应的十进制ASCII码分别是
69
和
70
我们发现Java的
getBytes()
方法是将字符串的每一个字符都储存到一个字节的,如果我们想把
EF
储存在一个字节里面,即
EF
是一个整体的,一个字节,不能拆分,那我们可以把
EF
放在一个字节里面
(byte)(0xEF)
,声明它是一个字节,不是字符,不用再将它转成字符对应的编码。下面说说我在进行
MD5
消息摘要算法时候遇到的坑,我要对QQ号对应的Hex进行
MD5
算法散列,这里我举例QQ号的
10
进制为
12345678
,对应的
16
进制为
00BC614E
(因为QQ号固定长度4个字节,所以前面补了2个0),一开始我是以下面的方式进行
MD5
算法的
12345678910111213141516171819202122232425
| public class test { public static void main(String[] args) throws NoSuchAlgorithmException { String qq = "00BC614E";// 将字符串转成字节数组 byte[] b1 = qq.getBytes(); MessageDigest md = MessageDigest.getInstance("MD5"); md.update(b1);// 得到MD5后的哈希值 byte[] hash = md.digest();// 将结构转成16进制 String c1 = binary(hash,16); System.out.println("对应的16进制:" + c1); } /** * 将byte[]转为各种进制的字符串 * @param bytes byte[] * @param radix 基数可以转换进制的范围,从Character.MIN_RADIX到Character.MAX_RADIX,超出范围后变为10进制 * @return 转换后的字符串 */ public static String binary(byte[] bytes, int radix){ return new BigInteger(1, bytes).toString(radix);// 这里的1代表正数 }}
|
调试可以看到上面的代码其实是将字符串
00BC614E
转成了
8
个字节,然后再对这8个字节进行散列,这也是基于
字符串进行的
MD5
散列,和通过网上一些网站散列得到的值是一样的
但是这个哈希值和预想的结果不一致,后来才知道预想的结果是基于
字节进行的
MD5
散列,也就是
00BC614E
应该分成
4
个字节(00、BC、61、4E)而不是
8
个字节(0、0、B、C、6、1、4、E),然后通过修改代码
123456789101112131415161718192021222324252627
| public class test { public static void main(String[] args) throws NoSuchAlgorithmException {// String qq = "00BC614E";// 将字符串转成字节数组// byte[] b1 = qq.getBytes(); byte[] b1 = {(byte)(0x00),(byte)(0xBC),(byte)(0x61),(byte)(0x4E)}; MessageDigest md = MessageDigest.getInstance("MD5"); md.update(b1);// 得到MD5后的哈希值 byte[] hash = md.digest();// 将结构转成16进制 String c1 = binary(hash,16); System.out.println("对应的16进制:" + c1); } /** * 将byte[]转为各种进制的字符串 * @param bytes byte[] * @param radix 基数可以转换进制的范围,从Character.MIN_RADIX到Character.MAX_RADIX,超出范围后变为10进制 * @return 转换后的字符串 */ public static String binary(byte[] bytes, int radix){ return new BigInteger(1, bytes).toString(radix);// 这里的1代表正数 }}
|
使用
(byte)
声明是一个字节,不是字符,不用再将它转成字符对应的编码。00、BC、61、4E分别是一个字节,当然因为字节为
8
个比特,能表示256个数字,因为Java的数据类型是有符号的,所以
8
个比特能表示的
10
进制范围是[-128,127],所以(byte)(x) x不能小于
-128
和不能大于
127
,否则会溢出,溢出的部分数据会丢失。热文推荐你知道SQL的这些错误用法吗?提高面试通过率?首先要具备以下技能。听说这10道Java面试题90%的人都不会!!!
同时,分享一份Java面试资料给大家,覆盖了算法题目、常见面试题、JVM、锁、高并发、反射、Spring原理、微服务、Zookeeper、数据库、数据结构等等。获取方式:点“
在看”,关注公众号并回复 面试 领取。