Java中Integer的整数溢出
1.需求
求两个数的平均数,并且假设现在参与计算的数据非常大,例如其中一个数是Integer类型的最大值.
2.整数溢出
当我们使用Integer的最大值再加1时,我们会发现两者之和变成了一个负值.
这是由于Integer类型数据是由32位组成的,其中第一位是符号位.符号位上的0表示正数,1表示负数.
将Integer的最大值打印到控制台,我们可以看到,它其实是由符号位0和31个1组成的.
int max = Integer.MAX_VALUE;
//2147483647
System.out.println("max = " + max);
//1111111111111111111111111111111
//0111 1111 1111 1111 1111 1111 1111 1111
System.out.println("Integer.toBinaryString(max) = " + Integer.toBinaryString(max));
当我们把1和这个最大值相加时,我们可以发现结果变成了-2147483648,将其转换为二进制数据打印到控制台上,发现其实是由符号位1和31个0组成的.这是因为数据在计算机底层都是以二进制进行存储和运算的.当该最大值加1时,满二进一,最终除了符号位是1,其余位上的数据都是0,结果就变成了一个负数.
如果再对其求平均数,发现结果是-1073741824,装换为二进制,发现是由符号位1,第二个数据是1,其余的都是0.
//-2147483648
System.out.println("(max + 1) = " + (max + 1));
//10000000000000000000000000000000
//1000 0000 0000 0000 0000 0000 0000 0000
System.out.println("Integer.toBinaryString(max + 1) = " + Integer.toBinaryString(max + 1));
//-1073741824
System.out.println("(max + 1) / 2 = " + (max + 1) / 2);
//11000000000000000000000000000000
//1100 0000 0000 0000 0000 0000 0000 0000
System.out.println("Integer.toBinaryString((max + 1) / 2) = " + Integer.toBinaryString((max + 1) / 2));
3.解决方案
3.1方案一:使用无符号右移运算
无符号右移运算(>>>)采取的策略是低位舍弃,高位补0.注意区别于普通右移运算(>>),低位舍弃,高位补符号位.
我们可以看看下面的几个例子:
//eg:
//111
System.out.println("Integer.toBinaryString(7) = " + Integer.toBinaryString(7));
//3
System.out.println("(7 >>> 1) = " + (7 >>> 1));
//11
System.out.println("Integer.toBinaryString(7 >>> 1) = " + Integer.toBinaryString(7 >>> 1));
//11111111111111111111111111110111
//1111 1111 1111 1111 1111 1111 1111 0111
System.out.println("Integer.toBinaryString(-9) = " + Integer.toBinaryString(-9));
//2147483643
System.out.println("(-9 >>> 1) = " + (-9 >>> 1));
//1111111111111111111111111111011
//0111 1111 1111 1111 1111 1111 1111 1011
System.out.println("Integer.toBinaryString(-9 >>> 1) = " + Integer.toBinaryString(-9 >>> 1));
一个负数进行无符号右移运算之后符号位补0,最低位舍弃,其他数据整体右移一位,就相当于原来数据对应的无符号数据除以2.
因此我们可以使用无符号右移来处理上述问题:
//1073741824
System.out.println("((max + 1) >>> 1) = " + ((max + 1) >>> 1));
//1000000000000000000000000000000
//0100 0000 0000 0000 0000 0000 0000 0000
System.out.println("Integer.toBinaryString(((max + 1) >>> 1)) = " + Integer.toBinaryString(((max + 1) >>> 1)));
3.2方案二:更改计算表达式
我们也可以通过修改原来的计算表达式,来避免求平均数时遇到的上述问题.
计算表达式更改思路:
(a + b) / 2
= a / 2 + b / 2
= a / 2 - b / 2 + b
= (a - b) / 2 + b
从上述计算过程我们可以看到,先用大的数的减去小的数再除以2,最后再加上小的数,最后也能得到两个数的平均值.通过这种方式得到的第一部分数据并没有发生符号位的变化,只要最后再加上较小的数就可以得到平均值结果了.
//1073741824
System.out.println("((max - 1) / 2 + 1) = " + ((max - 1) / 2 + 1));
//1000000000000000000000000000000
//0100 0000 0000 0000 0000 0000 0000 0000
System.out.println("Integer.toBinaryString((max - 1) / 2 + 1) = " + Integer.toBinaryString((max - 1) / 2 + 1));
4.应用场景
Arrays工具类中提供的binarySearch0方法就使用解决方案一.在计算中间指针的索引时就是将左指针与右指针之和进行无符号右移1位得到的.
虽然现实中出现这种长度数组的可能性较低,但从逻辑上体现了官方的严谨性,性能上也比普通计算要高.(就算有这种长度的数组,数组溢出的情况也只会在第二轮循环时才有可能出现,因为最大的索引也只是Integer的最大值-1)
下面是具体的源码:
private static int binarySearch0(byte[] a, int fromIndex, int toIndex,
byte key) {
int low = fromIndex;
int high = toIndex - 1;
while (low <= high) {
int mid = (low + high) >>> 1;
byte midVal = a[mid];
if (midVal < key)
low = mid + 1;
else if (midVal > key)
high = mid - 1;
else
return mid; // key found
}
return -(low + 1); // key not found.
}
5.完整代码
import java.util.Arrays;
/**
* 整数溢出
*/
public class Demo {
public static void main(String[] args) {
//需求:求两个数的平均数
//假设现在参与计算的数据比较大
int max = Integer.MAX_VALUE;
//2147483647
System.out.println("max = " + max);
//1111111111111111111111111111111
//0111 1111 1111 1111 1111 1111 1111 1111
System.out.println("Integer.toBinaryString(max) = " + Integer.toBinaryString(max));
//-2147483648
System.out.println("(max + 1) = " + (max + 1));
//10000000000000000000000000000000
//1000 0000 0000 0000 0000 0000 0000 0000
System.out.println("Integer.toBinaryString(max + 1) = " + Integer.toBinaryString(max + 1));
//-1073741824
System.out.println("(max + 1) / 2 = " + (max + 1) / 2);
//11000000000000000000000000000000
//1100 0000 0000 0000 0000 0000 0000 0000
System.out.println("Integer.toBinaryString((max + 1) / 2) = " + Integer.toBinaryString((max + 1) / 2));
//解决方案
//方案一:使用无符号位移运算(低位舍弃,高位补0)
//eg:
//111
System.out.println("Integer.toBinaryString(7) = " + Integer.toBinaryString(7));
//3
System.out.println("(7 >>> 1) = " + (7 >>> 1));
//11
System.out.println("Integer.toBinaryString(7 >>> 1) = " + Integer.toBinaryString(7 >>> 1));
//11111111111111111111111111110111
//1111 1111 1111 1111 1111 1111 1111 0111
System.out.println("Integer.toBinaryString(-9) = " + Integer.toBinaryString(-9));
//2147483643
System.out.println("(-9 >>> 1) = " + (-9 >>> 1));
//1111111111111111111111111111011
//0111 1111 1111 1111 1111 1111 1111 1011
System.out.println("Integer.toBinaryString(-9 >>> 1) = " + Integer.toBinaryString(-9 >>> 1));
//1073741824
System.out.println("((max + 1) >>> 1) = " + ((max + 1) >>> 1));
//1000000000000000000000000000000
//0100 0000 0000 0000 0000 0000 0000 0000
System.out.println("Integer.toBinaryString(((max + 1) >>> 1)) = " + Integer.toBinaryString(((max + 1) >>> 1)));
//方案二:更改计算表达式(先用大的数的减去小的数再除以2,最后再加上小的数)
// (a + b) / 2
// = a / 2 + b / 2
// = a / 2 - b / 2 + b
// = (a - b) / 2 + b
//1073741824
System.out.println("((max - 1) / 2 + 1) = " + ((max - 1) / 2 + 1));
//1000000000000000000000000000000
//0100 0000 0000 0000 0000 0000 0000 0000
System.out.println("Integer.toBinaryString((max - 1) / 2 + 1) = " + Integer.toBinaryString((max - 1) / 2 + 1));
//应用场景:Arrays工具类中提供的binarySearch0方法
int[] arr = {1, 3, 5, 8, 9};
//ctrl+鼠标左键,查看源码,转到binarySearch0方法
//它实质上是使用了位移运算,取代了直接使用((low + high) / 2)避免了数组溢出的问题,同时提高了计算效率
Arrays.binarySearch(arr, 3);
}
}