Java自动装箱拆箱

1.抛出问题(一次NullPointerException带来的思考)

遇到这样一个场景,代码类似如下:

一个Controller,使用GET方法获取参数id对应的数据

    @RequestMapping(value = "/home/show", method = {RequestMethod.GET})
    public Result homeShow(@RequestParam(name = "id") Long id) {
        List<HomeShow> res = homeShowService.show(id);
        if (res != null) {
            return Result.success(res);
        } else {
            return Result.fail(ResultCode.ERROR,"");
        }
    }

一个Service,homeShowService,对应的接口是这样写的,实现方法省略

public interface CircuitWarnService {
		List<HomeShow> show(long id);
}

代码这样写完上线,然后时不时就会报NullPointerException。

抛出问题:Controller接收参数id的类型是Long,而Service接收参数id的类型是long,

如果前端传进来的是id为null,Controller不会报错,调用Service的时候就会报错,

因为从类型 Long 到基本数据类型 long 有个自动拆箱过程,那这个过程中会不会抛出 NullPointerException 呢?

2.原因分析

模拟同样的一个场景

public class AutoUnbox {
    public static long test(long value) {
        return value;
    }

    public static void main(String[] args) {
        Long value = null;
        test(value);
    }
}

编译能通过,运行时就会抛NullPointerException;使用 javap -c AutoUnbox 对上述代码class文件进行反汇编,可以看到

public class first.demo.AutoUnbox {
  public first.demo.AutoUnbox();
    Code:
       0: aload_0
       1: invokespecial #1                  // Method java/lang/Object."<init>":()V
       4: return

  public static long test(long);
    Code:
       0: lload_0
       1: lreturn

  public static void main(java.lang.String[]);
    Code:
       0: aconst_null
       1: astore_1
       2: aload_1
       3: invokevirtual #2                  // Method java/lang/Long.longValue:()J
       6: invokestatic  #3                  // Method test:(J)J
       9: pop2
      10: return
}

test(value)对应着反汇编后的

 3: invokevirtual #2                  // Method java/lang/Long.longValue:()J
 6: invokestatic  #3                  // Method test:(J)J

可以看出确实有个自动拆箱过程「Method java/lang/Long.longValue:()J」,而我们传入的 value 为 null,value.longValue() 会抛出 NullPointerException。

所以对于开始的问题,解决方法在方法调用链路上都使用同样类型,要么使用基础类型long,要么使用引用类型Long

3.自动拆箱与装箱介绍

自动装箱与拆箱是 Java 1.5 引入的新特性,是一种语法糖。

在此之前,我们要创建一个值为 10 的 Integer 对象,只能写作:

Integer value = new Integer(10);

而现在,我们可以更方便地写为:

Integer value = 10;

定义

自动装箱,是指从基本数据类型值到其对应的包装类对象的自动转换。比如 Integer value = 10;,是通过调用 Integer.valueOf 方法实现转换的

自动拆箱,是指从包装类对象到其对应的基本数据类型值的自动转换。比如 int primitive = value;,是通过调用 Integer.intValue 方法实现转换的

基本数据类型

包装类型

装箱方法

拆箱方法

boolean

Boolean

Boolean.valueOf(boolean)

Boolean.booleanValue()

byte

Byte

Byte.valueOf(byte)

Byte.byteValue()

char

Character

Character.valueOf(char)

Character.charValue()

short

Short

Short.valueOf(short)

Short.shortValue()

int

Integer

Integer.valueOf(int)

Integer.intValue()

long

Long

Long.valueOf(long)

Long.longValue()

float

Float

Float.valueOf(float)

Float.floatValue()

double

Double

Double.valueOf(double)

Double.doubleValue()

发生时机

自动装箱与拆箱主要发生在以下四种时机:

  • 1.赋值时

Integer value = 10; // 自动装箱(赋值时)
int primitive = value; // 自动拆箱(方法调用时)
  • 2.比较时

Integer value = 1000;
if (value <= 1000) { // 自动拆箱(比较时)
}
  • 3.算术运算时

Integer v1 = new Integer(10);
Integer v2 = new Integer(20);
int v3 = 30;

int sum = v1 + v2; // 自动拆箱(算术运算时)
sum = v1 + 30; // 自动拆箱(算术运算时)
  • 4.方法调用时

List<Integer> list = new ArrayList<>();
list.add(10); // 自动装箱(方法调用时)

int i = list.get(0); // 自动拆箱(赋值时)

4.相关知识点

比较

除 == 以外,包装类对象与基本数据类型值的比较,包装类对象与包装类对象之间的比较,都是自动拆箱后对基本数据类型值进行比较,所以,要注意这些类型间进行比较时自动拆箱可能引发的 NullPointerException

== 比较特殊,因为可以用于判断左右是否为同一对象,所以两个包装类对象之间 ==,会用于判断是否为同一对象,而不会进行自动拆箱操作;包装类对象与基本数据类型值之间 ==,会自动拆箱。

示例代码:

Integer v1 = new Integer(10);
Integer v2 = new Integer(20);
if (v1 < v2) { }  // 自动拆箱
if (v1 == v2) { } // 不拆箱 
if (v1 == 10) { } // 自动拆箱  

缓存

Java 为整型值包装类 Byte、Character、Short、Integer、Long 设置了缓存,用于存储一定范围内的值,详细如下:

类型

缓存值范围

Byte

-128 ~ 127

Character

0 ~ 127

Short

-128 ~ 127

Integer

-128 ~ 127(可配置)

Long

-128 ~ 127

在一些情况下,比如自动装箱时,如果值在缓存值范围内,将不创建新对象,直接从缓存里取出对象返回,比如:

Integer v1 = 10;
Integer v2 = 10;
Integer v3 = new Integer(10);
Integer v4 = 128;
Integer v5 = 128;
Integer v6 = Integer.valueOf(10);

System.out.println(v1 == v2); // true,都是缓存中对象
System.out.println(v1 == v3); // false,v1是缓存中对象,v3强制new对象
System.out.println(v4 == v5); // false,都不是缓存中对象,都重新new
System.out.println(v1 == v6); // true,都是缓存中对象

缓存实现机制:

这里使用了设计模式享元模式:重用现有的同类对象,如果未找到匹配的对象,则创建新对象

以 Short 类实现源码为例:(jdk1.8中源码位置在/java/lang/Short.java)

public final class Short extends Number implements Comparable<Short> {
    private static class ShortCache {
        private ShortCache(){}
        static final Short cache[] = new Short[-(-128) + 127 + 1];
        static {
            for(int i = 0; i < cache.length; i++)
                cache[i] = new Short((short)(i - 128));
        }
    }

    public static Short valueOf(short s) {
        final int offset = 128;
        int sAsInt = s;
        if (sAsInt >= -128 && sAsInt <= 127) { // must cache
        		// 范围内的直接返回缓存中的对象,避免频繁创建对象
            return ShortCache.cache[sAsInt + offset];
        }
        return new Short(s);
    }
    }

在第一次调用到 Short.valueOf(short) 方法时,将创建 -128 ~ 127 对应的 256 个对象缓存到堆内存里。

这种设计,在频繁用到这个范围内的值的时候效率较高,可以避免重复创建和回收对象,否则有可能闲置较多对象在内存中。

使用不当的情况

如果使用不当,也有可能带来负面影响,如性能的损耗

Integer sum = 0;
for (int i = 1000; i < 5000; i++) {
    // 1. 先对 sum 进行自动拆箱
    // 2. 加法
    // 3. 自动装箱赋值给 sum,无法命中缓存,会 new Integer(int)
    sum = sum + i;
}

在循环过程中会分别调用 4000 次 Integer.intValue() 和 Integer.valueOf(int),并 new 4000 个 Integer 对象,

而这些操作将 sum 的类型改为 int 即可避免,节约运行时间和空间,提升性能。

 

主要参考:

https://mazhuang.org/2017/08/20/java-auto-boxing-unboxing/

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值