Skip to content

包装类比较陷阱

生产事故现场,你的订单金额比对出问题了。

java
Integer totalPrice = 200;
Integer discount = 200;

if (totalPrice == discount) {
    // 应该走进来,但没走
    applyDiscount();
}

明明都是 200,为什么 == 比较失败了?

这就是包装类比较的经典陷阱,而且往往在你不注意的时候给你一刀。

为什么 == 有时候不准?

基本类型用 == 比较值,这是天经地义的:

java
int a = 200;
int b = 200;
System.out.println(a == b);  // true,值相等

包装类用 == 比较的是引用地址,不是值:

java
Integer a = 200;  // 自动装箱 → Integer.valueOf(200) → new Integer(200)
Integer b = 200;  // 自动装箱 → Integer.valueOf(200) → new Integer(200)

System.out.println(a == b);  // false,两个不同的对象

问题出在缓存:只有 -128~127 范围的值会被缓存复用。

java
Integer a = 127;  // 缓存范围内,返回同一个对象
Integer b = 127;
System.out.println(a == b);  // true,碰巧相等

Integer c = 128;  // 超出范围,每次 new
Integer d = 128;
System.out.println(c == d);  // false,两个不同对象

基本类型 vs 包装类:四种组合

java
public class CompareCombinations {

    public static void main(String[] args) {
        // 组合一:基本类型 vs 基本类型
        int primitive1 = 200;
        int primitive2 = 200;
        System.out.println("int == int: " + (primitive1 == primitive2));  // true ✅

        // 组合二:包装类 vs 包装类(在缓存范围)
        Integer wrapper1 = 127;
        Integer wrapper2 = 127;
        System.out.println("Integer == Integer (127): " + (wrapper1 == wrapper2));  // true ✅

        // 组合三:包装类 vs 包装类(超出缓存)
        Integer wrapper3 = 200;
        Integer wrapper4 = 200;
        System.out.println("Integer == Integer (200): " + (wrapper3 == wrapper4));  // false ❌

        // 组合四:包装类 vs 基本类型
        Integer wrapper5 = 200;
        int primitive3 = 200;
        System.out.println("Integer == int: " + (wrapper5 == primitive3));  // true ✅
        // 包装类会自动拆箱,再和基本类型比较
    }
}

组合四为什么能 work? 因为 wrapper5 == primitive3 实际上被翻译成了 wrapper5.intValue() == primitive3

自动装箱的坑:与方法参数的暧昧关系

java
public class MethodParamTrap {

    public static void main(String[] args) {
        // 陷阱一:变量参与装箱
        int x = 100;
        Integer a = x;  // 变量装箱:new Integer(x)
        Integer b = 100;
        System.out.println("变量装箱 == 字面量: " + (a == b));  // false

        // 陷阱二:方法参数
        method(100);      // 字面量,缓存
        method(200);      // 超出缓存
    }

    static void method(Integer param) {
        // param == 100 实际上是比较 Integer 和 int
        // param.intValue() == 100 → true
        // 但如果外部传的是超出缓存的自动装箱...
        System.out.println("method: " + (param == 100));
    }
}

正确的比较方式

方式一:equals() —— 最通用

java
public class EqualsCompare {

    public static void main(String[] args) {
        Integer a = 200;
        Integer b = 200;

        // ✅ equals 比较值,永远正确
        System.out.println("a.equals(b): " + a.equals(b));  // true

        // ✅ equals 不受缓存影响
        Integer c = new Integer(200);
        Integer d = new Integer(200);
        System.out.println("new == new: " + (c == d));           // false
        System.out.println("new.equals(new): " + c.equals(d));  // true
    }
}

方式二:compare() —— 需要排序结果时

java
public class CompareMethod {

    public static void main(String[] args) {
        // compare 返回 -1、0、1,适合排序场景
        System.out.println(Integer.compare(10, 20));   // -1
        System.out.println(Integer.compare(20, 20));   // 0
        System.out.println(Integer.compare(30, 20));   // 1

        // 用于排序
        Integer[] nums = {3, 1, 2};
        Arrays.sort(nums, Integer::compare);
        System.out.println("排序后: " + Arrays.toString(nums));

        // 与 Comparator 的结合
        List<Integer> list = Arrays.asList(5, 2, 8);
        list.sort(Integer::compareTo);  // Integer 实现了 Comparable
    }
}

方式三:Objects.equals() —— null 安全

java
public class NullSafeCompare {

    public static void main(String[] args) {
        Integer a = null;
        Integer b = 100;

        // 直接 equals 会 NPE
        // System.out.println(a.equals(b));  // NPE

        // ✅ Objects.equals 是 null 安全的
        System.out.println(Objects.equals(a, b));  // false
        System.out.println(Objects.equals(null, null));  // true

        // ✅ 还可以自定义 null 时的返回值
        String result = Objects.equals(a, b) ? "相等" : "不等";
        System.out.println(result);
    }
}

Float 和 Double 的坑:精度问题

java
public class FloatPrecision {

    public static void main(String[] args) {
        // 浮点数的 == 永远不可靠
        Double d1 = 0.1;
        Double d2 = 0.1;
        System.out.println("Double 0.1 == 0.1: " + (d1 == d2));  // false(没有缓存)

        // 精度问题
        double a = 0.1;
        double b = 0.2;
        System.out.println("0.1 + 0.2 == 0.3: " + (a + b == 0.3));  // false!

        // NaN 比较
        Double nan1 = Double.NaN;
        Double nan2 = Double.NaN;
        System.out.println("NaN == NaN: " + (nan1 == nan2));        // false ❌
        System.out.println("NaN.equals(NaN): " + nan1.equals(nan2)); // true ✅

        // 正负零
        Double posZero = 0.0;
        Double negZero = -0.0;
        System.out.println("0.0 == -0.0: " + (posZero == negZero));  // true
        System.out.println("compare(0.0, -0.0): " + Double.compare(posZero, negZero)); // 1
    }
}

浮点数比较的正确方式

java
public class CorrectFloatCompare {

    // 方式一:使用 BigDecimal(金额计算必须用它)
    public static void main(String[] args) {
        BigDecimal bd1 = new BigDecimal("0.1");
        BigDecimal bd2 = new BigDecimal("0.2");
        BigDecimal result = bd1.add(bd2);
        System.out.println("BigDecimal 0.1 + 0.2: " + result);  // 0.3
        System.out.println("equals 0.3: " + result.equals(new BigDecimal("0.3")));  // true
    }

    // 方式二:容差比较(适用于工程计算)
    static boolean doubleEquals(double a, double b, double epsilon) {
        return Math.abs(a - b) < epsilon;
    }

    // 方式三:BigDecimal 字符串构造
    static boolean preciseEquals(double a, double b) {
        return BigDecimal.valueOf(a).equals(BigDecimal.valueOf(b));
    }
}

实战避坑指南

场景一:Map 的 key 比较

java
public class MapKeyTrap {

    public static void main(String[] args) {
        Map<Integer, String> map = new HashMap<>();

        map.put(200, "value");

        // ❌ 可能找不到
        Integer key = 200;
        System.out.println("== 查找: " + map.get(key));  // 可能 null

        // ✅ 用 equals 查找
        System.out.println("equals 查找: " + map.get(key));  // "value"
        // HashMap 用 hashCode 定位桶,用 equals 精确匹配
        // 所以只要 hashCode 对了,equals 就能找到
    }
}

场景二:集合排序

java
public class CollectionSort {

    public static void main(String[] args) {
        List<Integer> list = Arrays.asList(3, 1, 2);

        // ✅ 直接排序(Integer 实现了 Comparable)
        Collections.sort(list);
        System.out.println("排序: " + list);

        // ✅ 自定义比较器
        list.sort((a, b) -> Integer.compare(b, a));  // 降序
        System.out.println("降序: " + list);
    }
}

场景三:数据库映射

java
public class DbMapping {

    public static void main(String[] args) {
        // 数据库 ID 通常用 Long(避免 int 溢出)
        Long userId = 100L;
        Long orderId = 100L;

        // ⚠️ Long 的缓存范围也是 -128~127
        Long a = 100L;
        Long b = 100L;
        System.out.println("Long ==: " + (a == b));  // true(在缓存范围)

        a = 1000L;
        b = 1000L;
        System.out.println("Long == (1000): " + (a == b));  // false(超出)

        // ✅ 数据库 ID 比较永远用 equals
        System.out.println("Long equals: " + a.equals(b));  // true
    }
}

比较方法总结

场景推荐方法原因
基本类型比较==值比较
包装类比较equals()值比较
需要排序结果compare()返回 -1/0/1
可能为 nullObjects.equals()null 安全
浮点数比较BigDecimal 或容差精度问题
金额计算BigDecimal必须精确

要点回顾

  • 包装类 == 比较引用地址,不是值
  • 只有 -128~127 的 Integer 有缓存,== 可能碰巧为 true
  • 永远用 equals() 比较包装类的值
  • 浮点数 == 永远不可靠,用 BigDecimal 或容差
  • NaN 和自身比较也是 false,必须用 isNaN()
  • 数据库 ID 用 Long,比较用 equals()

血的教训:代码里出现 Integer == Integer,review 时直接打回。等线上出事就晚了。

基于 VitePress 构建