在使用Java进行网络编程时,常常需要进行进制转换,而在进行这类操作时,往往需要对进制与Java数据类型有较深入的理解,才能确保在编程时不会出现错误。同时,深入了解进制能写出更加高效的代码。
本文先从计算机进制的两个基本概念入手,带读者简单的了解计算机进制的原理,然后再对Java数据类型和数据之间的转化展开论述,最后才对数据类型间的进制转化做详细说明。
本文由浅入深,适合初学者和进阶者阅读。
在计算机中,分有符号和无符号的两种数据类型,无符号的数据类型只能表示正数,而有符号的数据类型则可以表示负数。为了能将数据表示为负数,在有符号的数据类型中,会将二进制数的第一位作为符号位,0表示正数,1则表示负数。通过判断第一位的值,计算机就能判断这个数是正数还是负数。
Java不像c/c++这样的语言,不能声明无符号的数据类型,所以Java的所有数据类型都是有符号的。
什么是补码?
补码是计算机中用来表示负数,使得负数能够使用加法器参与加法运算的一种码。
在计算中,正数采用原码存储,原码即为本身数值,而负数采用补码的方式进行存储,这样做的目的是方便计算机运算,降低电路设计的复杂性。
如何快速求得负数补码?
补码的转换规则为符号位不变,其余逐位求反再加1
关于补码和符号位在此我不进行详尽的解释,因为超出了本文的论述范围,可以阅读我的博客 二进制补码计算原理详解 进行了解,博客地址:https://blog.csdn.net/zhuozuozhi/article/details/80896838
特别说明一点,符号位是可以直接参与运算的。
示例:使用二进制表示以下数值
byte positiveNumber = 19 ; // 0 0010011
byte negativeNumber = -45;// 1 1010011 (【原】1 0101101 -> 【反】1 1010010 -> 【补】1 1010011 )
在上面的例子中,数据类型为byte,所以占一个字节,即8 bit,其中1位充当符号位,那么还有七位充当数据位,所以byte的取值范围从0~255变成-128~127。19为正数,所以二进制数第一位为0;而-45为负数,所以它的符号位为1,在计算机中采用补码的形式存储负数。-45的负数原码为 1 0101101,按照补码转换规则,即求得负数补码为 1 1010011
在说明数据类型之前,笔者要特别说明一点,Java是一门面向对象的高级编程语言,他为一般高级编程提供便利性,所以隐藏了底层的一些细节,这才导致底层相对不容易被操作和使用。笔者之所以强调数据类型与数据类型的转换规则,是为讲解进制转换规则做好准备。
随便翻开一本Java入门书籍,里面都有基本类型的的占用字节和取值范围。最好记住这些内容。如下图是摘自《Java程序设计》的一张数据类型表。
数据的表示范围大小从小到大的排序为:byte < short < int < long < float < double
需要注意的是:float 和 double是实数类型的数据,虽然只占用四个字节和8个字节,但是他们的数据类型中存储了小数点、指数、有效位数及其他一些数据,所以他们的取值范围远大于相同字节数的int和long类型。
在进行数据类型转换的时候有如下几种情况:
数据范围小的数据向数据范围大的数据转换时,无需要进行特殊转换,数值保持不变
示例:byte类型向short类型转换
byte b = 64;
short s = b; //数值不变,不报错
如果数据范围大的数据类型数据范围小的数据类型转换时,小范围的数据可能无法表示该数值,所以需要强转
示例 : short类型向byte类型转换
short s = 125;
byte b = (byte)s; // b = 125; 125在数据范围内,没有变化
short s2= 129;
byte b = (byte)s; //b = -127; 129不在byte数据范围内,数值被强制转换,与原数据不一致。
分析上面例子的两个强转,由于byte的数值表示范围在-128 - 127,所以s 的值在范围内,能够顺利强转不出错,而s2却出现偏差,这就不得不了解它们的转换规则。
首先我们分析一下为什么short类型的129转化成byte类型,为什么会变成-127?观察short类型的129,short占两个字节,即16位,他的的二进制表示为 0 0000000 10000001 , 当我们强制转化成byte时,由于byte只有8bit,这就导致数据位溢出,short只能存储16位中的8位,Java会自动舍弃前面8位,保留后面8位,所以强转后就变成了 1 0000001。由于在Java中,第一位为符号位,这就导致了这个数强转之后被识别成了一个负数。而负数在符号位当中是以补码的形式存储,系统认为这是一个负数补码,我们从这个补码反求原码来确定整个负数的数值是多少,【补】1 0000001 -> [反] 1 0000000 -> [原]1 1111111 得到原码 1 1111111 ,所以这个数值才被强转为-127。
这是Byte类型转换图
byte类型的取值范围为-128~127,当某个数值到达上限127时,若再+1则会变成-128,周而复始。
当一个数值大于byte类型时,若要将他强转为byte类型时,则需要遵循此循环。
题外补充:浮点类型与整数的相关强制转换
我们知道,Int类型的最大值是4个字节31位表示的最大数,即 2147483647;如果浮点类型的数值在Integer之内,那么会直接省略小数部分,而当浮点数大于Integer范围的时候,会有一些不同。
示例: 浮点类型转long类型,int类型
double d = 2147483649.123d; //一个略大于Int类型表示范围的数
long l = (long)l;//l = 2147483649 //实际上,double转long类型,只要不超过Long类型表示的最大数,都是省略小数点以后取整即可
int i = (int)d; //2147483647
注意,当浮点类型的示数超过int 类型上限时,i就会固定取int 类型的最大值
byte b = (byte)d;//b=-1,
浮点转byte类型时,先将double转成int,再从int转到byte,当double超过Int上限时int会取最大值,再转化为byte时即固定为-1,short类型也是类似。
上一节中已经详细论述了Java的数据类型转换规则,在这一节,将从二进制角度看数据转换是如何进行的。
数据范围小的数据向数据范围大的数据转换时,数值保持不变,那么二进制数是如何变化的?
示例:正数byte类型向short类型转化
byte b = 64; // 二进制表示: 0 1000000
short s = b; // 二进制表示: 0 0000000 01000000
正数byte在向short类型转化时,s的高地址位会自动补0
例3:byte类型负数向short数据类型转化
byte b= -64 // 二进制表示: 1 1000000 = -64
short s = b //二进制表示: 1 1111111 11000000 = -64
在这个例子中,-64为负数,s的高位地址会自动补1,确保s反求原码还等于-64
负数向范围更大的类型转换时,原理上是先求负数原码,在数据高位上填充0,扩充到固定的数据类型宽度,然后再求补码。
在实际编程中,这个隐式转换会带来很多问题,比如你想把byte类型数据当做无符号类型使用,需要将无符号数据放在int数据类型中计算,但是Java并不知道,会在前面自动填充的1可能会导致计算出错。好在Java包装类中提供了向上无符号转型的方法
示例:利用Byte.toUnsignedShort()获取向上转型获得无符号Short数据
byte b = -45// 1 1010011
short s1 = b; //1 1111111 11010011
int s2 = Byte.toUnsignedInt(b); // 得到 0 0000000 11010011 = 211
如果在使用过程中想要向上转型,想得到无符号位的数据,那么就需要使用Byte提供的toUnsignedInt(),其他包装类也有提供向上无符号转型的方法。当然,熟练的可以不使用这些方法,直接进行位运算就好,这也是我推荐的。
如果数据范围大的数据类型数据范围小的数据类型转换时,直接截断高位数据
示例:short类型的129强转为byte类型
short s = 129;
byte b = (byte)s; // b = -127
当进行强制转化时,s的数据高位都被舍弃,只留下 10000001,因为最高位为1,计算机判定为负数补码,补码10000001表示 -127,所以就是强转后就是-127。
再次强调,由于Java数据类型没有提供无符号的数据类型,所以在进行这类操作时务必要注意数值的取值范围。
将数值转为二进制、八进制数、十六进制、任意进制字符串
int i = 100;
Integer.toBinaryString(i); //转二进制 = 1100100
Integer.toOctalString(i);//转8进制 = 144
Integer.toHexString(i);//转16进制 = 64
Integer.toString(int value,int radix); //转任意进制
使用使用有以下几点注意点
1. short和byte没有提供转进制字符串,实际上也不太需要,如果byte和short需要打印,可以先强转为int类型,使用Integer的方法进行打印,但是为负数的时候打印可能会有偏差,需要注意。
2. 打印时高位不会补0,比如打印int类型的二进制字符串,高位不会补0,需要自己格式化输出
从二进制、八进制、十六进制、任意进制字符串转到指定数值
1.几乎每个包装类型下面都有一个 valueOf(string value,int radix) 方法,可以使用这个方法转化
2.进制字符串的前缀(0x,0b..)不需要带,带了会报错。
格式化输出进制数
int i = 10;
String.format("%02x",i); // 0a
16进制可以直接补充,二进制需要自己实现
向上无符号转型
示例:byte通过方法向上转型获取int类型
Byte.toUnsignedInt(b); //其他包装类类似
示例: byte通过位运算向上转型获得short数据
byte b = -45; //1 1010011
short s = (short)(((int)b) & 0xff) //211
解析,Java中的类型转化不太方便,因为默认数值类型是int,所以所有的类型转化都要先转成int,再强转为自己想要的类型,在本例中,b为byte类型,且为负数,b强转为int类型的二进制表示为 1 1111111 11111111 11111111 11010011, 数值 0xff的二进制表示为 0 0000000 00000000 00000000 11111111,两者通过与位运算,
1 1111111 11111111 11111111 11010011
0 0000000 00000000 00000000 11111111 &
得
0 0000000 00000000 00000000 11010011 //即211
再通过将至转换为short,截断前16位,得到
0 0000000 11010011 , 得211
读取两个byte数值转化为一个short类型数据
byte b = -45; //1 1010011
byte b2 = 11; //0 0001011
short s = (short)(((int)b & 0xff << 8) | ((int)b2 & 0xff)) ; //-11509
解析, 首先通过(int)b & 0xff 获得整数值211,二进制数表示为 0 0000000 00000000 00000000 11010011,然后对这个数进行左移位8位
0 0000000 00000000 00000000 11010011 << 8 ,得 0 0000000 00000000 11010011 00000000,再计算 ((int)b2 & 0xff) 得 0 0000000 00000000 00000000 00001011
将两者进行或位运算
0 0000000 00000000 11010011 00000000
0 0000000 00000000 00000000 00001011 |
得
0 0000000 00000000 11010011 00001011
最后转为short截断前16位,算得
1 1010011 00001011,即-11509
实际上,在位运算中,看十进制数没意义,因为二进制运算的结果在十进制什么都看不出来
判断二进制数的某位是0还是1
示例:判断byte数据的第五位是0还是1
byte b = -45; //1 1010011;
boolean bool = (b & 0x10) != 0
解析:0x10的二进制数表示为 0001 0000
00000000 00000000 00000000 11010011
00000000 00000000 00000000 00010000 &
得
00000000 00000000 00000000 00010000 //16
通过与位计算,0x10除了第五位是1,其他位都是0,那么其他位进行与位运算,都会变成0,而第五位如果是1,那么得1,否则为0,而如果所有位都为0,那么所得结果为0,否则就是有值,以此来判断该位是否有值
思考:如果某数据类型判断的某两位为1还是0,该如何做,这个留给读者自己思考吧
补充:
十六进制,二进制如何快速转化?
示例:十六进制、二进制快速转化技巧
0101 1010 转16进制
一位十六进制对应4位二进制数,比如上面的二进制数,前四位得到的结果是 5,而后四位得到的数值为10,即a,所以这个数的十六进制表示数为 0x5a,反之,十六进制转二进制就是把一位16进制数转成4位二进制数。
位运算是如何进行的?
位运算就是将前后两个数值的每个位逐一进行布尔运算,算得想要的数值
示例:与位运算
0010 0010
1110 0001 &
上下对应逐一进行布尔运算,得
0010 0000
本文纯属个人理解,如有纰漏,请勿拍砖。