關注潛在的整數越界問題

2023-11-20 21:01:21

在平時的開發過程中,整數越界是一個容易被忽視的問題,關注潛在的整數越界問題可使我們編寫的程式碼更加健壯,規避因整數越界導致的 bug。

比較器

以下是在 Code Review 中發現的比較器實現:

乍一看該比較器實現不存在問題,但是如果 tag1 = Integer.MIN_VALUE = -2147483648, tag2 為大於 0 的數位如 1,則此時 tag1 - tag2 = 2147483647,但是按照 java.util.Comparator#compare 的定義,tag1 小於 tag2 時,應該返回一個負數,以上寫法在遇到這樣的範例資料時將導致排序結果錯亂,引發相關 bug。

下面看看 Spring 中比較器的實現,在 Spring 中,提供了 @Order 註解用於指定 bean 的順序,預設值為 Ordered.LOWEST_PRECEDENCE = Integer.MAX_VALUE,即在排序時排在最後,相關原始碼如下:

對應的比較器實現如下:

可知其採用的 Integer.compare 方法對兩個整數進行比較操作,檢視 Integer#compare 方法的原始碼:

/**
 * Compares two {@code int} values numerically.
 * The value returned is identical to what would be returned by:
 * <pre>
 *    Integer.valueOf(x).compareTo(Integer.valueOf(y))
 * </pre>
 *
 * @param  x the first {@code int} to compare
 * @param  y the second {@code int} to compare
 * @return the value {@code 0} if {@code x == y};
 *         a value less than {@code 0} if {@code x < y}; and
 *         a value greater than {@code 0} if {@code x > y}
 * @since 1.7
 */
public static int compare(int x, int y) {
    return (x < y) ? -1 : ((x == y) ? 0 : 1);
}

可知 java.lang.Integer#compare 並未採取 x - y 的方式進行比較,而是使用小於等於運運算元直接進行比較,規避了潛在的整數越界問題。 那麼文首程式碼正確的實現方式應為 return Integer.compare(tag1, tag2)。如果檢視 JDK 中常見數值類的原始碼,可知均提供了靜態的 compare 方法,如:java.lang.Long#compare,java.lang.Double#compare,此處不再贅述。

切量比例

以上程式碼是某段業務邏輯中初始切量比例實現,取餘 100 的模式常用於按比例切量、按比例降級等業務場景。以上程式碼使用 userPin 的雜湊值取餘 100 判斷是否小於切量比例以決定是否執行新業務邏輯,如果我們檢視 java.lang.String#hashCode 的原始碼實現:

/**
 * Returns a hash code for this string. The hash code for a
 * {@code String} object is computed as
 * <blockquote><pre>
 * s[0]*31^(n-1) + s[1]*31^(n-2) + ... + s[n-1]
 * </pre></blockquote>
 * using {@code int} arithmetic, where {@code s[i]} is the
 * <i>i</i>th character of the string, {@code n} is the length of
 * the string, and {@code ^} indicates exponentiation.
 * (The hash value of the empty string is zero.)
 *
 * @return  a hash code value for this object.
 */
public int hashCode() {
    int h = hash;
    if (h == 0 && value.length > 0) {
        char val[] = value;

        for (int i = 0; i < value.length; i++) {
            h = 31 * h + val[i];
        }
        hash = h;
    }
    return h;
}

可知 java.lang.String#hashCode 本質上是對字串進行 s[0]*31^(n-1) + s[1]*31^(n-2) + ... + s[n-1] 多項式求值,此處潛在的風險在於計算出的 hash 值可能越界,導致 userPin.hashCode() 返回值為負數,如:"jd_xxxxxxxxxxxx".hashCode() = -1406647067,且在 Java 語言中,使用負數對正數取餘,是可能得到負數的。以上程式碼的風險在於潛在的放大了期望的切量比例,如使用以上的程式碼進行上線,那麼當我們設定 1% 的切量比例時,會導致遠超 1%的使用者執行新的業務邏輯(通過取樣紀錄檔發現使用者 pin 集合 hashCode 值負數佔比並不低),導致非預期的切量結果。

基於以上的背景,容易想到的一種修復方案為在 userPin.hashCode 外層使用 Math.abs 保證取餘前的數位為正數:

以上修復方案看似不再存在問題,但是並不能保證完全正確,我們檢視 Math.abs 的原始碼實現:

/**
 * Returns the absolute value of an {@code int} value.
 * If the argument is not negative, the argument is returned.
 * If the argument is negative, the negation of the argument is returned.
 *
 * <p>Note that if the argument is equal to the value of
 * {@link Integer#MIN_VALUE}, the most negative representable
 * {@code int} value, the result is that same value, which is
 * negative.
 *
 * @param   a   the argument whose absolute value is to be determined
 * @return  the absolute value of the argument.
 */
public static int abs(int a) {
    return (a < 0) ? -a : a;
}

可知在註釋中特意提到,如果入參是 Integer.MIN_VALUE,即 int 域中最小的值時,返回值依然為 Integer.MIN_VALUE,因為 int 域的範圍為 [-2147483648, 2147483647]。如果按照 JLS 中的解釋,-x equals (~x)+1。那麼可知:

x = Integer.MIN_VALUE:
10000000_00000000_00000000_00000000

~x:
01111111_11111111_11111111_11111111

(~x) + 1:
10000000_00000000_00000000_00000000

如果在神燈上搜尋 Math.abs,可以發現有三篇文章與該函數有關,均與 Math.abs(Integer.MIN_VALUE) 依然為 Integer.MIN_VALUE 有關。而我們在 Code Review 階段發現該問題即從根本上規避了該問題,不會使存在 bug 的程式碼上線。最後切量比例修改後的實現如下:

總結

  • java.lang.String#hashCode 在計算過程中可能因為整數越界導致返回值為負數
  • Java 語言中的 % 是取餘而不是取模,如:(-21) % 4 = (-21) - (-21) / 4 *4 = -1
  • Math.abs(int a) 當入參是 Integer.MIN_VALUE 時返回值依然是負數 Integer.MIN_VALUE

參考

15.15.4. Unary Minus Operator -

What's the difference between 「mod」 and 「remainder」? - Stack Overflow

Best way to make Java's modulus behave like it should with negative numbers? - Stack Overflow

OrderComparator.java · spring-projects/spring-framework

作者:京東物流 劉建設 張九龍 田爽

來源:京東雲開發者社群 自猿其說Tech 轉載請註明來源