導致我去看Integer源碼的原因是項目中的一個問題,業務邏輯:項目中有一個扣除優惠券的操作,爲了使用戶優惠券使用正確,在扣除優惠券之前,會先比較一下優惠券的使用數量(總量-餘量)和優惠券的使用明細表中的數量是否一致,如果一致則扣除優惠券,否則扣除優惠券失敗(使用異常了)。
最後出現了一個問題:用戶操作一定時間後發現,扣除失敗,前面都是成功的。
項目中大概的邏輯是下面這樣的:
// 這裏判斷優惠券使用情況
// 1、先從數據庫中查出來優惠券使用明細中的數量
Integer useCouponCount = couponDao.getUseCouponCount(memberId, couponId);
// 2、從數據庫中查出來優惠券的詳情(其中包含優惠券的總量和剩餘數量)
CounponInfo couponInfo = couponDao.getCouponById(couponId);
// 3、計算正常優惠券的使用數量
Integer payCount = couponInfo.getPayCount();// 總量
Integer residueCount = couponInfo.getResidueCount();// 剩餘數量
Integer useCount = payCount - residueCount;
// 4、比較使用明細和使用數量是否相等
if (useCouponCount != useCount) {
// 這裏說明優惠券使用情況異常,拋出異常
} else {
// 這裏優惠券使用正常,進行相應的業務邏輯操作
}
1、首先查看用戶優惠券使用情況並沒有發生異常情況,數量是對着呢,所以轉而考慮應該是代碼的問題
2、根據日誌的跟蹤發現,最終發現問題出現在上面的第4步中,但是大致看了下沒什麼問題啊,爲什麼會出現使用異常的情況呢?
useCouponCount 和 useCount 爲什麼不相等?
最後發現這兩個對象是Integer對象,Java中對象的== 和 !=操作是比較的對象的地址,所以導致了兩個對象不相等,才造成了上面bug的出現,問題發現之後,再一想爲什麼前面的使用沒有出現異常呢,所以開啓研究這個問題。
1、先做了幾個簡單的驗證:
// 驗證1
Integer a = new Integer(10);
Integer b = new Integer(10);
System.out.println(a == b);// 驗證結果:false(在意料之中,兩個對象兩個地址肯定不相同)
2、因爲Integer對應的有基本類型int,所以又做了如下驗證:
Integer a = 10;
Integer b = 10;
System.out.println(a == b);// 結果:true(哇,why?)
感覺發現了新大陸,有木有?
3、接着做驗證:
Integer a = 127;
Integer b = 127;
System.out.println(a == b);// 結果爲:true
Integer a = 128;
Integer b = 128;
System.out.println(a == b);// 結果爲:false
終於發現問題了,大致猜想了下估計是因爲Java的拆裝箱導致的問題。Java的裝箱用的是Integer.valueOf(int)方法,拆箱用的是intValue()方法,所以我們看下裝箱幫我們做了什麼操作。
// valueOf中比較了參數i是否在Integer維護的一個緩存數組IntegerCache的範圍內
// 如果再IntegerCache.low 和 IntegerCache.high之間就返回緩存中的對象
// 不在緩存範圍內的話,才返回了一個新的Integer對象(new Integer(i))
public static Integer valueOf(int i) {
if (i >= IntegerCache.low && i <= IntegerCache.high)
return IntegerCache.cache[i + (-IntegerCache.low)];
return new Integer(i);
}
我們可以看到裝箱操作對應的IntegerCache(也就是-128~127) 幫我們new了256Integer對象,所以纔會導致了我們的優惠券使用127之內是沒有問題的,超過127就不行了
至於如何查看Integer是如何拆裝箱的,其實是編譯器幫我們做了些什麼事,我們使用javap可以將class文件翻譯成彙編內容查看一下class文件中執行的指令信息。附上一個我看Integer拆裝箱的class
java源碼:
public class Test{
public static void main(String[] args) {
Integer a = 10;
int b = a;
}
}
1、利用javac Test.java編譯成class文件
2、利用javap -v -l -c -s -sysinfo -constants Test輸出class對應的彙編格式
C:\Users\Administrator\Desktop>javap -v -l -c -s -sysinfo -constants Test
Classfile /C:/Users/Administrator/Desktop/Test.class
Last modified 2019-6-5; size 367 bytes
MD5 checksum 40139609a08238441e8ca2e01940f968
Compiled from "Test.java"
public class Test
minor version: 0
major version: 52
flags: ACC_PUBLIC, ACC_SUPER
Constant pool:
#1 = Methodref #5.#14 // java/lang/Object."<init>":()V
#2 = Methodref #15.#16 // java/lang/Integer.valueOf:(I)Ljava/lang/Integer;
#3 = Methodref #15.#17 // java/lang/Integer.intValue:()I
#4 = Class #18 // Test
#5 = Class #19 // java/lang/Object
#6 = Utf8 <init>
#7 = Utf8 ()V
#8 = Utf8 Code
#9 = Utf8 LineNumberTable
#10 = Utf8 main
#11 = Utf8 ([Ljava/lang/String;)V
#12 = Utf8 SourceFile
#13 = Utf8 Test.java
#14 = NameAndType #6:#7 // "<init>":()V
#15 = Class #20 // java/lang/Integer
#16 = NameAndType #21:#22 // valueOf:(I)Ljava/lang/Integer;
#17 = NameAndType #23:#24 // intValue:()I
#18 = Utf8 Test
#19 = Utf8 java/lang/Object
#20 = Utf8 java/lang/Integer
#21 = Utf8 valueOf
#22 = Utf8 (I)Ljava/lang/Integer;
#23 = Utf8 intValue
#24 = Utf8 ()I
{
public Test();
descriptor: ()V
flags: ACC_PUBLIC
Code:
stack=1, locals=1, args_size=1
0: aload_0
1: invokespecial #1 // Method java/lang/Object."<init>":()V
4: return
LineNumberTable:
line 1: 0
public static void main(java.lang.String[]);
descriptor: ([Ljava/lang/String;)V
flags: ACC_PUBLIC, ACC_STATIC
Code:
stack=1, locals=3, args_size=1
0: bipush 10
// 這行就是幫我們做的裝箱操作(執行的是valueOf(int)方法)
2: invokestatic #2 // Method java/lang/Integer.valueOf:(I)Ljava/lang/Integer;
5: astore_1
6: aload_1
// 這行是幫我們做的拆箱操作(執行的是intValue()方法)
7: invokevirtual #3 // Method java/lang/Integer.intValue:()I
10: istore_2
11: return
LineNumberTable:
line 3: 0
line 5: 6
line 7: 11
}
SourceFile: "Test.java"
之前只知道有裝箱和拆箱的說法,但是並不知道有什麼用,現在有點懵懂了。
希望這篇對大家理解Java有所幫助,有不完善的地方,希望指出。