在平時的開發過程中,整數越界是一個容易被忽視的問題,關註潛在的整數越界問題可使我們編寫的代碼更加健壯,規避因整數越界導致的 bug。 ...
在平時的開發過程中,整數越界是一個容易被忽視的問題,關註潛在的整數越界問題可使我們編寫的代碼更加健壯,規避因整數越界導致的 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 轉載請註明來源