【避坑指南】Java Long包装类比较引发的异常——用户ID>127删除订单失败的BUG

一、问题描述

在最近的一个项目开发过程中,我遇到了一个非常诡异的BUG:

现象:

  • 用户ID=2的用户,可以正常删除自己的订单 ✅
  • 用户ID=129的用户,删除自己的订单时却提示"不能删除他人订单" ❌

明明是自己的订单,为什么会被拦截?难道系统认为这是别人的订单?

二、问题代码

先来看出问题的代码:

@Override
public void deleteOrder(Long id) {
    // 1.获取登录用户
    Long userId = UserContext.getUser();
    // 2.查询订单
    Order order = getById(id);
    if (order == null) {
        return;
    }
    // 3.判断订单所属用户与当前登录用户是否一致
    if(userId != order.getUserId()){  // ❌ BUG就在这里!
        // 不一致,说明不是当前用户的订单,结束
        throw new BadRequestException("不能删除他人订单");
    }
    // 4.删除订单
    boolean success = removeById(id);
    if (!success) {
        throw new DbException(OPERATE_FAILED);
    }
}

三、DEBUG排查过程

通过IDEA的调试模式,我查看了运行时对象的详细信息:

调试截图显示:
在这里插入图片描述
在这里插入图片描述

发现问题:

  • 两个Long对象的数值都是129,应该相等
  • 但它们的内存地址不同(@15448 vs @15452)
  • 使用 != 比较时,比较的是对象引用地址,而不是数值!

四、底层原理剖析

1. Java包装类的缓存机制

在Java中,LongInteger等包装类有一个缓存池机制。以Long为例,查看JDK源码:

@HotSpotIntrinsicCandidate
public static Long valueOf(long l) {
    final int offset = 128;
    if (l >= -128 && l <= 127) {  // 缓存范围
        return LongCache.cache[(int)l + offset];
    }
    return new Long(l);  // 超出范围,new新对象
}

核心逻辑:

  • -128 到 127:从缓存池中获取,同一个数值永远是同一个对象
  • 超出范围:每次都会 new Long()生成新对象

2. !=equals() 的区别

比较方式 比较内容 适用场景
== / != 内存地址(引用是否相同) 基本类型比较
equals() 数值内容(值是否相等) 包装类对象比较

3. 问题复现

场景一:用户ID=2(在缓存范围内)

Long userId = 2L;           // 从缓存获取,地址@1234
Long orderUserId = 2L;      // 从缓存获取,地址@1234(同一个对象)

userId != orderUserId       // false(地址相同,判断正确)✅

场景二:用户ID=129(超出缓存范围)

Long userId = 129L;         // new Long(129),地址@15448
Long orderUserId = 129L;    // new Long(129),地址@15452(不同对象)

userId != orderUserId       // true(地址不同,判断错误)❌
// 系统误判为"他人订单",抛出异常

五、正确解决方案

方案一:使用 equals()(推荐)

@Override
public void deleteOrder(Long id) {
    // 1.获取登录用户
    Long userId = UserContext.getUser();
    // 2.查询订单
    Order order = getById(id);
    if (order == null) {
        return;
    }
    // 3.判断订单所属用户与当前登录用户是否一致
    if(!userId.equals(order.getUserId())){  // ✅ 正确:比较数值
        // 不一致,说明不是当前用户的订单,结束
        throw new BadRequestException("不能删除他人订单");
    }
    // 4.删除订单
    boolean success = removeById(id);
    if (!success) {
        throw new DbException(OPERATE_FAILED);
    }
}

方案二:比较基本类型

if(userId.longValue() != order.getUserId().longValue()){  // ✅ 也可以
    throw new BadRequestException("不能删除他人订单");
}

方案三:Objects.equals()(更安全)

if(!Objects.equals(userId, order.getUserId())){  // ✅ 最安全,避免NPE
    throw new BadRequestException("不能删除他人订单");
}

七、扩展知识:其他包装类的缓存范围

包装类 缓存范围 说明
Byte -128 ~ 127 全部值(Byte就这么多)
Short -128 ~ 127 常用值
Integer -128 ~ 127 常用值,可通过JVM参数调整
Long -128 ~ 127 常用值
Float/Double 无缓存 浮点数精度问题,不建议缓存
Character 0 ~ 127 ASCII字符
Boolean true/false 只有两个值

八、避坑指南

🚫 错误写法

// 错误:包装类使用 == 或 !=
if (userId == order.getUserId()) { }
if (userId != order.getUserId()) { }

// 错误:包装类与null比较时可能NPE
if (userId.intValue() > 100) { }  // userId为null时报错

✅ 正确写法

// 推荐:使用equals()
if (userId.equals(order.getUserId())) { }

// 更安全:使用Objects.equals()
if (Objects.equals(userId, order.getUserId())) { }

// 比较基本类型
if (userId.longValue() > 100L) { }

💡 最佳实践

  1. 数据库ID、金额等Long类型字段比较,一律使用 equals()
  2. 涉及null值比较时,使用 Objects.equals()
  3. 如果确定不为null,可以使用 .longValue().intValue()
  4. 开启IDEA警告检查,IDE会提示包装类比较问题

九、总结

这个BUG虽然简单,但非常隐蔽:

  • 小数字用户正常,大数字用户异常,容易被误认为是权限或数据问题
  • 调试时数值相同,但地址不同,需要深入理解Java包装类机制
  • 修复成本极低,但排查成本很高

核心教训:

Java中所有包装类(Long、Integer等)的比较,必须使用 equals(),永远不要用 ==!=


如果你觉得这篇文章对你有帮助,欢迎点赞、收藏、转发!有问题欢迎在评论区交流讨论~

更多推荐