• 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操作,当然他们必须在不溢出的情况下。

    出处:http://www.zhaiqianfeng.com    
    本文版权归作者和博客园共有,欢迎转载,但未经作者同意必须保留此段声明,且在文章页面明显位置给出原文连接,否则保留追究法律责任的权利。
  • 相关阅读:
    Go 解析JSON
    查看端口占用并结束进程
    DNS
    u盘禁用
    主机规划与磁盘分区
    响应式设计初识
    SVG基础
    BootStrap入门
    python中 "is"和"=="的区别
    关于C++模板不能分离编译的问题思考
  • 原文地址:https://www.cnblogs.com/zhaiqianfeng/p/4620404.html
Copyright © 2020-2023  润新知