Integer.toBinaryString()源码探究

在Integer类中有静态方法toBinaryString(int i)方法,此方法返回int变量的二进制表示的字符串。
同理,Integer类中也提供了toHexString(int i)方法和toOctalString(int i)方法来分别返回int变量的16进制表示和8进制便是字符串。
三个方法的源码分别为:

public static String toBinaryString(int i) {
        return toUnsignedString0(i, 1);
    }
public static String toOctalString(int i) {
        return toUnsignedString0(i, 3);
    }
 public static String toHexString(int i) {
        return toUnsignedString0(i, 4);
    }

从上面的三段代码可以看出他们都使用了同一个函数toUnsignedString0(int val,int shift),源代码如下:

 private static String toUnsignedString0(int val, int shift) {
        // assert shift > 0 && shift <=5 : "Illegal shift value";
        int mag = Integer.SIZE - Integer.numberOfLeadingZeros(val);
        int chars = Math.max(((mag + (shift - 1)) / shift), 1);
        if (COMPACT_STRINGS) {
            byte[] buf = new byte[chars];
            formatUnsignedInt(val, shift, buf, 0, chars);
            return new String(buf, LATIN1);
        } else {
            byte[] buf = new byte[chars * 2];
            formatUnsignedIntUTF16(val, shift, buf, 0, chars);
            return new String(buf, UTF16);
        }
    }

代码主要流程是两步:第一步,计算出用于表示二/八/十六进制的字符数组的长度;第二步,使用formatUnsignedInt方法填充字符数组,得到所需进制表示的字符串并返回。
第一步中字符数组长度的计算是通过numberOfLeadingZeros方法计算int变量的计算机二进制表示的高位连续0位的数量,进而获得最高非0位到最低位的长度,也就是需要表示的位数。【比如整数18在计算机中的二进制存储位0000,0000,0000,0000,0000,0000,0001,0010,那么需要表示的部分便是1,0010,前面的28位均为0,不用表示。】
如何获得变量二进制表示中高位连续的0的个数呢?一般能给出的解决方法是通过判断高位是否为0,然后移位重复判断,记录连续的0的个数。如下:

public static int numberOfLeadingZeros0(int i) {
		if (i==0) {
			return 32;
		}
		int n=0;
		int mask=0x80000000;
		int j=i&mask;
		System.out.println(j);
		while(j==0) {
			n++;
			i<<=1;
			j=i&mask;
			System.out.println(j);
		}
		return n;
	}

这个方法的平均时间复杂度为o(k),k为变量i的位数,如果i为int,k就是32.看起来好像可以接受,但这种很基本的库函数会经过很多次的调用,需要进一度的优化。下面是java源码中的写法(简易的二分法):

public static int numberOfLeadingZeros0(int i) {
		if (i==0) {
			return 32;
		}
		int n=1;
		if (i>>>16==0) {
			n+=16;
			i<<=16;
		}
		if (i>>>24==0) {
			n+=8;
			i<<=8;
		}
		if (i>>>28==0) {
			n+=4;
			i<<=4;
		}
		if (i>>>30==0) {
			n+=2;
			i<<=2;
		}
		n-=i>>>31;
			return n;
	}

对源码的看法:

  • 两处代码有几点不同,首先在判断某位是否为0时,后者采用了无符号右移然后与0对比的方式,前者采用了与0x80000000即2的-31次方进行与运算的结果与0对比的方式,后者一次判断多位连续为0的情况,而且更加简单不易出错。
  • 其次,前者从最高为,逐位进行判断;后者则通过划分16,8,4,2等逐步缩小的四个区间进行判断和移位。不管if的代码块是否执行,需要判断的位的区间也在随着代码的推进而逐步缩减。将算法的平均时间复杂度降低到o(log2k)。但这种方法并不是在任何情况都能使用,它需要问题字神具备一定的条件。【换个类似的问题,一个数组,数组中的元素只包含0和1,求最前面连续0的个数。对于这个问题,恐怕不能用上述方式求解,因为,上述方式可以通过i的无符号右移的结果与0比对的方式判断是否符合移动的所有位均为0的条件,但数组却没有这种方式,所以无符号右移的结果变量与0比对的方式是上述算法成立的必要条件。并不是所有类似的问题都有这种类似的方式,可以通过类似的解决方法来解决。】
  • 由于需要进行判断分析的区间长度不断的对半减小,在经过了if(i>>>30==0)之后,区间长度变成了2,但n-=i>>>32语句只讨论了区间为2的第一个位是否为0的情况,而没有考虑第二位的情况,为什么?
    因为,在方法的开始已经判断并返回了变量为0即所有的位均为0的情况,所以能进行到后面的代码部分,说明变量不为0,即必然存在至少一个位为1,而我们在选择需要讨论的位区间时,始终选择的是包含位为1的区间进行讨论。所以,在最后区间长度变为2时,区间中必然至少存在一位是1,而我们讨论的是高位连续的0的个数,如果区间第一位为1,那么第二位自然不同讨论。如果区间第一位是0,那么第二位必然是1,也不需要讨论。所以这里只用讨论第一位的情况。

让我们回到toUnsignedString0函数的源码,我们刚刚解释了如何获取到二进制中前面连续0的个数,而Integer.SIZE是int的位数,也就是32,求得mag为val值除去前面连续的零后所需要的位数,chars为新建字符数组的长度,其中shift的值代表进制,shift=1代表二进制,最后用formatunsignedInt对字符数组进行赋值。
formatUnsignedInt(int val,int shift,char[] buf, int offset, int len)函数

static void formatUnsignedInt(int val, int shift, byte[] buf, int offset, int len) {
        int charPos = offset + len;
        int radix = 1 << shift;
        int mask = radix - 1;
        do {
            buf[--charPos] = (byte)Integer.digits[val & mask];
            val >>>= shift;
        } while (charPos > offset);
    }
//int到char转化的方式,它是通过一个char[]实现从int到char的转化
static final char[] digits = {
        '0' , '1' , '2' , '3' , '4' , '5' ,
        '6' , '7' , '8' , '9' , 'a' , 'b' ,
        'c' , 'd' , 'e' , 'f' , 'g' , 'h' ,
        'i' , 'j' , 'k' , 'l' , 'm' , 'n' ,
        'o' , 'p' , 'q' , 'r' , 's' , 't' ,
        'u' , 'v' , 'w' , 'x' , 'y' , 'z'
    };

通过val&mask和val的右移位进行赋值,最后逐步返回这个二进制字符串数组。【个人认为完全可以通过char c=(char)((val&mask)+48)实现】

猜你喜欢

转载自blog.csdn.net/weixin_43872459/article/details/86560753