本文主要介绍 Java 中的自动拆箱与自动装箱的有关知识.
基本数据类型
基本类型, 或者叫做内置类型, 是 Java 中不同于类 (Class) 的特殊类型. 它们是我们编程中使用最频繁的类型.
Java 是一种强类型语言, 第一次申明变量必须说明数据类型, 第一次变量赋值称为变量的初始化.
Java 基本类型共有八种, 基本类型可以分为三类:
字符类型 char
布尔类型 boolean
整数类型 byte,short,int,long
浮点数类型 float,double.
Java 中的数值类型不存在无符号的, 它们的取值范围是固定的, 不会随着机器硬件环境或者操作系统的改变而改变.
实际上, Java 中还存在另外一种基本类型 void, 它也有对应的包装类 java.lang.Void, 不过我们无法直接对它们进行操作.
基本数据类型有什么好处
我们都知道在 Java 语言中, new 一个对象是存储在堆里的, 我们通过栈中的引用来使用这些对象; 所以, 对象本身来说是比较消耗资源的.
对于经常用到的类型, 如 int 等, 如果我们每次使用这种变量的时候都需要 new 一个 Java 对象的话, 就会比较笨重.
所以, 和 C++ 一样, Java 提供了基本数据类型, 这种数据的变量不需要使用 new 创建, 他们不会在堆上创建, 而是直接在栈内存中存储, 因此会更加高效.
整型的取值范围
Java 中的整型主要包含 byte,short,int 和 long 这四种, 表示的数字范围也是从小到大的, 之所以表示范围不同主要和他们存储数据时所占的字节数有关.
先来个简答的科普, 1 字节 = 8 位(bit).java 中的整型属于有符号数.
先来看计算中 8bit 可以表示的数字:
最小值: 10000000 (-128)(-2^7)
最大值: 01111111(127)(2^7-1)
整型的这几个类型中,
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, 也可直接写为 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 包, 包装类和基本数据类型的对应关系如下表所示
在这八个类名中, 除了 Integer 和 Character 类以后, 其它六个类的类名和基本数据类型一致, 只是类名的第一个字母大写即可.
为什么需要包装类
很多人会有疑问, 既然 Java 中为了提高效率, 提供了八种基本数据类型, 为什么还要提供包装类呢?
这个问题, 其实前面已经有了答案, 因为 Java 是一种面向对象语言, 很多地方都需要使用对象而不是基本数据类型. 比如, 在集合类中, 我们是无法将 int ,double 等类型放进去的. 因为集合的容器要求元素是 Object 类型.
为了让基本类型也具有对象的特征, 就出现了包装类型, 它相当于将基本类型 "包装起来", 使得它具有了对象的性质, 并且为其添加了属性和方法, 丰富了基本类型的操作.
拆箱与装箱
那么, 有了基本数据类型和包装类, 肯定有些时候要在他们之间进行转换. 比如把一个基本数据类型的 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 会帮我们进行自动拆装箱. 前面提到的变量的初始化和赋值的场景就不介绍了, 那是最简单的也最容易理解的.
我们主要来看一下, 那些可能被忽略的场景.
场景一, 将基本数据类型放入集合类
我们知道, Java 中的集合类只能接收对象类型, 那么以下代码为什么会不报错呢?
- List li = new ArrayList<>();
- for (int i = 1; i < 50; i ++){
- li.add(i);
- }
将上面代码进行反编译, 可以得到以下代码:
- List li = new ArrayList<>();
- for (int i = 1; i < 50; i += 2){
- li.add(Integer.valueOf(i));
- }
以上, 我们可以得出结论, 当我们把基本数据类型放入集合类中的时候, 会进行自动装箱.
场景二, 包装类型和基本类型的大小比较
有没有人想过, 当我们对 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?"真":"假");
可以看到, 包装类与基本数据类型进行比较运算, 是先将包装类进行拆箱成基本数据类型, 然后进行比较的.
场景三, 包装类型的运算
有没有人想过, 当我们对 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());
我们发现, 两个包装类型之间的运算, 会被自动拆箱成基本类型进行.
场景四, 三目运算符的使用
这是很多人不知道的一个场景, 作者也是一次线上的血淋淋的 Bug 发生后才了解到的一种案例. 看一个简单的三目运算符的代码:
- 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;
这其实是三目运算符的语法规范: 当第二, 第三位操作数分别为基本类型和对象时, 其中的对象就会拆箱为基本类型进行操作.
因为例子中, flag ? i : j; 片段中, 第二段的 i 是一个包装类型的对象, 而第三段的 j 是一个基本类型, 所以会对包装类进行自动拆箱. 如果这个时候 i 的值为 null, 那么久会发生 NPE.(自动拆箱导致空指针异常)
场景五, 函数参数与返回值
这个比较容易理解, 直接上代码了:
- // 自动拆箱
- public int getNum1(Integer num) {
- return num;
- }
- // 自动装箱
- public Integer getNum2(int num) {
- return num;
- }
自动拆装箱与缓存
Java SE 的自动拆装箱还提供了一个和缓存有关的功能, 我们先来看以下代码, 猜测一下输出结果:
- public static void main(String... strings) {
- Integer integer1 = 3;
- Integer integer2 = 3;
- if (integer1 == integer2)
- System.out.println("integer1 == integer2");
- else
- System.out.println("integer1 != integer2");
- Integer integer3 = 300;
- Integer integer4 = 300;
- if (integer3 == integer4)
- System.out.println("integer3 == integer4");
- else
- System.out.println("integer3 != integer4");
- }
我们普遍认为上面的两个判断的结果都是 false. 虽然比较的值是相等的, 但是由于比较的是对象, 而对象的引用不一样, 所以会认为两个 if 判断都是 false 的.
在 Java 中,== 比较的是对象应用, 而 equals 比较的是值.
所以, 在这个例子中, 不同的对象有不同的引用, 所以在进行比较的时候都将返回 false. 奇怪的是, 这里两个类似的 if 条件判断返回不同的布尔值.
上面这段代码真正的输出结果:
- integer1 == integer2
- integer3 != integer4
原因就和 Integer 中的缓存机制有关. 在 Java 5 中, 在 Integer 的操作上引入了一个新功能来节省内存和提高性能. 整型对象通过使用相同的对象引用实现了缓存和重用.
适用于整数值区间 - 128 至 +127.
只适用于自动装箱. 使用构造函数创建对象不适用.
具体的代码实现可以阅读 Java 中整型的缓存机制一文, 这里不再阐述.
我们只需要知道, 当需要进行自动装箱时, 如果数字在 - 128 至 127 之间时, 会直接使用缓存中的对象, 而不是重新创建一个对象.
其中的 javadoc 详细的说明了缓存支持 - 128 到 127 之间的自动装箱过程. 最大值 127 可以通过 - XX:AutoBoxCacheMax=size 修改.
实际上这个功能在 Java 5 中引入的时候, 范围是固定的 - 128 至 +127. 后来在 Java 6 中, 可以通过 java.lang.Integer.IntegerCache.high 设置最大值.
这使我们可以根据应用程序的实际情况灵活地调整来提高性能. 到底是什么原因选择这个 - 128 到 127 范围呢? 因为这个范围的数字是最被广泛使用的. 在程序中, 第一次使用 Integer 的时候也需要一定的额外时间来初始化这个缓存.
在 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 循环中有大量拆装箱操作, 会浪费很多资源.
来源: http://zhuanlan.51cto.com/art/201809/582835.htm