【Java】整數反轉引發的一個問題

問題引入

整數反轉

給出一個 32 位的有符號整數,你需要將這個整數中每位上的數字進行反轉。
示例 1:
輸入: 123
輸出: 321

示例 2:
輸入: -123
輸出: -321

示例 3:
輸入: 120
輸出: 21

注意:假設我們的環境只能存儲得下 32 位的有符號整數,則其數值範圍爲 [−231, 231 − 1]。請根據這個假設,如果反轉後整數溢出那麼就返回 0。

踩坑之路

Too Young Too Simple

思路:使用一個StringBuffer從數字的尾部開始倒着追加數字字符。最後用Integer.parseInt()函數將String轉換成整數,同時還可以去掉0。代碼如下:

import java.util.Scanner;

public class ReverseNum {
    public static void main(String[] args) {
        Scanner sc = new Scanner(System.in);
        int num = sc.nextInt();
        System.out.println(reverse(num));
    }

    private static int reverse(int x) {
        String numStr = String.valueOf(x);
        StringBuffer stringBuffer = new StringBuffer();
        String res = "";
        if (numStr.charAt(0) == '-') {
            for (int i = numStr.length() - 1; i >= 1; i--) {
                stringBuffer.append(numStr.charAt(i));
            }
            res = "-" + stringBuffer.toString();
        } else {
            for (int i = numStr.length() - 1; i >= 0; i--) {
                stringBuffer.append(numStr.charAt(i));
            }
            res = stringBuffer.toString();
        }
        return Integer.parseInt(res);
    }
}

然而:當輸入1234560789,出現瞭如下異常

Exception in thread "main" java.lang.NumberFormatException: For input string: "9870654321"
	at java.base/java.lang.NumberFormatException.forInputString(NumberFormatException.java:68)
	at java.base/java.lang.Integer.parseInt(Integer.java:652)
	at java.base/java.lang.Integer.parseInt(Integer.java:770)
	at com.me.ReverseNum.reverse(ReverseNum.java:27)
	at com.me.ReverseNum.main(ReverseNum.java:9)

填坑永遠在路上

根據異常,我們定位到Integer.parseInt()函數上,打開該函數的源碼,如下:

public static int parseInt(String s) throws NumberFormatException {
        return parseInt(s,10);
    }

繼續往下看:

    /**
     * Parses the string argument as a signed integer in the radix
     * specified by the second argument. The characters in the string
     * must all be digits of the specified radix (as determined by
     * whether {@link java.lang.Character#digit(char, int)} returns a
     * nonnegative value), except that the first character may be an
     * ASCII minus sign {@code '-'} ({@code '\u005Cu002D'}) to
     * indicate a negative value or an ASCII plus sign {@code '+'}
     * ({@code '\u005Cu002B'}) to indicate a positive value. The
     * resulting integer value is returned.
     *
     * <p>An exception of type {@code NumberFormatException} is
     * thrown if any of the following situations occurs:
     * <ul>
     * <li>The first argument is {@code null} or is a string of
     * length zero.
     *
     * <li>The radix is either smaller than
     * {@link java.lang.Character#MIN_RADIX} or
     * larger than {@link java.lang.Character#MAX_RADIX}.
     *
     * <li>Any character of the string is not a digit of the specified
     * radix, except that the first character may be a minus sign
     * {@code '-'} ({@code '\u005Cu002D'}) or plus sign
     * {@code '+'} ({@code '\u005Cu002B'}) provided that the
     * string is longer than length 1.
     *
     * <li>The value represented by the string is not a value of type
     * {@code int}.
     * </ul>
     *
     * <p>Examples:
     * <blockquote><pre>
     * parseInt("0", 10) returns 0
     * parseInt("473", 10) returns 473
     * parseInt("+42", 10) returns 42
     * parseInt("-0", 10) returns 0
     * parseInt("-FF", 16) returns -255
     * parseInt("1100110", 2) returns 102
     * parseInt("2147483647", 10) returns 2147483647
     * parseInt("-2147483648", 10) returns -2147483648
     * parseInt("2147483648", 10) throws a NumberFormatException
     * parseInt("99", 8) throws a NumberFormatException
     * parseInt("Kona", 10) throws a NumberFormatException
     * parseInt("Kona", 27) returns 411787
     * </pre></blockquote>
     *
     * @param      s   the {@code String} containing the integer
     *                  representation to be parsed
     * @param      radix   the radix to be used while parsing {@code s}.
     * @return     the integer represented by the string argument in the
     *             specified radix.
     * @throws     NumberFormatException if the {@code String}
     *             does not contain a parsable {@code int}.
     */
    public static int parseInt(String s, int radix)
                throws NumberFormatException
    {
        /*
         * WARNING: This method may be invoked early during VM initialization
         * before IntegerCache is initialized. Care must be taken to not use
         * the valueOf method.
         */

        if (s == null) { // 如果接受的字符串爲空, 就報空字符串的異常
            throw new NumberFormatException("null");
        }

        if (radix < Character.MIN_RADIX) { // 判斷基數是不是符合要求
            throw new NumberFormatException("radix " + radix +
                                            " less than Character.MIN_RADIX");
        }

        if (radix > Character.MAX_RADIX) { // 判斷基數是不是符合要求
            throw new NumberFormatException("radix " + radix +
                                            " greater than Character.MAX_RADIX");
        }

        boolean negative = false; // 判斷符號
        int i = 0, len = s.length(); // 設置初始位置和字符串的長度
        int limit = -Integer.MAX_VALUE;

        if (len > 0) { // 字符串的長度必須大於零
            char firstChar = s.charAt(0); // 獲得字符串的第一個字符
            if (firstChar < '0') { // Possible leading "+" or "-"
                if (firstChar == '-') {
                    negative = true;
                    limit = Integer.MIN_VALUE;
                } else if (firstChar != '+') { // 如果不爲+的話就報錯
                    throw NumberFormatException.forInputString(s, radix);
                }
                // 字符串的長度爲1但是又不是數字, 那肯定就出錯了
                if (len == 1) { // Cannot have lone "+" or "-"
                    throw NumberFormatException.forInputString(s, radix);
                }
                i++;
            }
            int multmin = limit / radix;
            int result = 0;
            /*
            * 下面的過程其實很好理解, 以8進制的"534"爲例
            * * (-5*8-3)*8-4 = -348, 根據符號位判斷返回的是348
            */
            while (i < len) {
                // Accumulating negatively avoids surprises near MAX_VALUE
                // 除了前面的判斷這裏的也有點複雜, 因爲要考慮到各種進位
                // 這個將i位置上的字符根據基數轉爲實際的值, A->11
                int digit = Character.digit(s.charAt(i++), radix);
                if (digit < 0 || result < multmin) {
                    throw NumberFormatException.forInputString(s, radix);
                }
                result *= radix;
                if (result < limit + digit) {
                    throw NumberFormatException.forInputString(s, radix);
                }
                result -= digit;
            }
            return negative ? result : -result; // 根據符號位來判斷返回哪一個
        } else {
            throw NumberFormatException.forInputString(s, radix);
        }
    }

函數說明

Integer.parseInt(String s):方法用於將字符串參數作爲有符號的十進制整數進行解析。
如果方法有兩個參數, 使用第二個參數指定的基數,將字符串參數解析爲有符號的整數。

  • parseInt(String s): 返回用十進制參數表示的整數值。
  • parseInt(String s, int radix):使用指定基數的字符串參數表示的整數 (基數可以是 10, 2, 8, 或 16 等進制數)
其他相關函數

**Integer.valueof(String s):**把字符串s解析成Integer對象類型,返回的integer 可以調用對象中的方法。
**StringUtils.defaultIfBlank(T str, T defaultStr):**如果字符串爲空白符就返回默認字符串defaultStr。可以以string的類型比較數值大小

Integer.parseInt(s)與Integer.valueOf(s)的區別:
Integer.parseInt(s) 多次解析同一個字符串得到的int基本類型數據是相等的,可以直接通過“==" 進行判斷是值是否相等。基本類型不含equals方法。

 Integer.parseInt(s) == Integer.parseInt(s)

Integer.valueOf(s) 多次解析相同的一個字符串時,得到的是Integer類型的對象,得到的對象有時是同一個對象,有時是不同的對象,要根據把s字符串解析的整數值的大小進行決定

  • 如果s字符串對應的整數值在 -128~127之間,則解析出的Integer類型的對象是同一個對象;
  • 如果s字符串對應的整數值不在-128~127之間,則解析出的Integer類型的對象不是同一個對象。
    不管對象是否相等,對象中的value值是相等的。
 Integer.parseInt(s) == Integer.parseInt(s)
 Integer.parseInt(s).equals(Integer.parseInt(s))

equals是比較的兩個對象value值是否相等;
“==”是比較兩個對象是否相等。

JDK源碼中,由於在-128~127之間的整數值用的比較頻繁,當每次要創建一個value值在-128~127之間的Integer對象時,直接從緩存中拿到這個對象,所以value值相同的Integer對象都是對應緩存中同一個對象。-128~127之外的整數值用的不是太頻繁,每次創建value值相同的Integer對象時,都是重新創建一個對象,所以創建的對象不是同一個對象。

常見異常

看完了源碼,大概知道了什麼情況下會出現異常:

  1. 如下4種情況會拋 NumberFormatException 異常
  • 第一個字符串參數s爲null、或長度爲0的字符串
  • 第二個基數參數radix小於MIN_RADIX=2、或大於MAX_RADIX=36
  • 字符串s的任何數字都不是指定基數的字符(數字/字母),除如下2種情況:
    ①第一個字符可以爲減號-
    ②加號+且長度大於1第一個字符可以爲減號-、或加號+且長度大於1
  • 字符串s的值不是int型
  1. 常見的 NumberFormatException 異常
  • s爲空串、空,如:parseInt("")
  • s中包括空格,如:parseInt("23 ")
  • 10進制時,s中包括字符串,如:parseInt(“a32”)
  • s不以-、+、數字開頭、或包含字符串
  • s超出int允許的範圍[-2147483648,2147483647],對應到2進制數字:"-10000000000000000000000000000000" (32位數字)~ “1111111111111111111111111111111”(31位數字)
  • 其他轉換基數radix時,超出表示範圍

恍然大悟

看到這裏,我們就知道了爲什麼輸入1234560789就會出現NumberFormatException 異常,因爲這個數值已經超出了int所允許的範圍,因此,會拋出異常。
那麼問題來了,如何解決?

正解

採用計算的方式,不再使用字符追加的方式。以12345爲例,先拿到5,再拿到4,之後是3,2,1,我們按這樣的順序就可以反向拼接處一個數字了,也就能達到反轉的效果。怎麼拿末尾數字呢?用取模運算就可以。
取模反向
過程說明:
1、將12345 % 10 得到5,之後將12345 / 10
2、將1234 % 10 得到4,再將1234 / 10
3、將123 % 10 得到3,再將123 / 10
4、將12 % 10 得到2,再將12 / 10
5、將1 % 10 得到1,再將1 / 10

這麼看起來,一個循環就搞定了,循環的判斷條件是x>0
但這樣做忽略了負數
循環的判斷條件應該是while(x!=0),無論正數還是負數,按照上面不斷的/10這樣的操作,最後都會變成0,所以判斷終止條件就是!=0
有了取模和除法操作,對於像12300這樣的數字,也可以完美的解決掉了。

看起來這道題就這麼解決了,但請注意,題目上還有這麼一句

假設我們的環境只能存儲得下 32 位的有符號整數,則其數值範圍爲 [2^31,  2^311]

也就是說我們不能用long存儲最終結果,而且有些數字可能是合法範圍內的數字,但是反轉過來就超過範圍了。假設有1147483649這個數字,它是小於最大的32位整數2147483647的,但是將這個數字反轉過來後就變成了9463847411,這就比最大的32位整數還要大了,這樣的數字是沒法存到int裏面的,所以肯定要返回0(溢出了)。
甚至,我們還需要提前判斷
正數範圍判斷
上圖中,綠色的是最大32位整數
第二排數字中,橘子的是5,它是大於上面同位置的4,這就意味着5後跟任何數字,都會比最大32位整數都大
所以,我們到【最大數的1/10】時,就要開始判斷了
如果某個數字大於 214748364那後面就不用再判斷了,肯定溢出了。
如果某個數字等於 214748364呢,這對應到上圖中第三、第四、第五排的數字,需要要跟最大數的末尾數字比較,如果這個數字比7還大,說明溢出了。

對於負數也是一樣的
負數範圍判斷
上圖中綠色部分是最小的32位整數,同樣是在【最小數的 1/10】時開始判斷
如果某個數字小於 -214748364說明溢出了
如果某個數字等於 -214748364,還需要跟最小數的末尾比較,即看它是否小於8

代碼:

public static int reverse(int x) {
        int res = 0;
        while (x != 0) {
            // 每次取末尾數字
            int tmp = x % 10;
            // 判斷是否大於最大32位整數
            if (res > 214748364 || (res == 214748364 && tmp > 7)) {
                return 0;
            }
            // 判斷是否小於最小32位整數
            if (res < -214748364 || (res == -214748364 && tmp < -8)) {
                return 0;
            }
            res = res * 10 + tmp;
            x = x / 10;
        }
        return res;
    }

完整代碼

package com.me;

import java.util.Scanner;

public class ReverseNum {
    public static void main(String[] args) {
        Scanner sc = new Scanner(System.in);
        int num = sc.nextInt();
//        System.out.println(reverse0(num));
        System.out.println(reverse1(num));
        System.out.println(reverse2(num));
        System.out.println(reverse3(num));
    }

    public static int reverse0(int x) {
        String numStr = String.valueOf(x);
        StringBuffer stringBuffer = new StringBuffer();
        String res = "";
        if (numStr.charAt(0) == '-') {
            for (int i = numStr.length() - 1; i >= 1; i--) {
                stringBuffer.append(numStr.charAt(i));
            }
            res = "-" + stringBuffer.toString();
        } else {
            for (int i = numStr.length() - 1; i >= 0; i--) {
                stringBuffer.append(numStr.charAt(i));
            }
            res = stringBuffer.toString();
        }
        return Integer.parseInt(res);
    }

    public static int reverse1(int x) {
        int res = 0;
        while (x != 0) {
            // 每次取末尾數字
            int tmp = x % 10;
            // 判斷是否大於最大32位整數
            if (res > 214748364 || (res == 214748364 && tmp > 7)) {
                return 0;
            }
            // 判斷是否小於最小32位整數
            if (res < -214748364 || (res == -214748364 && tmp < -8)) {
                return 0;
            }
            res = res * 10 + tmp;
            x = x / 10;
        }
        return res;
    }

    public static int reverse2(int x) {
        int res = 0;
        while (x != 0) {
            int t = x % 10;
            int newRes = res * 10 + t;
            //如果數字溢出,直接返回0
            if ((newRes - t) / 10 != res)
                return 0;
            res = newRes;
            x = x / 10;
        }
        return res;
    }

    public static int reverse3(int x) {
        long res = 0;
        while (x != 0) {
            res = res * 10 + x % 10;
            x /= 10;
        }
        return (int) res == res ? (int) res : 0;
    }
}

參考

  1. https://leetcode-cn.com/problems/reverse-integer/solution/tu-wen-xiang-jie-javadai-ma-de-2chong-shi-xian-fan/
  2. https://leetcode-cn.com/problems/reverse-integer/solution/tu-jie-7-zheng-shu-fan-zhuan-by-wang_ni_ma/
  3. https://blog.csdn.net/bingleihenshang/article/details/84838150
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章