• 已删除用户
Administrator
发布于 2019-02-20 / 6 阅读
0

Java 拆箱与装箱

转载自 《Java工程师成神之路-自动拆装箱》。

一、什么是包装类型

Java 语言是一个面向对象的语言,但是 Java 中的基本数据类型却是不面向对象的,这在实际使用时存在很多的不便,为了解决这个不足,在设计类时为每个基本数据类型设计了一个对应的类进行代表,这样八个和基本数据类型对应的类统称为包装类(Wrapper Class)。

包装类均位于 java.lang 包,包装类和基本数据类型的对应关系如下表所示:

基本数据类型

包装类

byte

Byte

boolean

Boolean

short

Short

char

Character

int

Integer

long

Long

float

Float

double

Double

在这八个类名中,除了 Integer 和 Character 类以后,其它六个类的类名和基本数据类型一致,只是类名的第一个字母大写即可。

二、拆箱与装箱

那么,有了基本数据类型和包装类,肯定有些时候要在他们之间进行转换。比如把一个基本数据类型的 int 转换成一个包装类型的 Integer 对象。

我们认为包装类是对基本类型的包装,所以,把基本数据类型转换成包装类的过程就是打包装,英文对应于 boxing,中文翻译为装箱。

反之,把包装类转换成基本数据类型的过程就是拆包装,英文对应于 unboxing,中文翻译为拆箱。

在 Java SE5 之前,要进行装箱,可以通过以下代码:

Integer i = new Integer(10);

三、自动拆箱与自动装箱

在 Java SE5 中,为了减少开发人员的工作,Java 提供了自动拆箱与自动装箱功能。

  • 自动装箱: 就是将基本数据类型自动转换成对应的包装类。

  • 自动拆箱:就是将包装类自动转换成对应的基本数据类型。

Integer i = 10;  //自动装箱
int b = i;     //自动拆箱

Integer i=10 可以替代 Integer i = new Integer(10);,这就是因为 Java 帮我们提供了自动装箱的功能,不需要开发者手动去 new 一个 Integer 对象。

四、自动装箱与自动拆箱的实现原理

既然 Java 提供了自动拆装箱的能力,那么,我们就来看一下,到底是什么原理,Java 是如何实现的自动拆装箱功能。

我们有以下自动拆装箱的代码:

public static void main(String[]args){
    Integer integer = 1; //装箱
    int i = integer; //拆箱
}

对以上代码进行反编译后可以得到以下代码:

public static void main(String[]args){
    Integer integer=Integer.valueOf(1);
    int i = integer.intValue();
}

从上面反编译后的代码可以看出,int 的自动装箱都是通过 Integer.valueOf() 方法来实现的,Integer 的自动拆箱都是通过 integer.intValue 来实现的。如果读者感兴趣,可以试着将八种类型都反编译一遍 ,你会发现以下规律:

自动装箱都是通过包装类的 valueOf() 方法来实现的.自动拆箱都是通过包装类对象的 xxxValue() 来实现的。

五、哪些地方会自动拆装箱

我们了解过原理之后,在来看一下,什么情况下,Java 会帮我们进行自动拆装箱。前面提到的变量的初始化和赋值的场景就不介绍了,那是最简单的也最容易理解的。

我们主要来看一下,那些可能被忽略的场景。

5.1 场景一、将基本数据类型放入集合类

我们知道,Java 中的集合类只能接收对象类型,那么以下代码为什么会不报错呢?

List<Integer> li = new ArrayList<>();
for (int i = 1; i < 50; i ++){
    li.add(i);
}

将上面代码进行反编译,可以得到以下代码:

List<Integer> li = new ArrayList<>();
for (int i = 1; i < 50; i += 2){
    li.add(Integer.valueOf(i));
}

以上,我们可以得出结论,当我们把基本数据类型放入集合类中的时候,会进行自动装箱。

5.2 场景二、包装类型和基本类型的大小比较

有没有人想过,当我们对 Integer 对象与基本类型进行大小比较的时候,实际上比较的是什么内容呢?看以下代码:

Integer a = 1;
System.out.println(a == 1 ? "等于" : "不等于");
Boolean bool = false;
System.out.println(bool ? "真" : "假");

对以上代码进行反编译,得到以下代码:

Integer a = 1;
System.out.println(a.intValue() == 1 ? "等于" : "不等于");
Boolean bool = false;
System.out.println(bool.booleanValue ? "真" : "假");

可以看到,包装类与基本数据类型进行比较运算,是先将包装类进行拆箱成基本数据类型,然后进行比较的。

5.3 场景三、包装类型的运算

有没有人想过,当我们对 Integer 对象进行四则运算的时候,是如何进行的呢?看以下代码:

Integer i = 10;
Integer j = 20;
System.out.println(i+j);

反编译后代码如下:

Integer i = Integer.valueOf(10);
Integer j = Integer.valueOf(20);
System.out.println(i.intValue() + j.intValue());

我们发现,两个包装类型之间的运算,会被自动拆箱成基本类型进行。

5.4 场景四、三目运算符的使用

这是很多人不知道的一个场景,看一个简单的三目运算符的代码:

boolean flag = true;
Integer i = 0;
int j = 1;
int k = flag ? i : j;

很多人不知道,其实在 int k = flag ? i : j; 这一行,会发生自动拆箱。反编译后代码如下:

boolean flag = true;
Integer i = Integer.valueOf(0);
int j = 1;
int k = flag ? i.intValue() : j;
System.out.println(k);

这其实是三目运算符的语法规范。当第二,第三位操作数分别为基本类型和对象时,其中的对象就会拆箱为基本类型进行操作。

因为例子中,flag ? i : j; 片段中,第二段的 i 是一个包装类型的对象,而第三段的 j 是一个基本类型,所以会对包装类进行自动拆箱。如果这个时候 i 的值为 null,那么就会发生 NPE。

5.5 场景五、函数参数与返回值

这个比较容易理解,直接上代码了:

//自动拆箱
public int getNum1(Integer num) {
    return num;
}
//自动装箱
public Integer getNum2(int num) {
    return num;
}

六、自动拆装箱与缓存

Java SE 的自动拆装箱还提供了一个和缓存有关的功能,在 Boxing Conversion 部分的 Java 语言规范(JLS)规定如下:

如果一个变量 p 的值是:

  • -128 至 127 之间的整数 (§3.10.1)

  • true 和 false 的布尔值 (§3.10.3)

  • \u0000\u007f 之间的字符 (§3.10.4)

范围内的时,将 p 包装成 a 和 b 两个对象时,可以直接使用 a == b 判断 a 和 b 的值是否相等。

七、自动拆装箱带来的问题

当然,自动拆装箱是一个很好的功能,大大节省了开发人员的精力,不再需要关心到底什么时候需要拆装箱。但是,他也会引入一些问题。

  • 包装对象的数值比较,不能简单的使用 ==,虽然 -128 到 127 之间的数字可以,但是这个范围之外还是需要使用 equals 比较。

  • 前面提到,有些场景会进行自动拆装箱,同时也说过,由于自动拆箱,如果包装类对象为 null ,那么自动拆箱时就有可能抛出 NPE。

  • 如果一个 for 循环中有大量拆装箱操作,会浪费很多资源。