这篇文章将深入讲解Java的拆箱和装箱机制 |
☕️ 基本数据类型
基本类型,或者叫做内置类型,是Java中不同于类(Class)的特殊类型,也是我们编程中使用最频繁的类型。Java基本类型共有八种,基本类型可以分为三类:
- 字符类型 char
- 布尔类型 boolean
- 数值类型 byte、short、int、long、float、double
其中,数值类型又可以分为:整数类型 byte、short、int、long 和浮点数类型 float、double
注意:Java中的数值类型不存在无符号的,它们的取值范围是固定的,不会随着机器硬件环境或者操作系统的改变而改变 |
基本数据类型的好处
我们都知道,在Java语言中,new 一个对象是存储在堆里的,我们通过栈中的引用来使用这些对象,所以,对象本身来说是比较消耗资源的。
对于经常用到的类型,如 int 等,如果我们每次使用这种变量的时候都需要 new 一个 Java 对象的话,就会比较笨重。所以,和 C++ 一样,Java 提供了基本数据类型,这种数据的变量不需要使用 new 创建,他们不会在堆上创建,而是直接在栈内存中存储,因此会更加高效
整型的取值范围
Java 中的整型主要包含 byte、short、int 和 long 这四种,表示的数字范围也是从小到大的:
- byte:byte 用1个字节来存储,范围为 -128(-2^7) 到 127(2^7-1),在变量初始化的时候,byte 类型的默认值为0
- short:short 用2个字节存储,范围为 -32,768 (-2^15) 到 32,767 (2^15-1),在变量初始化的时候,short 类型的默认值为0,一般情况下,因为 Java 本身转型的原因,可以直接写为0
- int:int 用4个字节存储,范围为 -2,147,483,648 (-2^31) 到 2,147,483,647 (2^31-1),在变量初始化的时候,int类型的默认值为0
- long:long用8个字节存储,范围为-9,223,372,036,854,775,808 (-2^63)到9,223,372,036, 854,775,807 (2^63-1),在变量初始化的时候,long 类型的默认值为 0L 或 0l(小写字母l),也可直接写为0
从上面可以看出每个整型类型都有一定的表示范围,所以,如果在程序中的计算超出了表示范围则会发生溢出,比如:
int i = Integer.MAX_VALUE;
int j = Integer.MAX_VALUE;
int k = i + j;
System.out.println("i(" + i + ") + j(" + j + ") = k(" + k + ")");
>>>>>
i(2147483647) + j(2147483647) = k(-2)
溢出的时候并不会抛异常,也没有任何提示。所以,在程序中,使用同类型的数据进行运算的时候,一定要注意数据溢出的问题
☕️ 包装类型
Java 语言是一个面向对象的语言,但是 Java 中的基本数据类型却是不面向对象的,这在实际使用时存在很多的不便,为了解决这个不足,在设计类时为每个基本数据类型设计了一个对应的类进行代表,这八个和基本数据类型对应的类统称为包装类(Wrapper Class),包装类均位于 java.lang 包,包装类和基本数据类型的对应关系如下表所示
基本数据类型 | 对应的包装类 | 基本数据类型 | 对应的包装类 |
---|---|---|---|
byte | Byte | boolean | Boolean |
short | Short | char | Character |
int | Integer | long | Long |
float | Float | double | Double |
在这八个类名中,除了 Intege r和 Character 类以后,其它六个类的类名和基本数据类型一致,只是类名的第一个字母大写即可
为什么需要包装类
既然 Java 中为了提高效率,提供了八种基本数据类型,为什么还要提供包装类呢?因为 Java 是一种面向对象语言,很多地方都需要使用对象而不是基本数据类型。比如,在集合类中,我们是无法将 int 、double 等类型放进去的。因为集合的容器要求元素是 Object 类型。
所以,为了让基本类型也具有对象的特征,就出现了包装类型,它相当于将基本类型“包装起来”,使得它具有了对象的性质,并且为其添加了属性和方法,丰富了基本类型的操作。
☕️ 拆箱与装箱
那么,有了基本数据类型和包装类,肯定有些时候要在他们之间进行转换。比如把一个基本数据类型的int转换成一个包装类型的Integer对象。
- 把基本数据类型转换成包装类的过程就是打包装,专业术语叫装箱(boxing)
- 把包装类转换成基本数据类型的过程就是拆包装,专业术语叫拆箱(unboxing)
☕️ 自动拆箱与自动装箱
在Java SE 5之前,要进行装箱,需要通过以下代码:
Integer i = new Integer(10);
在 Java SE 5 之后,为了减少开发人员的工作,Java 提供了自动拆箱与自动装箱功能
- 自动装箱: 就是将基本数据类型自动转换成对应的包装类。
- 自动拆箱:就是将包装类自动转换成对应的基本数据类型。
比如:
Integer i = 10; // 自动装箱
int j = 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() 来实现的,这里的 xxx 表示对应的基本类型。 |
☕️ 自动拆装箱的应用场景
包装类和基本类形之间的运算
包括包装类与基本类型之间的赋值、比较、算数运算等,以下面的代码为例:
// 赋值
Integer i = 10;
// 比较大小
i > 1 ? "大于" : "不大于";
// 真假判断
Boolean b = true;
b ? "真" : "假";
// 算数运算
int j = i + 5;
反编译后的代码为:
Integer i = Integer.valueOf(10);
i.intValue() > 1 ? "大于" : "不大于";
b.booleanValue() ? "真" : "假";
int j = i.intValue() + 5;
可以看到,包装类与基本数据类型进行各种运算时,是先将包装类进行拆箱成基本数据类型,然后进行运算的。
注意:在进行三目运算符的时候,由于三目运算符的语法规范,当第二,第三位操作数分别为基本类型和包装类时,其中的包装类就会拆箱为基本类型进行操作。比如:flag ? i : j; 如果第二段的 i 为包装类型的对象,而第三段 j 为基本类型时,就会对包装类 i 进行自动拆箱,如果这个时候 i 的值为 null,那么久会发生 NullPointerException 异常 |
基本数据类型放入集合类
我们知道,Java 中的集合类只能接收对象类型,但放入基本类型却是合法的,比如:
List<Integer> li = new ArrayList<>();
for (int i = 0; i < 10; i++) {
li.add(i);
}
我们将上面代码进行反编译来看一下:
List<Integer> li = new ArrayList<>();
for (int i = 0; i < 10; i++) {
li.add(Integer.valueOf(i)); // 进行了自动装箱
}
从中可以看出,当我们把基本数据类型放入集合类的时候,会进行自动装箱。
作为函数参数和返回值
这个就比较好理解了,比如:
// 如果传递的参数为基本类型则参数自动装箱为包装类,返回时则会自动拆箱为基本类型
public int getNum(Integer num) {
return num;
}
// 如果传递的参数为包装类时则参数会自动拆箱为基本类型,返回时则会装箱为包装类
public Integer getNum(int num) {
return num;
}
☕️ 自动拆装箱与缓存
先看一下下面的代码:
Integer integer1 = 5;
Integer integer2 = 5;
Integer integer3 = 300;
Integer integer4 = 300;
System.out.println(integer1 == integer2);
System.out.println(integer3 == integer4);
>>>>>
true
false
我们都知道,在 Java 中,== 比较的是对象的引用,所以,在这个例子中,不同的对象有不同的引用,进行比较的时候,理应该都返回 false 才对的呀,可第一个比较却返回了 true,这是为什么呢?
原因是 Integer 中的缓存机制。在 Java 5 中,Integer 的操作上引入了一个新功能来节省内存和提高性能:整型对象通过使用相同的对象引用实现了缓存和重用。但又限制条件:
- 适用于整数值区间-128 至 +127
- 只适用于自动装箱,使用构造函数创建包装类对象不适用
我们只需要知道,当需要进行自动装箱时,如果数字在 -128 至 127 之间时,会直接使用缓存中的对象,而不是重新创建一个对象
☕️ 自动拆装箱的弊端
- 包装对象的数值比较,不能简单的使用 ==,虽然 -128 到 127 之间的数字可以,但是这个范围之外还是需要使用 equals 比较
- 自动拆箱时,如果包装类对象为 null,那么就会抛出 NullPointerException 异常
- 如果一个for循环中有大量拆装箱操作,会浪费很多资源