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