java的Integer比較==你真的懂嗎?

導致我去看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有所幫助,有不完善的地方,希望指出。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章