包装类这颗语法糖,其实并不甜

本文探讨了Java包装类在实际使用中可能出现的问题,包括与基本类型的相等性判断差异、性能下降、不易察觉的错误及令人困惑的API设计。文章通过实例展示了包装类可能导致的不一致行为,强调了避免不必要的对象创建以提高性能,并提供了最佳实践建议,如在特定场景下使用包装类或基本类型。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

历史文章推荐:

  1. 你真的了解时间吗
  2. 细数ThreadLocal三大坑,内存泄露仅是小儿科
  3. Java 8 ConcurrentHashMap源码中竟然隐藏着两个BUG
  4. ConcurrentHashMap中有十个提升性能的细节,你都知道吗?
  5. HashMap面试,看这一篇就够了
  6. 七种方式教你在SpringBoot初始化时搞点事情

包装类在Java 5中和泛型一起引入,引入包装类的原因有两点:

  1. 解决无法创建基本类型泛型集合的问题
  2. 加入对基本类型为null这个语义的支持

并提供boxingunboxing的语法糖,让编译器支持基本类型和包装类的自动转化,减少开发者的工作量。但是经常有同学因为误用包装类导致惨烈的线上问题,在使用包装类的时候务必需要注意一下四点:

  1. 与基础类截然不同的==equals语义
  2. 糟糕的性能
  3. 不易察觉的NPE问题
  4. 令人疑惑的API设计

1. 相等还是不相等?这是个问题

比如以下代码片段

class Biziclop {
    public static void main(String[] args) {
        System.out.println(new Integer(5) == new Integer(5)); // false
        System.out.println(new Integer(500) == new Integer(500)); // false

        System.out.println(Integer.valueOf(5) == Integer.valueOf(5)); // true
        System.out.println(Integer.valueOf(500) == Integer.valueOf(500)); // false
    }
}

第一个和第二个语句返回false是比较容易理解的,因为对于Java中的对象调用=其实是在比较对象在堆上的地址,由于两个对象都是新建的,所以地址肯定不等,返回false。比较令人疑惑的是第三个语句,按照我们前面的分析,应该也返回false才对,但其实Integer.valueOf(5) == Integer.valueOf(5)比较的结果是true,这是因为JVM缓存了-128-127的整数,所以当数值在这个区间的时候,返回的对象都是同一个的。第四个语句因为数值已经不再-128-127的区间范围,所以返回了false

上面的这几个例子都是比较经典的例子,大家比较熟悉,一般也比较难掉坑里,但是下面的几个例子就比较有迷惑性了

class Biziclop {
    	public static void main(String[] args) {
        List<Long> list = new ArrayList<>();
        list.add(Long.valueOf(200));
        System.out.println(list.contains(200)); // false
        
        Long temp = 0L;
				System.out.println(temp.equals(0)); // false
       System.out.orintln(0==0L); // true
	}
}

原因在于

public boolean equals(Object obj) {
    if (obj instanceof Long) {
      return value == ((Long)obj).longValue();
    }
    return false;
}

包装类重写了equals方法,导致包装类即便是调用equals方法比较大小,也会和基本类型出现不一致的结果。与基础类截然不同的==equals语义经常会导致代码走到非期望的分支,再配上JVM对数字独特的缓存策略,极容易出现测试环境和正式环境不一样的运行结果。

2. 糟糕的性能

Effective Java》中有如下的例子:

public static void main(String[] args) {
    Long sum = 0L; // uses Long, not long
    for (long i = 0; i <= Integer.MAX_VALUE; i++) {
        sum += i;
    }
    System.out.println(sum);
}

这段代码的耗时比使用基本类型long的版本慢6倍(声明变量sum 类型为 Long 的耗时是43秒, 如果声明变量sum为基本类型long,则耗时6.8秒)。导致这样的原因是包装类要经过在堆上开辟内存空间,初始化,内存寻址以及数据载入寄存器的过程,性能差也就不足为奇了。因此Joshua Bloch对开发者的建议是:Avoid creating unnecessary objects.

在经典的JMH workbench上跑的包装类和基础类性能对比如下图所示:
在这里插入图片描述

可以看到与基础类相比,包装类普遍要慢不少。

图片来源地址:https://www.baeldung.com/java-primitives-vs-objects

3. 不易察觉的NPE

不同于基本类型,作为对象的包装类是可能为null的,这就意味着一个指向null的包装类unboxing的时候会抛出NPE异常,比如以下代码:

Integer in = null;
...
...
int i = in; // NPE at runtime

这段代码也是比较明显的,但是如果包装类遇到三元运算符,则会出现更复杂的NPE

class Biziclop {
    public static void main(String[] args) {
      Boolean b = true ? returnsNull() : false; // NPE on this line.
      System.out.println(b);
    }
  	public static Boolean returnsNull() {
    	return null;
		}
}

这跟Java中三元运算符类型的判定有关系,有一条判定规则是,

如果三元运算符的第二个或者第三个参数是基本类型T,并且另一个是相应的包装类型的话,那么三元运算符的返回类型就是这个基本类型T

所以在上面的代码中,returnsNull的返回值还要进行一次unboxing,因此抛出了NPE.

4. 令人疑惑的API

Long这个类中,有一个apigetLong,其声明如下:


    /**
     * Determines the {@code long} value of the system property
     * with the specified name.
     */
    public static Long getLong(String nm) {
        return getLong(nm, null);
    }

这个api的作用是获取JVM中的属性值的,并且转换为Long 类型,比如:

class Biziclop {
    	public static void main(String[] args) {
        System.setProperty("22", "22");
        System.setProperty("23", "hello world!");
        System.out.println(Long.getLong("22")); // 22
        System.out.println(Long.getLong("23")); // null
        System.out.println(Long.getLong("24")); // null
		}
}

这个api的设计妥妥是一个反例,经常有同学误用,把它当成Long.valueOf或者是Long.parseLong,结果返回不符合期望的值。

5. 最佳实践

《阿里巴巴Java编程手册》对包装类的使用有以下三条建议:

  1. 所有POJO类属性使用包装类
  2. RPC方法的返回值和参数使用包装类
  3. 所有的局部变量使用基本数据类型

说明:POJO类属性没有初值是提醒使用在在需要使用时,必须自己显式的进行赋值,任何NPE问题,或者入库检查,都有使用者来保证。

正例:数据库的查询结果可能是null,因为自动拆箱,用基本数据类型接受有NPE的风险

反例:某业务的交易报表上显式成交额涨跌情况,即x%,x为基本数据类型,调用的HSF服务,调用不成功时,返回的是默认值,页面展示0%,这是不合理的,应该展示成中划线-,所以包装类的null值,能够表示额外的信息,如:远程调用失败,异常退出。
在这里插入图片描述

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值