Java中byte与16进制字符串的互换原理

我们都知道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不行吗?答案是不行的.

其原因在于:

  1. byte的大小为8bits而int的大小为32bits;
  2. 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的位运算

位运算表达式由操作数和位运算符组成,实现对整数类型的二进制数进行位运算。位运算符可以分为逻辑运算符(包括~、&、|和^)及移位运算符(包括>>、<<和>>>)。

  1. 左移位运算符(<<)能将运算符左边的运算对象向左移动运算符右侧指定的位数(在低位补0)。
  2. “有符号”右移位运算符(>>)则将运算符左边的运算对象向右移动运算符右侧指定的位数。 “有符号”右移位运算符使用了“符号扩展”:若值为正,则在高位插入0;若值为负,则在高位插入1。
  3. Java也添加了一种“无符号”右移位运算符(>>>),它使用了“零扩展”:无论正负,都在高位插入0。这一运算符是C或C++没有的。
  4. 若对char,byte或者short进行移位处理,那么在移位进行之前,它们会自动转换成一个int,转换时使用“符号扩展规则”。

在进行位运算时,需要注意以下几点。   

  1. >>>和>>的区别是:在执行运算时,>>>运算符的操作数高位补0,而>>运算符的操作数高位移入原来高位的值。
  2. 右移一位相当于除以2,左移一位(在不溢出的情况下)相当于乘以2;移位运算速度高于乘除运算。   
  3. 若进行位逻辑运算的两个操作数的数据长度不相同,则返回值应该是数据长度较长的数据类型。   
  4. 按位异或可以不使用临时变量完成两个值的交换,也可以使某个整型数的特定位的值翻转。   
  5. 按位与运算可以用来屏蔽特定的位,也可以用来取某个数型数中某些特定的位。   
  6. 按位或运算可以用来对某个整型数的特定位的值置。

位运算符的优先级:~的优先级最高,其次是<<、>>和>>>,再次是&,然后是^,优先级最低的是|。

回顾

回顾上述问题:为什么在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();
}

附录:位操作用途

位与运算的主要用途如下:

  1. 清零:快速对某一段数据单元的数据清零,即将其全部的二进制位为0。例如整型数a=321对其全部数据清零的操作为a&0x0。
      321= 0000 0001 0100 0001
    & 0= 0000 0000 0000 0000
    =   0000 0000 0000 0000
  2. 获取一个数据的指定位。例如获得整型数a=321的低八位数据的操作为a&0xFF。
      321= 0000 0001 0100 0001
    & 0xFF= 0000 0000 1111 11111
    =   0000 0000 0100 0001

    获得整型数a的高八位数据的操作为a&0xFF00

  3. 保留数据区的特定位。例如获得整型数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. 定位翻转:设定一个数据的指定位,将1换为0,0换为1。例如整型数a=321,,将其低八位数据进行翻位的操作为a^0XFF
      321= 0000 0001 0100 0001
    ^ 0XFF= 0000 0000 1111 1111
    =   0000 0001 1011 1110
  2. 数值交换:例如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操作,当然他们必须在不溢出的情况下。

上一篇:SpringBoot的注解:@SpringBootApplication注解 vs @EnableAutoConfiguration+@ComponentScan+@Configuration


下一篇:自测之Lesson14:多线程编程