我们都知道Java中的byte是由8个bit组成的,而16进制即16中状态,它是由4个bit来表示的,因为24=16。所以我们可以把一个byte转换成两个用16进制字符,即把高4位和低4位转换成相应的16进制字符,并组合这两个16进制字符串,从而得到byte的16进制字符串。同理,相反的转换也是将两个16进制字符转换成一个byte。转换的函数如下:
/** * Convert byte[] to hex string * @param src * @return */ public static String bytesToHexString(byte[] src){ StringBuilder stringBuilder = new StringBuilder(""); if(src==null||src.length<=0){ return null; } for (int i = 0; i < src.length; i++) { int v = src[i] & 0xFF; String hv = Integer.toHexString(v); if (hv.length() < 2) { stringBuilder.append(0); } stringBuilder.append(hv); } return stringBuilder.toString(); } /** * Convert hex string to byte[] * @param hexString * @return */ public static byte[] hexStringToBytes(String hexString) { if (hexString == null || hexString.equals("")) { return null; } hexString = hexString.toUpperCase(); int length = hexString.length() / 2; char[] hexChars = hexString.toCharArray(); byte[] d = new byte[length]; for (int i = 0; i < length; i++) { int pos = i * 2; d[i] = (byte) (charToByte(hexChars[pos]) << 4 | charToByte(hexChars[pos + 1])); } return d; } /** * Convert char to byte * @param c * @return */ private static byte charToByte(char c) { return (byte) "0123456789ABCDEF".indexOf(c); }
bytesToHexString方法中src[i] & 0xFF将一个byte和0xFF进行了与运算,然后使用Integer.toHexString取得了十六进制字符串,可以看出src[i] & 0xFF运算后得出的仍然是个int,那么为何要和0xFF进行与运算呢?直接 Integer.toHexString(src[i]);,将byte强转为int不行吗?答案是不行的.
其原因在于:
- byte的大小为8bits而int的大小为32bits;
- java的二进制采用的是补码形式;
如果还不明白,我们还是温习下计算机基础理论和Java的位运算知识吧。
原码、反码和补码
计算机中的符号数有三种表示方法,即原码、反码和补码。三种表示方法均有符号位和数值位两部分,符号位都是用0表示“正”,用1表示“负”,而数值位,三种表示方法各不相同。
- 原码表示法是机器数的一种简单的表示法。其符号位用0表示正号,用:表示负号,数值一般用二进制形式表示。设有一数为x,则原码表示可记作[x]原。
例如X1= +1010110
X2= -1001010[X1]原=[+1010110]原=01010110
[X2]原=[-1001010]原=11001010 -
机器数的反码可由原码得到。如果机器数是正数,则该机器数的反码与原码一样;如果机器数是负数,则该机器数的反码是对它的原码(符号位除外)各位取反而得到的。设有一数X,则X的反码表示记作[X]反。
例如X1= +1010110
X2= -1001010
[X1]原=01010110
[X1]反=[X1]原=01010110
[X2]原=11001010
[X2]反=10110101 -
机器数的补码可由原码得到。如果机器数是正数,则该机器数的补码与原码一样;如果机器数是负数,则该机器数的补码是对它的原码(除符号位外)各位取反,并在未位加1而得到的。设有一数X,则X的补码表示记作[X]补。
例如[X1]=+1010110
[X2]=-1001010
[X1]原=01010110
[X1]补=01010110[X1]原=[X1]补=01010110
[X2]原= 11001010
[X2]补=10110101+1=10110110
为何要使用原码, 反码和补码
byte是一个字节保存的,有8个位,即8个0、1。8位的第一个位是符号位, 也就是说0000 0001代表的是数字1,而1000 0000代表的就是-1,所以正数最大位0111 1111,也就是数字127 负数最大为1111 1111,也就是数字-128。
这里 0 是 00000000 ,而 10000000 是-1 ,正数计算里面去掉了一个0,所有最大值只能是2^7 -1 =127;而负数并没有用去掉0,所以是2^7 = -128 。
现在我们知道了计算机可以有三种编码方式表示一个数。 对于正数因为三种编码方式的结果都相同:
[+1] = [00000001]原 = [00000001]反 = [00000001]补
所以不需要过多解释, 但是对于负数:
[-1] = [10000001]原 = [11111110]反 = [11111111]补
可见原码,,反码和补码是完全不同的。 既然原码才是被人脑直接识别并用于计算表示方式,为何还会有反码和补码呢?
首先, 因为人脑可以知道第一位是符号位,在计算的时候我们会根据符号位,选择对真值区域的加减(真值的概念在本文最开头)。但是对于计算机,加减乘数已经是最基础的运算,要设计的尽量简单。计算机辨别"符号位"显然会让计算机的基础电路设计变得十分复杂! 于是人们想出了将符号位也参与运算的方法。我们知道,根据运算法则减去一个正数等于加上一个负数,即: 1-1 = 1 + (-1) = 0,所以机器可以只有加法而没有减法,这样计算机运算的设计就更简单了。
于是人们开始探索 将符号位参与运算,并且只保留加法的方法。首先来看原码:计算十进制的表达式: 1-1=0
1 - 1 = 1 + (-1) = [00000001]原 + [10000001]原 = [10000010]原 = -2
如果用原码表示,让符号位也参与计算,显然对于减法来说,结果是不正确的。这也就是为何计算机内部不使用原码表示一个数。为了解决原码做减法的问题,出现了反码:
1 - 1 = 1 + (-1) = [0000 0001]原 + [1000 0001]原= [0000 0001]反 + [1111 1110]反 = [1111 1111]反 = [1000 0000]原 = -0
发现用反码计算减法,结果的真值部分是正确的。 而唯一的问题其实就出现在"0"这个特殊的数值上。 虽然人们理解上+0和-0是一样的,但是0带符号是没有任何意义的。 而且会有[0000 0000]原和[1000 0000]原两个编码表示0。
于是补码的出现,解决了0的符号以及两个编码的问题:
1-1 = 1 + (-1) = [0000 0001]原 + [1000 0001]原 = [0000 0001]补 + [1111 1111]补 = [0000 0000]补=[0000 0000]原
这样0用[0000 0000]表示,而以前出现问题的-0则不存在了。而且可以用[1000 0000]表示-128:
(-1) + (-127) = [1000 0001]原 + [1111 1111]原 = [1111 1111]补 + [1000 0001]补 = [1000 0000]补
-1-127的结果应该是-128,在用补码运算的结果中,[1000 0000]补 就是-128。 但是注意因为实际上是使用以前的-0的补码来表示-128,所以-128并没有原码和反码表示。(对-128的补码表示[1000 0000]补算出来的原码是[0000 0000]原,这是不正确的)。
使用补码,不仅仅修复了0的符号以及存在两个编码的问题,而且还能够多表示一个最低数。 这就是为什么8位二进制,使用原码或反码表示的范围为[-127,+127],而使用补码表示的范围为[-128,127]。
因为机器使用补码,所以对于编程中常用到的32位int类型,可以表示范围是: [-231,231-1] 因为第一位表示的是符号位。而使用补码表示时又可以多保存一个最小值。
Java的位运算
位运算表达式由操作数和位运算符组成,实现对整数类型的二进制数进行位运算。位运算符可以分为逻辑运算符(包括~、&、|和^)及移位运算符(包括>>、<<和>>>)。
- 左移位运算符(<<)能将运算符左边的运算对象向左移动运算符右侧指定的位数(在低位补0)。
- “有符号”右移位运算符(>>)则将运算符左边的运算对象向右移动运算符右侧指定的位数。 “有符号”右移位运算符使用了“符号扩展”:若值为正,则在高位插入0;若值为负,则在高位插入1。
- Java也添加了一种“无符号”右移位运算符(>>>),它使用了“零扩展”:无论正负,都在高位插入0。这一运算符是C或C++没有的。
-
若对char,byte或者short进行移位处理,那么在移位进行之前,它们会自动转换成一个int,转换时使用“符号扩展规则”。
在进行位运算时,需要注意以下几点。
- >>>和>>的区别是:在执行运算时,>>>运算符的操作数高位补0,而>>运算符的操作数高位移入原来高位的值。
- 右移一位相当于除以2,左移一位(在不溢出的情况下)相当于乘以2;移位运算速度高于乘除运算。
- 若进行位逻辑运算的两个操作数的数据长度不相同,则返回值应该是数据长度较长的数据类型。
- 按位异或可以不使用临时变量完成两个值的交换,也可以使某个整型数的特定位的值翻转。
- 按位与运算可以用来屏蔽特定的位,也可以用来取某个数型数中某些特定的位。
- 按位或运算可以用来对某个整型数的特定位的值置。
位运算符的优先级:~的优先级最高,其次是<<、>>和>>>,再次是&,然后是^,优先级最低的是|。
回顾
回顾上述问题:为什么在bytesToHexString方法中不直接把byte类型的src[i]强制转换成int使用?
因为:byte会转换成int时,对于负数,会做符号扩展,如byte的-1(即0xff),转换成int的-1会扩展成0xffffffff,显然这不是我们所需要的。而把0xffffffff与0xff做与运算就能把高24位清零,这才是我们需要的。
Java的MD5
有了上述的理论知识我们不能写出MD5的加密方法啦
/** * MD5加密 * @param oraginalStr * @return * @throws NoSuchAlgorithmException */ public static String md5(String oraginalStr) throws NoSuchAlgorithmException{ MessageDigest md5=MessageDigest.getInstance("MD5"); md5.update(oraginalStr.getBytes()); return bytesToHexString(md5.digest()).toUpperCase(); }
附录:位操作用途
位与运算的主要用途如下:
- 清零:快速对某一段数据单元的数据清零,即将其全部的二进制位为0。例如整型数a=321对其全部数据清零的操作为a&0x0。
321= 0000 0001 0100 0001 & 0= 0000 0000 0000 0000 = 0000 0000 0000 0000 -
获取一个数据的指定位。例如获得整型数a=321的低八位数据的操作为a&0xFF。
321= 0000 0001 0100 0001 & 0xFF= 0000 0000 1111 11111 = 0000 0000 0100 0001 -
保留数据区的特定位。例如获得整型数a=的第7-8位(从0开始)位的数据:
321= 0000 0001 0100 0001 & 384= 0000 0001 1000 0000 = 0000 0001 0000 0000
位或运算的主要用途:设定一个数据的指定位。例如整型数a=321,将其低八位数据置为1的操作为a=a|0XFF。
321= | 0000 0001 0100 0001 | |
| | 0XFF= | 0000 0000 1111 1111 |
= | 0000 0000 1111 1111 |
位异或运算的主要用途:
- 定位翻转:设定一个数据的指定位,将1换为0,0换为1。例如整型数a=321,,将其低八位数据进行翻位的操作为a^0XFF
321= 0000 0001 0100 0001 ^ 0XFF= 0000 0000 1111 1111 = 0000 0001 1011 1110 -
数值交换:例如a=3,b=4,无须引入第三个变量,利用位运算即可实现数据交换:
int a=3,b=4; System.out.println(a+","+b); a=a^b; b=b^a; a=a^b; System.out.println(a+","+b);
3,4
4,3
左移运算主要用于除2操作,右移运算用于乘2操作,当然他们必须在不溢出的情况下。