Java basic complement value and bit operator

Java's bit operator is a symbol that the pointer operates on each bit of the binary number. He does not care about the actual meaning of the data (whether it is positive or negative, etc.) during the operation, but directly saves the data in memory according to It is calculated in the form of, it is the foundation of the Java foundation, and most developers do not know much about this, so this article introduces some of its basic uses.

The concept of the complement of the original code

Before introducing bit operators formally, let's take a look at the relevant knowledge about complements and complements.

Positive numbers have the same complement of the original code, both of which convert numbers to binary form, and then complement the high-order zeros.

For example, for 8-bit:

  • The corresponding complement of the original code of 10 is 0000 1010
  • The complement of the original code corresponding to 30 is 0001 1110

For negative numbers, the original code of the negative number is the binary corresponding to its absolute value, and the highest bit is 1;

and so:

  • The original code corresponding to -10 is 1000 1010
  • The original code corresponding to -30 is 1001 1110

The inverse of a negative number is the inverse of all other bits except the highest bit of its original code;

and so:

  • The inverted code corresponding to -10 is 1111 0101
  • The inverted code corresponding to -30 is 1110 0001

The complement of the negative number is to add the value of its complement to +1;

and so:

  • The complement of -10 is 1111 0110
  • The complement of -30 is 1110 0010

Do you feel that the three kinds of negative numbers are very messy? It doesn't matter, it makes you feel like this right away ~

For the three codes of the negative number that we just calculated, we put aside the concept of the symbol and consider the three codes as a single stored binary number:

First of all, for the original code, the original code is obtained by writing 1 to the highest bit, and for 8-bit binary, the highest bit 1 is 128, so:

  • The original code corresponding to -10 is 1000 1010, and this binary number is 128 + 10;
  • The original code corresponding to -30 is 1001 1110, and this binary number is 128 + 30;

Then for the inverted code, on the basis of the original code, the highest bit is unchanged, and the remaining bits are inverted, that is to say, the highest bit is discarded, and the sum of the remaining bits before and after the change is 111 1111, so:

  • The inverted code corresponding to -10 is 1111 0101, and its value is 127-10 + 128
  • The inverted code corresponding to -30 is 1110 0001, and its value is 127-30 + 128

As for the two's complement, it is the inverted one plus one, so:

  • The complement of -10 is 1111 0110, and its value is 256-10
  • The complement of -30 is 1110 0010, and its value is 256-30

Why should computers introduce the concept of complement? The main complement is to simplify the calculation. For example, for 8-bit binary numbers, from 0000 0000 to 1111 1111, if viewed as a complement, the number represented is 0, 1,…, 127, -128, -127, …, -2, -1.

For the addition of two positive numbers, the complement of the positive number is itself, so the result can be obtained by adding them directly (note that if the sum exceeds 127, the result will be wrong);

For the addition of two negative numbers, we can still add their complements directly, and get the result ~ For example, for two negative numbers -a and -b, their complements are 256-a and 256-b, respectively. The latter is 512- (a + b), when a + b is less than 128 (a + b> 128 when these two numbers add up beyond the range, and the correct result is not obtained), the computer's representation is 1 xxxx xxxx Because the most significant 1 exceeds the 8-bit range, it is discarded, so the result of the addition is 128- (a + b) and this is the complement of -ab;

For the addition between the positive number a and the negative number -b, we add their complements to get 256 + ab, and this is the complement of ab (regardless of which ab is true).

Since subtraction can invert the subtracted number, we only need to consider the addition operation.

So if you use the complement, you can convert the addition and subtraction operations into two positive numbers (the complement of negative numbers can be regarded as a positive number 256-x). If the computer saves the two's complement, it can directly add the two's complement when doing the addition and subtraction between the numbers to get the correct result (in the case of not exceeding the range).

We can also add directly to get the correct result.

Since subtraction can invert the subtracted number, we only need to consider the addition operation

In fact, consider dividing all rational numbers by 256 modulo equal to divide into 256 groups. When adding, subtracting, multiplying, the result of any two numbers in the same group is the same. The two's complement is in the same group, so when doing four operations, the two's complement can be used in addition, subtraction and multiplication.

How Java numbers are stored in memory

Integer in Java memory form

After learning the two's complement, we realize that if the two's complement is used, it is convenient when doing calculations. The Java shaping is the complement of each number saved, we can also verify on Java:

System.out.println(Integer.toHexString(-1));
System.out.println(Integer.toHexString(-10));
System.out.println(Integer.toHexString(Integer.MAX_VALUE));
System.out.println(Integer.toHexString(Integer.MAX_VALUE));

System.out.println(Long.toHexString(-1));
System.out.println(Long.toHexString(Long.MAX_VALUE));

ffffffff
fffffff6
7fffffff
7fffffff
ffffffffffffffff
7fffffffffffffff

Integer types are saved in Java as follows:

Types of Occupied bytes Binary digits Range of numbers -1 storage form
byte 1 8 -27(-128)~27-1(127) 0xff
short 2 16 -215~215-1 0xffff
int 4 32 -231~231-1 0xffff ffff
long 8 64 -263~263-1 0xffff ffff ffff ffff

The above is the storage of integer in Java.

Floating-point form in Java memory

For decimals, the standard for storing decimals in modern computers is generally the IEEE binary arithmetic standard. Before officially introducing this standard, let ’s recall the scientific notation that we learned before. Scientific notation refers to representing a number as a number within 1-10 The number of times multiplied by 10 to the nth power, for example:

  • 120=1.2*102
  • -10=-1*101
  • 0.023=2.3*10-2

The above is the representation of scientific notation in decimal. From this we can derive the scientific notation in binary:

  • 10 = 1.01 * 10 11 (the numbers on the left are in decimal, the numbers on the right are in binary)
  • -0.125=-1*10-11

What does this representation have to do with how decimals are stored on Java? The IEEE Binary Arithmetic Standard specifies the format for storing decimals with 32-bit precision:

SEEE EEEE EMMM MMMM MMMM MMMM MMMM MMMM

  • Where S stands for symbol, 0 is positive and 1 is negative;
  • 8 E represents the exponent, 8 represents the number represented by the binary number from 0 to 255, the offset 127 must be subtracted (so that the exponent is a negative number), the range of the exponent is -127 to 128;
  • The remaining 23 Ms are the numbers after the decimal point. Since they are all 1s before the decimal point, they do not need to be saved.

Then according to this standard, the expression of 10 is: the symbol is 0, the exponent is 127 + 3, which is 1000 0010, the number of digits is 0100 0000…, together it is: 0 1000 0010 0100 0000…

Similarly -0.125 is 1 0111 1100 00000 ...

We can also use code to verify:

    private static void print(float a) {
        //Float.floatToIntBits是将float类型的小数按照它的保存形式直接转换为int类型,因为他们都是32位的
        StringBuilder s = new StringBuilder(Integer.toBinaryString(Float.floatToIntBits(a)));
        for (int i = 0; i < 32 - s.length(); i++) {
            //按二进制输出的时候,如果高位是0,会被省略,用这个方法来补全到32位
            s.insert(0, "0");
        }
        System.out.println("sign" + s.substring(0, 1));
        System.out.println("Exponent" + s.substring(1, 9));
        System.out.println("Significand" + s.substring(9));

    }
    
    print(10);
    print(-0.125f);
    
    sign0
    Exponent10000010
    Significand01000000000000000000000
    sign1
    Exponent01111100
    Significand00000000000000000000000

For double progress (64-bit specific to Java is double type) floating point number, the rules are similar to the above, divided into 1 sign bit, 11 exponent bits, 52 mantissa bits, the offset of the exponent bit is 1023, for 10 He is 0 1000 0000 010 0100 0000…

The code verification is as follows:

System.out.println(Long.toBinaryString(Double.doubleToLongBits(10)));
100000000100100000000000000000000000000000000000000000000000000//最高位的0被省略了

Regarding the maximum value of float, we must think that the symbol is 0, the maximum value in the exponent field is 1, the offset is 128, and the mantissa is all 1. This number corresponds to the maximum value of int, we can use the following The method prints out:

System.out.println(Float.intBitsToFloat(Integer.MAX_VALUE));
NaN

The result is printed out and found to be NaN, meaningless, what the hell? ? In fact, IEEE has made special provisions for numbers with an exponent field of 0,255:

form index decimal part
zero 0 0
Non-regular form 0 Non-zero
Statute form 1 to 254 Any
endless 255 0
NaN 255 Nonzero

The exponential part of the non-reduced form is all 0, and the normal should be regarded as the exponent part is -127, but the non-reduced form stipulates: the exponent part is regarded as -126, and the corresponding mantissa part is not supplemented by 1, such as the following string: 0000 0000 1000 ... The exponent part is 0, and the decimal part is not 0, so it is parsed according to a non-reduced form, and its value is: -0.5x2 -126

Verify with code as follows:

System.out.println(Float.intBitsToFloat(0b1_0000_0000_10000000000000000000000));
System.out.println(0.5*Math.pow(2,-126));
-5.877472E-39
5.8774717541114375E-39

It can be seen that they are approximately equal in value (the calculation will have a loss of accuracy).

For 32-bit precision numbers, the following table can be drawn:

category Sign Actual index With offset index Exponential domain Mantissa field Numeric value (in parentheses stands for binary system)
zero 0 -127 0 0000 0000 000 0000 0000 0000 0000 0000 0.0
Negative zero 1 -127 0 0000 0000 000 0000 0000 0000 0000 0000 −0.0
1 0 0 127 0111 1111 000 0000 0000 0000 0000 0000 1.0
-1 1 0 127 0111 1111 000 0000 0000 0000 0000 0000 −1.0
Non-reduced number with the smallest absolute value * -126 0 0000 0000 000 0000 0000 0000 0000 0001 ±(0.000…1)× 2-126
Non-reduced number of intermediate sizes * -126 0 0000 0000 100 0000 0000 0000 0000 0000 ±(0.1)x2-126
The largest number of irregularities * -126 0 0000 0000 111 1111 1111 1111 1111 1111 ±(0.11…1) × 2-126
Minimum reduction * -126 1 0000 0001 000 0000 0000 0000 0000 0000 ±(1.000…0)x2-126
Number of reductions with the largest absolute value * 127 254 1111 1110 111 1111 1111 1111 1111 1111 ±(1.111…1) × 2127
Positive infinity 0 128 255 1111 1111 000 0000 0000 0000 0000 0000 +∞
Negative infinity 1 128 255 1111 1111 000 0000 0000 0000 0000 0000 −∞
NaN * 128 255 1111 1111 not zero NaN

It can be seen that the non-reducing number with the largest absolute value is probably the general one with the smallest absolute value, and it can also be seen that the definition of the non-reducing number is mainly to represent a number with a small absolute value.

The precision of float and double

Because the numbers saved by the computer are based on binary, and we are used to representing decimal numbers in the logarithm, there are many numbers that we are used to not being finite decimals in binary, which leads to the use of float or double to represent floating-point There will be a loss of precision. For example, for the decimal number 0.1, it is converted to a binary number of 0.0 0011 0011 0011 ... Obviously, we ca n’t accurately express 0.1 with a binary number of limited bits, and this will cause some strange problem:


System.out.println(1.0 - 0.42);
System.out.println(0.05 + 0.01 == 0.06);
System.out.println((int) (4.015 * 100 + 0.5));

0.5800000000000001//预期输出0.58
false//预期输出true
401//预期输出402

These problems can be circumvented by technical means. When we judge whether floating-point numbers are equal, we generally choose a number that is relatively small for the business based on the data involved in the current business. If the two numbers decrease For this number, we think that the two numbers are equal. When converting double to int, we can choose whether to force or use rounding according to the needs of the business. For example, for the above example, we can use the following method to avoid accuracy problem:

System.out.println(Math.abs(0.05 + 0.01 - 0.06) < 1e-5);
System.out.println(Math.round(4.015 * 100 + 0.5));

true
402

So for int and double, what is the loss of precision? For the reduced number, it means that the part of the number has 23 bits, plus the default 1, the loss precision is not greater than 1/2 23 of the number itself , in fact, because the lowest bit is obtained by rounding, the precision loss is at most 1 / 2 24 , and the double precision number is 1/2 53 .

After taking the logarithm of these two numbers on the basis of ten, it can be concluded that single-precision and double-precision floating-point numbers can guarantee 7 and 15 decimal significant digits.

Bit operator in Java

Java has the following bit operators:

symbol effect example
& Seek and 1 & 1 = 0 1 & 0 = 0 (for a certain calculation)
| OR 1 | 0 = 1 0 | 0 = 0 (for a certain calculation)
^ XOR 1 ^ 1 = 0 0 ^ 1 = 1 (for a certain calculation)
~ Find ~ 1 = 0 ~ 0 = 1 (for a certain calculation)
<< Shift left 1 << 1 = 2 (for the calculation of int number)
>> Right shift 4 >> 1 = 2 (for the calculation of int number)
>>> Unsigned right 4 >>> 1 = 2 (for the calculation of int number)

Refer to the following example for the specific meaning of left shift right shift unsigned right shift:

        System.out.println(Integer.toHexString(0x7fffffff >> 4));
        System.out.println(Integer.toHexString(0x8fffffff >> 4));
        System.out.println(Integer.toHexString(0xffffffff >> 4));
        System.out.println(Integer.toHexString(0x0fffffff >> 4));
        System.out.println();
        System.out.println(Integer.toHexString(0x7fffffff << 4));
        System.out.println(Integer.toHexString(0x8fffffff << 4));
        System.out.println(Integer.toHexString(0xffffffff << 4));
        System.out.println(Integer.toHexString(0x0fffffff << 4));
        System.out.println();
        System.out.println(Integer.toHexString(0x7fffffff >>> 4));
        System.out.println(Integer.toHexString(0x8fffffff >>> 4));
        System.out.println(Integer.toHexString(0xffffffff >>> 4));
        System.out.println(Integer.toHexString(0x0fffffff >>> 4));
        
        7ffffff
        f8ffffff
        ffffffff
        ffffff
        
        fffffff0
        fffffff0
        fffffff0
        fffffff0
        
        7ffffff
        8ffffff
        fffffff
        ffffff

总结来说:

  • 右移:整体向右移,原来的最高位是0,就补0,原来的最高位是1,就补1;

  • 左移:整体左移,在最低位补0;

  • 无符号右移:最高位补0。

位运算符的使用又如下几点需要注意:

  • 位运算符都是在整型保存在内存地址上的每一位进行运算的,所以有些会看起来超出我们的常识;
  • 左移右移并不能简单的看作是在数的基础上乘以2或者除以2,而要结合它的内存形式来具体分析。

例如:

System.out.println(~1);
结果是-2而不是0

这是因为1在内存上是000....001,~1就是111...1110,而这是-2的补码,所以结果是-1;

System.out.println(-1 >> 1);
结果还是-1而不是0

这是因为-1在内存上是111...1,它的符号位是1,所以右移的时候高位补1,结果不变;

System.out.println(Integer.MAX_VALUE << 1);
结果是-2,正数左移移成了负数。。。

System.out.println(0xBFFFFFFF);
System.out.println(0xBFFFFFFF << 1);
-1073741825
2147483646
负数左移成了正数。。。

System.out.println(-1 >>> 1);
2147483647
-1左移成了int的最大值。。。

Java位运算符的使用

我们来看一项位运算符的具体使用:处理颜色值。

在Android中,颜色值是用int类型的数来表示的,例如0xffffffff表示白色,其中前两位代表透明度,后六位分别代表红绿蓝三色的值,注意0xffffff表示的颜色是透明的,因为它没有写明透明度,而这个int值对应的最高两位是00,所以它代表的颜色是透明的。

现在我们来考虑一个场景:取一个颜色ARGB的值,这在颜色渐变的动画中要使用到(因为如果想要颜色平滑的渐变,必须要在ARGB上分别渐变,如果按照int值的简单渐变,会导致颜色的闪动),比如对于颜色0xff123456,我们想分别取到ff,12,34,56,那么会很容易写出如下代码:

//取B  56
System.out.println(Integer.toHexString(0xff123456 % 0xff));
//取G  34
System.out.println(Integer.toHexString(0xff123456 / 0xff % 0xff));
//取R  12
System.out.println(Integer.toHexString(0xff123456 / 0xffff % 0xff));
//取A  ff
System.out.println(Integer.toHexString(0xff123456 / 0xffffff));

ffffff9c
ffffff57
ffffff13
0

结果完全和我们想的不一样,实际上,颜色0xff123456表示的int数是一个负数因为它的最高位是1,他已经超过了int的最大正数的范畴,我们要正确的得到ARGB的方法是使用位运算符:

//取B  56
System.out.println(Integer.toHexString((0xff123456 & 0xff) >>> 0));
//取G  34
System.out.println(Integer.toHexString((0xff123456 & 0xff00) >>> 8));
//取R  12
System.out.println(Integer.toHexString((0xff123456 & 0xff0000) >>> 16));
//取A  ff
System.out.println(Integer.toHexString((0xff123456 & 0xff000000) >>> 24));

56
34
12
ff

先通过与运算符拿到ARGB对应的值,然后在通过无符号右移将值移到最低位,之后可以分别改变ARGB的值,然后在合并成颜色,读者可自行尝试。

最后,我们来尝试使用位运算符来实现加减乘除运算。

我们先来实现求相反数。由于正数的补码和他相反数的补码的关系我们已经很清楚,那么我们只需要对正数和负数分别处理就好了:

    public static int getOppositeNumber(int number) {
		if ((number & 0x80000000) == 0x80000000) {
			// number是负数
			// 先把这个数看作一个无符号的数,
			//执行-1,方法是从最低位算起,
			//如果遇到0,就将之变成1
			//如果遇到1,就将之变成0,并跳出循环
			for (int i = 0; i <= 31; i++) {
//				System.out.println(number);
				int flag = 1 << i;
				if ((number & flag) == flag) {
					// 当前位是1,就换成0
					number = number ^ flag;
					break;
				} else {
					number = number ^ flag;
                }
			}
//			System.out.println(number);
			// 执行全部位的取反
			return number ^ 0xffffffff;

		} else {
			// number是正数
			// 执行取反
			number=number ^ 0xffffffff;
//			System.out.println(number);
			// 执行+1,方法和执行-1差不多
			for (int i = 0; i <= 31; i++) {
//				System.out.println(number);
				int flag = 1 << i;
				if ((number & flag) != flag) {
					// 当前位是0,就换成1
					number = number ^ flag;
					break;
				} else {
					number = number ^ flag;
				}
			}
			return number;
		}

	}
	
	public static void main(String[] args) {
        System.out.println(getOppositeNumber(-10));
		
		System.out.println(getOppositeNumber(-12423));
		
		System.out.println(Integer.toHexString(getOppositeNumber(Integer.MIN_VALUE)));
		
		System.out.println(getOppositeNumber(10));
		
		System.out.println(getOppositeNumber(2346760));
		
		System.out.println(Integer.toHexString(getOppositeNumber(Integer.MAX_VALUE)));
		
		System.out.println(getOppositeNumber(0));

	}

10
12423
80000000
-10
-2346760
80000001
0

其中int的最小值的相反数算出来是它本身,因为严格来说,int的最小值的相反数已经超出了int的范围。

有了求相反数之后,我们就只用考虑加法运算了。加法运算实现如下:

	public static int add(int a, int b) {
		int sum = 0;
		int carry = 0;
		for (int i = 0; i < 32; i++) {
			int flag = 1 << i;

			int currentIndex = ((a ^ b)) & flag;
			int currentCarry = ((a & b) & flag) << 1;
			if (carry == 0) {
				sum = sum | currentIndex;
				carry = currentCarry;
			} else {
				if (currentIndex == 0) {
					sum = sum | flag;
					carry = currentCarry;
				}  else {
					carry = 1;
				}
			}
//			System.out.println("currentIndex "+currentIndex+" currentCarry "+currentCarry+" carry "+carry+" sum "+sum);
		}

		return sum;
	}
	

System.out.println(add(12, 342));
System.out.println(add(-12, 342));
System.out.println(add(-12, -342));
System.out.println(add(Integer.MAX_VALUE, 1));
System.out.println(add(Integer.MIN_VALUE, -1));
System.out.println(add(Integer.MIN_VALUE, 1));

354
330
-354
-2147483648
2147483647
-2147483647

因为补码的性质,我们可以将int当作无符号的数来做相加,结果是正确的。代码中我们从低位开始相加,并判断有无进位,一直计算到最高位。

其中int的最大值加1之后,超过了int的上限,得到的数是和那个实际值关于232同模的值,也就是int的最小值-231

然后是乘法,乘法我们依旧可以把int当作无符号的数来计算,计算乘法的方法是,对其中一个数进行循环,然后当遇到1的时候,将另一个数王左移,然后就所有左移的结果加起来就是相乘的结果。由于我们已经实现了加法,所以这儿可以直接使用:

    public static int multiply(int a, int b) {
        int sum = 0;
        for (int i = 0; i < 32; i++) {
            int flag = 1 << i;

            if ((a & flag) == flag) {
                sum += b << i;
            }
//			System.out.println("sum " + sum);
        }
        return sum;

    }

System.out.println(multiply(11, 12));
System.out.println(multiply(-7, -26));
System.out.println(multiply(-5, 9));
System.out.println(multiply(0x10000, 0x10000));

132
182
-45
0

其中0x10000的平方是232次方,它超过了int的范围,,所以结果是int范围内,和232关于232同模的0。

最后关于除法,由于我们已经能够计算相反数,所以我们可以只考虑正数之间的除法,计算除法的方法是,先算出两个数的最高位,然后对被除数王左移到与除数相同的位数,比较大小,如果除数大则在上上加上相应的数,一直循环到被除数不往右移的情况,然后得到最终的商,具体代码如下:

public static int divide(int a, int b) {
        if (b > a) {
            return 0;
        }
        //获取a的最高位
        int maxA = 0;
        for (int i = 0; i < 31; i++) {
            int flag = 1 << 31 - i;
            if ((a & flag) == flag) {
                maxA = 31 - i;
                break;
            }
        }

        //获取b的最高位
        int maxB = 0;
        for (int i = 0; i < 31; i++) {
            int flag = 1 << 31 - i;
            if ((b & flag) == flag) {
                maxB = 31 - i;
                break;
            }
        }
//        System.out.println("maxA " + maxA + " maxB " + maxB);
        int index = maxA - maxB;
        int sum = 0;
        for (int i = 0; i <= index; i++) {
            if (a > b << (index - i)) {
                sum += 1 << (index - i);
                a -= b << (index - i);
//                System.out.println("sum" + sum);
            }
        }
        return sum;
    }
    
    
System.out.println(divide(100, 3));
System.out.println(divide(32424, 200));
System.out.println(divide(239, 13));

33
162
18

以上代码使用了大小比较,而大小比较可以通过把两个数相减判断结果的正负方式很简单的实现,所以不在赘述。

总结

综上所述,我们讨论了反码补码以及Java数值在计算机的存储,以及Java位运算的操作,并结合知识这些实现了颜色的获取以及位运算实现加减乘除,相信读者对Java的数值以及位运算符有了更深的认识,以后遇到相关问题的时候也能够很好的解决。

发布了19 篇原创文章 · 获赞 8 · 访问量 4041

Guess you like

Origin blog.csdn.net/u014068277/article/details/102654292