您的位置:首页 > 编程语言 > Java开发

Java整型变量溢出的判断方法

2018-02-11 00:00 204 查看
最近在做牛客网上《剑指Offer》的一道算法题:

题目要求:将一个字符串转换成一个整数,要求不能使用字符串转换整数的库函数。 数值为0或者字符串不是一个合法的数值则返回0

输入描述:

输入一个字符串,包括数字字母符号,可以为空

输入描述:

如果是合法的数值表达则返回该数字,否则返回0

实例1:

输入:+2147483647

1a33

输出:2147483647

0

在最初做这道题时,我和其他很多人一样都不同程度犯了错误,下面先看代码。

错误一:没有考虑溢出

public class Solution {
public int StrToInt(String str)
{
if (str == null || str.trim().length() == 0)//空字符串情况
return 0;
char[] a = str.trim().toCharArray();
int i = 0;
char fuhao;//+-符号位
if (a[0] == '-'){
fuhao = '-';
i = 1;//第一位如果是-号,则从第二位开始循环
}else if(a[0] == '+'){
fuhao = '+';//第一位是+正号,则从第二位开始循环
i = 1;
}else if(a[i ]>= 48 && a[i] <= 57){
fuhao = '+';
i = 0;//第一位是0-9的数字,则从第一位开始循环
}else{
return 0;//字符串非法
}
int sum = 0;
for (; i < a.length; i++)
{
if (a[i] < 48 || a[i] > 57)
return 0;//有非数字字符
sum = sum * 10 + a[i] - 48;
}
return fuhao == '+' ? sum : -sum;
}
}

这大概是很多人初次写的代码了吧,乍一看这题十分简单,信手拈来就写出了这样的代码,毫无疑问会因为没有通过可能导致溢出的测试用例而不被AC。

错误二:判断溢出错误1

public class Solution {
public int StrToInt(String str)
{
if (str == null || str.trim().length() == 0)//空字符串情况
return 0;
char[] a = str.trim().toCharArray();
int i = 0;
char fuhao;//+-符号位
if (a[0] == '-'){
fuhao = '-';
i = 1;//第一位如果是-号,则从第二位开始循环
}else if(a[0] == '+'){
fuhao = '+';//第一位是+正号,则从第二位开始循环
i = 1;
}else if(a[i ]>= 48 && a[i] <= 57){
fuhao = '+';
i = 0;//第一位是0-9的数字,则从第一位开始循环
}else{
return 0;//字符串非法
}
int sum = 0;
for (; i < a.length; i++)
{
if (a[i] < 48 || a[i] > 57)
return 0;//有非数字字符
sum = sum * 10 + a[i] - 48;
//判断是否溢出,正整数最大0X7FFFFFFF,最小负整数0X80000000
//可将0x7fffffff用Integer.MAX_VALUE,0x80000000用Integer.MIN_VALUE替代
if((fuhao == '+' && sum > 0X7fffffff) || (fuhao == '--' && -sum < 0X80000000)){
return 0;
}

}
return fuhao == '+' ? sum : -sum;
}
}

首先,一部分人在做这道题时根本没有考虑溢出的问题,另一部分人,则是在判断整型溢出时写出了错误的代码,比如上面的代码中的判断。用sum > 0x7fffffff 和-sum < 0x80000000是判断不了整型溢出的。而这样的错误代码竟然是某《剑指Offer》Java版的示例代码。我们来看一下测试用例的运行结果。

/**
* Created by ray on 2018/2/11.
*/
public class Test {
public static void main(String[] args) {
Solution solution = new Solution();
long value1 = 0x7fffffffL + 1;
long value2 = -0x80000000L - 1;//-不可少,0x80000000在long型中表示正数而不是负数
String str1 = String.valueOf(value1);
String str2 = String.valueOf(value2);;
System.out.println("0x7fffffffL + 1转换结果:" + result1);
System.out.println("-0x80000000L - 1转换结果:" + result2);
}
}

class Solution {
public int StrToInt(String str)
{
if (str == null || str.trim().length() == 0)//空字符串情况
return 0;
char[] a = str.trim().toCharArray();
int i = 0;
char fuhao;//+-符号位
if (a[0] == '-'){
fuhao = '-';
i = 1;//第一位如果是-号,则从第二位开始循环
}else if(a[0] == '+'){
fuhao = '+';//第一位是+正号,则从第二位开始循环
i = 1;
}else if(a[i ]>= 48 && a[i] <= 57){
fuhao = '+';
i = 0;//第一位是0-9的数字,则从第一位开始循环
}else{
return 0;//字符串非法
}
int sum = 0;
for (; i < a.length; i++)
{
if (a[i] < 48 || a[i] > 57)
return 0;//有非数字字符
sum = sum * 10 + a[i] - 48;
//判断是否溢出,正整数最大0X7FFFFFFF,最小负整数0X80000000
//可将0x7fffffff用Integer.MAX_VALUE,0x80000000用Integer.MIN_VALUE替代
if((fuhao == '+' && sum > 0X7fffffff) || (fuhao == '-' && -sum < 0X80000000)){
return 0;
}

}
return fuhao == '+' ? sum : -sum;
}
}

上面是测试代码,然后来看测试结果。



结果并不是显示0以表示转换异常,显然,错误二中的代码没能正确判断是否溢出,转换结果异常。

事实上,由于整型数字(包括32位的int和64位的long)是以2进制的形式在计算机中表示,通常以补码形式表示,而在Java语言中,只存在有符号类型,没有无符号类型。那么对于32位的int来说,最高位如果是1,则表示负数,最高位是0,则表示正数,最大正数0x7fffffff的最高位是0,其余后面31位都是1,因此,sum > 0x7fffffff永远不可能成立,判断结果始终为false,同理,最小负数0x80000000,最高位是1,其他31位是0,转成10进制就是-2^31,sum < 0x80000000永远不成立,判断结果始终为false。因此,在上述错误二的判断溢出代码中,判断结果始终为false,相当于if中的代码块始终没有执行,这种错误的判断逻辑肯定得不到正确的结果。

错误三:溢出判断错误2

/**
* Created by ray on 2018/2/11.
*/
public class Test {
public static void main(String[] args) {
Solution solution = new Solution();
long value1 = 0x7fffffffL + 1;
long value2 = -0x80000000L;//-不可少,0x80000000在long型中表示正数而不是负数
String str1 = String.valueOf(value1);
String str2 = String.valueOf(value2);
int result1 = solution.StrToInt(str1);
int result2 = solution.StrToInt(str2);
System.out.println("0x7fffffffL + 1转换结果:" + result1);
System.out.println("-0x80000000L转换结果:" + result2);
}
}

class Solution {
public int StrToInt(String str) {
if (str == null || str.trim().length() == 0)//空字符串情况
return 0;
char[] a = str.trim().toCharArray();
int i = 0;
char fuhao;//+-符号位
if (a[0] == '-') {
fuhao = '-';
i = 1;//第一位如果是-号,则从第二位开始循环
} else if (a[0] == '+') {
fuhao = '+';//第一位是+正号,则从第二位开始循环
i = 1;
} else if (a[i] >= 48 && a[i] <= 57) {
fuhao = '+';
i = 0;//第一位是0-9的数字,则从第一位开始循环
} else {
return 0;//字符串非法
}
int sum = 0;
for (; i < a.length; i++) {
if (a[i] < 48 || a[i] > 57)
return 0;//有非数字字符
//在对sum计算之前先判断此次计算是否会导致溢出
//实际上就是用边界值Integer.MAX_VALUE对此次计算做一个逆运算来比较
if (sum > (Integer.MAX_VALUE - (a[i] - 48)) / 10){
return 0;
}
sum = sum * 10 + a[i] - 48;
//判断是否溢出,正整数最大0X7FFF FFFF,最小负整数0X8000 0000
//可将0x7fffffff用Integer.MAX_VALUE,0x80000000用Integer.MIN_VALUE替代
//            if((fuhao == '+' && sum > 0X7fffffff) || (fuhao == '-' && -sum < 0X80000000)){
//                return 0;
//            }

}
return fuhao == '+' ? sum : -sum;
}
}

测试结果:



一看,0x7fffffffL+1得到了正确判断,返回了结果0表示非法字符串,但是-0x80000000L明明没有溢出,为什么也返回了0,是不是很奇怪。

其实是因为这种判断逻辑存在一个问题。在错误1,2的字符串转换过程中,均是先计算一个正数sum值,然后根据符号是‘+’或是‘-’返回sum或者sum的相反数。然后,在有符号数中,最大正数与最小负数不是互为相反数的,比如,short型,最大为32767,最小为-32768,int型,最大为2147483647,最小为-2147483648,可以看出,最小负数的相反数比最大整数大1。实际上,对于n位的有符号数来说,表示范围为[-2^n - 1,2^n]。要想转换-0x80000000,上述代码的思路是先计算该值的绝对值,然后根据符号,返回结果。而-0x80000000的绝对值是大于最大整型值的,也就是会导致溢出,所以这种方法也是不正确的。然而,在牛客网该题的测试用例中,并没有包含最小负数这个测试用例,以至于很多人使用这种判断逻辑,也成功AC了。

从上面的代码,我们可以看出,不能通过先求绝对值,再取相反数的方式来转换负数。那我们换一种思路,既然有符号数的最小负数的绝对值大于最大正数的绝对值,那我们可不可以先求负的绝对值,再根据符号转成相应的正确值呢?例如,123,我们可以先求-123,然后判断符号,返回123,这样就可以避免最小负数转换失败的情况了。下面上完整版的代码。

正确解答:

/**
* Created by ray on 2018/2/11.
*/
public class Test {
public static void main(String[] args) {
Solution solution = new Solution();
long value1 = 0x7fffffffL;
long value2 = -0x80000000L;
long value3 = 0x7fffffffL + 1;
long value4 = -0x80000000L - 1;
String str1 = String.valueOf(value1);
String str2 = String.valueOf(value2);
String str3 = String.valueOf(value3);
String str4 = String.valueOf(value4);
int result1 = solution.StrToInt(str1);
int result2 = solution.StrToInt(str2);
int result3 = solution.StrToInt(str3);
int result4 = solution.StrToInt(str4);
System.out.println("0x7fffffffL转换结果:" + result1);
System.out.println("-0x80000000L转换结果:" + result2);
System.out.println("0x7fffffffL + 1转换结果:" + result3);
System.out.println("-0x80000000L - 1转换结果:" + result4);

}
}

class Solution {
public int StrToInt(String str) {
if (str == null || str.trim().length() == 0)//空字符串情况
return 0;
char[] a = str.trim().toCharArray();
int i = 0;
int sum = 0;
int len = a.length;
//limit是sum的边界值
int limit = -Integer.MAX_VALUE;
//multmin是倍乘前的边界值
int multmin;
char fuhao;//+-符号位
if (a[0] == '-') {
if (len == 1) return 0;//单独的'-'非法
fuhao = '-';
limit = Integer.MIN_VALUE;
i = 1;//第一位如果是-号,则从第二位开始循环
} else if (a[0] == '+') {
if (len == 1) return 0;//单独的'+'非法
fuhao = '+';//第一位是+正号,则从第二位开始循环
i = 1;
} else if (a[i] >= 48 && a[i] <= 57) {
fuhao = '+';
i = 0;//第一位是0-9的数字,则从第一位开始循环
} else {
return 0;//字符串非法
}

//因为字符串是10进制,所以除以10,进制为几,就除以几
multmin = limit / 10;

for (; i < a.length; i++) {
if (a[i] < 48 || a[i] > 57)
return 0;//有非数字字符

//*************************
//下面是核心判断代码
int digit = a[i] - 48;
//在倍乘10前先判断
if (sum < multmin)
return 0;
sum *= 10;
//在加之前先判断
//注意!sum < limit +digit不能写成sum - digit < limit
//因为sum - digit就有可能导致溢出了
if (sum < limit + digit)
return 0;
sum -= digit;
//************************
}
//是正数就返回负数的相反数,是负数就直接返回
return fuhao == '+' ? -sum : sum;
}
}

测试结果如下:



注释已经写的很清楚了,如果还没有理解的话,可以画一个数轴来帮助理解。这个代码还对单独的“+”’或者“-”做了判断,可以满足更加严格的测试用例,鲁棒性更好,实际上,这也是JDK中Integer.parseInt()方法所采用的思路。

内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签:  Java 整型 溢出