Java IO(02) 编码问题(二)

作为一个程序员可能在日常工作中对于编码的了解可能不会造成什么大的问题,但是作为计算机技术的基础知识,我认为还是有必要进行一下了解的,在本篇文章之前,还有一篇姊妹篇《Java IO(01)编码问题(一)》,其中用一些代码演示了编码的一些内容。那么本文章将会详细的进行说明编码的原理和演进,这作为第一部分进行介绍,另一部分呢,和本文或许关联性不是非常的密切,就是位运算的内容了,而且位运算这种计算方式,通常在面向对象语言中很少使用,大多出现在底层代码中,但是对于算法研究和阅读JDK源码来说还是很重要的,位运算的威力可以说很强大,记得上学的时候学习单片机和汇编课程的时候,位运算是不可能缺少的。

(一)编码


1、ASCII编码

就先从计算机行业人员最为熟知的ASCII编码说起吧。上一篇文章当中的程序中,英文字符都是一个字节表示的,比如说A是十六进制的41(二进制01000001),B是十六进制的42(二进制01000011)。我们来看一个图。

图1


一个字节(byte)需要8位(bit)来表示,所以一个字节最多有256种表示(0-255),但是从ASCII表中,我们可以看到,实际上我们并没有把256种编码全部使用,只使用了0-127这部分的二进制编码,也就是所以首位为”1“的八位二进制都没有进行编码。这里说的没有编码只是相对而言的,计算机使用一个字节来保存以上的字符,但是在后来的发展过程中,剩下的可利用编码被编为了欧洲语言中的一些符号,暂且不表。这就是ASCII编码了,每一个计算机专业的学生最先接触的编码。

2、GB xxxx编码

从以上我们知道了编码的意义,那么很明显ASCII码并无法表示世界上所有的字符,所以后面的128种编码被使用,但是即使这样,一个字节最多也只能表示256个字符,对比我泱泱中华的汉字来说,简直是车水杯薪。但是这难不倒我们,一个字节不行,再来一个嘛,两个字节就是16位(bit),可以表示2^16个字符。先不管这到底够不够,先来看看GB2312这种编码方式的原理,为了与ASCII编码兼容,0-127与ASCII编码一致,这些字符仍旧用一个字节表示。而规定两个码点大于127的字符连在一起表示一个汉字,其中第一个字节在0xA1-0xF7(10100001-11110111),第二个字节在0xA1-0XFE(10100001-11111110)之间,这样就有6763个汉字得到了编码表示,但是对于汉字来讲仍旧远远不够,所以就出现了更进一步的拓展"GBK"编码,在《Java IO(01)编码问题(一)》中,介绍了一种编码叫做"gbk",也就是接下来要介绍的"GBK",这种编码收录了21886个字符,其中21003个汉字,基本兼容"GB2312",它的两个字节的范围:第一个字节在0x81-0xFE(10000001-11111110),第二个字节在0x40-0xFE(1000000-11111110)。其实"GBK"是国标扩展的意思,^-^!

3、UNICODE

到这里是不是已经知道编码的基本原理,就是编嘛,但是,如果有一种编码格式,将世界上所有的符号都编一个独一无二的代码,那岂不是最好,这样就再也不用担心乱码的问题了。就这样,UNICODE就是这个世界上超级大的字符集,需要强调的是它只是一个字符集,它已经给世界上超过一百万种字符进行了编码。它的基本格式是U+[XX]XXXX,每个X代表一个十六进制的数,目前范围是U+0000--U+10FFFF。这样的超级字符集确实很好,但是也带了一个问题,那就是任何一个字符都要用4个字节,包括那些只需要7位(bit)的ASCII字符,现在都要用32位(bit)来表示了,这有点太浪费了。但是聪明的计算机科学家可不会直接把UNICODE编码拿到计算机上使用,而是用各种不同的实现方式应用到计算机上。

4、UTF-XXX

UTF(Unicode Transformation Format),是将UNICODE转换成最终编码的一种方式,在《Java IO(01)编码问题(一)》中明显我们已经用过了"utf-8"和"utf-16"。其中"utf-32"就是直接4个字节(32bit)来表示一个字符,可以说就是把UNICODE直接拿来用了,但是UNICODE中大部分的字符都不会完全占用四个字节,只需要两个字节就可以,这样就是"utf-16",只有当需要用到更多字节表示的字符时,才会选择四个字节来表示,其它均采用两个字符来表示,当然,只需要一个字节的字符也需要两个字节来表示,双字节编码嘛。同理,"utf-8"编码能用一个字节表示就用一个字节,不行的话再用两个,三个或四个,可以说"utf-8"最为灵活,是一种变长的,自适应编码(这是一个通信领域的编码技术,这里强行用一下这个次来描述)。

那么"utf-8"的具体方式是什么样的呢?两条规则:

  1. 对于单字节的符号,字节的第一位设为0,后面7位是这个符号的UNICODE码,所以对于英文字母,"utf-8"等同于ASCII码。
  2. 对于n(>1)个字节的符号,第一个字节的前n位都设为1,第n+1位设为0,后面字节的前两位一律设为10。剩下的二进制位,全部为这个符号的UNICODE码。

根据上表,就很好理解这两条规则了。如果一个字节的第一位是0,那么这个字节就是一个单独的字符;如果第一位是1,那么看有多少个连续的1,有几个就表示该字符占有几个字节。

5、大小端

(1) 什么是大小端呢?

所谓的大端模式,是指数据的低位保存在内存的高地址中,而数据的高位,保存在内存的低地址中。
所谓的小端模式,是指数据的低位保存在内存的低地址中,而数据的高位保存在内存的高地址中。

(2) 为什么会有大小端模式之分呢?

这是因为在计算机系统中,我们是以字节为单位的,每个地址单元都对应着一个字节,一个字节为8bit。但是在C语言中除了8bit的char之外,还有16bit的short型,32bit的long型(要看具体的编译器),另外,对于位数大于8位的处理器,例如16位或者32位的处理器,由于寄存器宽度大于一个字节,那么必然存在着一个如果将多个字节安排的问题。因此就导致了大端存储模式和小端存储模式。例如一个16bit的short型x,在内存中的地址为0x0010,x的值为0x1122,那么0x11为高字节,0x22为低字节。对于大端模式,就将0x11放在低地址中,即0x0010中,0x22放在高地址中,即0x0011中。小端模式,刚好相反。我们常用的X86结构是小端模式,而KEIL C51则为大端模式。很多的ARM,DSP都为小端模式。有些ARM处理器还可以由硬件来选择是大端模式还是小端模式。
那么IDEA的存储是大端还是小端呢?
 short x = 0x1122;
 System.out.println(Integer.toHexString(x&0xff));
运行结果:
22
所以是大端模式。

(二)位运算的威力


这里直接摘取 http://blog.csdn.net/iukey/article/details/7195265的部分,感谢这位作者的文章。

1、基础概念

位运算包括:&(与)、|(或)、^(异或)、~(取反)、>>(右移)、<<(左移)
环境预设:32位机下面,int占2个字节,有符号(括号外数字代表进制)
int a = 11;
int b = 1000;
(a)2 = (00000000 00001011 )2                                      //a的二进制表示
(b)2 = (00000011 11101000 )2                                       //b的二进制表示
a&b =(00000000 00001000 )2 =(8)10                            //一一为一,其它为0
a|b =  (00000011 11101011 )2 =(1003)10                      //有一为一,零零为0
a^b = (00000011 11100011 )2 =(995)10                        //相同为0,不相同为1
~b =   (11111100 00010111 )2 =(-31767)10                   //按位取反
b>>3 =  (00000000 01111101 )2 =(125)10                    // 去掉低3位,高位补0
或      =  (11100000 01111101 )2 =(-24701)10               //去掉低3位,高位补1    补0还是1具体情况视编译环境决定
a<<3 =  (00000000 01011000 )2 =(88)10                     //去掉高3位,低位补0
看了上面的例子,相信你已经明白具体规则了,不明白自己去google。下面讲具体作用。
位运算应用口诀
清零取数要用与,某位置一可用或
若要取反和交换,轻轻松松用异或

2、实例


3、应用实例

例1.子网掩码

子网掩码是个啥东东我也就不讲了,计算机科学技术本身就是个非常庞大系统,一个人不可能面面俱到,但是一些基本的尝试还是要懂的,不懂的可以自己去google,也可以等我的相关网络方面的文章。这里只讲与本问有关的应用部分。
假如我是一个网管,公司内部使用C类地址,现在我要把公司网络划分成5个子网,网络号为192.168.1.0的前三段,那么子掩码怎么填呢?
我现在先告诉你子网的子网掩码分别怎么填:192.168.1.224。(当然这里还有其他答案,我取的是在子网扩充不超过8个的情况下的每个子网所容纳主机最多的最佳方案)。
这个怎么来的呢?ip本身是个二进制的东东,为了方便人们设置,我们采用了点分十进制的转换,把32位的ip地址转换成了4个字节的十进制莱表示。比如 192.168.1.213 这个ip地址的二进制表示为:11000000 10101000 00000001 11010101 。对于C类地址默认的前三个字节表示网络号,那么这个网络号就是:11000000 10101000 00000001   ,最后一个字节11010101表示主机号,可以知道这个网络可以容纳的最多主机数为2^8-2,为什么减2自己去查。现在要划分子网,那么我们就要从表示主机的那个字节也就是8个位里面拿出几个位来表示子网号, 几位比较合适呢?这就要看你需要划分多少个子网咯。比如我们现在要划分5个子网,(5)10 = (101)2 ,那么至少就需要3位了,而且最多可以划分2^3 = 8个子网。现在你把224换成二进制看看吧(224)10 = (11100000)2  ,明白了吧,我们可以推断出子网掩码干了什么勾当?不错子网掩码与ip地址做了按位与运算,他的作用就是屏蔽了主机号获取网络号与子网号。如果你明白了这点,你就知道自己在192.168.1.64子网的ip该怎么填了,不会错误滴填成192.168.1.10了。 
竟然扯到一边去了,讲了半天才讲了一个与运算的应用。

例2. 防止int型变量溢出

int x = 32760;int y = 32762; 要求求x、y的平均值,要求空间复杂度位O(0)。
你能用常规方法去解决吗?可以。我不会讲,这里只讲位运算的 方法。
int ave(int x, int y)   //返回X、Y的平均值
{   
     return (x & y) + ( (x^y)>>1 );
}
知识点:
>>n 相当于除于2^n ,<<n 相当于乘于2^n .
x,y对应位均为1,相加后再除以2还是原来的数,如两个00001000相加后除以2仍得00001000,那么我们把x与y分别分成两个部分来看,两者相同的位分别拿出来则 :
x = (111111111111000)2 =  (111111111111000)2 +  (000000000000000)2
y =  (111111111111010)2 = (111111111111000)2 +  (000000000000010)2
相同部分我们叫做x1,y1,不同部分我们叫做x2,y2.那么现在(x+y)/2 =(x1+y1)/2 +(x2 + y2)/2 ,因为x1 == y1 ,所以(x1+y1)/2==x1 ==y1,相同部分我们用与运算求出来 x1 = x&y ,不同部分的和我们用^求出来,然后除于2是不是我们想要的结果了呢?言至于此,无需再言!
这个例子有点难于理解.但是经过我的分解应该还算好理解了,弄懂这个例子相信你的位运算已经登入大门了。

参考文章:

猜你喜欢

转载自blog.csdn.net/adelaide_guo/article/details/78757546