写在前面
Java 为我们提供了 8 种基本数据类型,为什么还需要提供各自的包装类型呢?您可能会觉得这个问题问的很奇怪,但是我觉得还是值的思考的。
因为 Java 是一门面向对象的语言,基本数据类型并不具备对象的性质。而包装类型则是在基本类型的基础上,添加了属性和方法,从而成为了对象。试想,一个 int 类型怎么添加到 List 集合中?
如何理解Java中的自动拆箱和自动装箱?
自动拆箱?自动装箱?什么鬼,听都没听过啊,这...这..知识盲区...
回到家后赶紧查资料,我透,这不就是问基本类型跟封装类型吗,面试官整啥名词呢...
别问结果,问就是没过。
自动装箱、拆箱
定义: 基本数据类型和包装类之间可以自动地相互转换
理解: 装箱就是自动将基本数据类型转换为封装类型,拆箱就是自动将封装类型转换为基本数据类型。
我们来看个例子:
Integer number = 10; // 自动装箱 int count = number; // 自动拆箱 复制代码
我们知道,Java 中提供了四大类基本数据类型,分别是:整数、浮点数、字符型和布尔型,其中:
- 整数包含:byte、int、short、long
- 浮点数包含:float、double
- 字符类型:char
- 布尔类型:boolean
基本数据类型相信大家一定很熟悉了吧,来来来,说说他们的取值范围~
数据类型 | 取值范围 |
byte | -128 ~ 127 |
short | -32786 ~ 32767 |
int | -4294967296 ~ 4294967295 |
long | -264 ~ 264 -1 |
float | 3.4e-038 ~ 3.4e+038 |
dubbo | 1.7e-308 ~ 1.7e+308 |
char | \u0000 ~ \uffff |
boolean | true 、false |
日常开发中,靠这些基本数据类型几乎能够满足我们的需求,但是基本类型终究不是对象,往重了说不满足面向对象的开发思想,往轻了说就是使用不方便。怎么讲?例如做一些数据类型转换,获取 int 数据类型的取值范围等等。
我们知道,类的优点在于它可以定义成员变量、成员方法,提供丰富便利的功能,因此 Java 在 JDK1.0 的时候就设计了基本数据类型的包装类,而在 JDK1.5 中引入了新特性:自动装箱和拆箱。
我们来看一下基本类型跟封装类型之间的对应关系:
数据类型 | 封装类 |
byte | Byte |
short | Short |
int | Integer |
long | Long |
float | Float |
double | Double |
char | Character |
boolean | Boolean |
何时会发生拆箱、装箱?
自动装箱和拆箱在 Java 中很常见,比如我们有一个方法,接受一个对象类型的参数,如果我们传递一个原始类型值,那么 Java 会自动将这个原始类型值转换成与之对应的对象。最经典的一个场景就是当我们向 ArrayList 这样的容器中增加原始类型数据时或者是创建一个参数化的类,比如下面的 ThreadLocal。
ArrayList<Integer> intList = new ArrayList<Integer>(); intList.add(1); // 装箱 intList.add(2); // 装箱 ThreadLocal<Integer> intLocal = new ThreadLocal<Integer>(); intLocal.set(4); // 装箱 int number = intList.get(0); // 拆箱 int local = intLocal.get(); // 拆箱 复制代码
上面的示例我们介绍了自动装箱和拆箱以及它们何时发生,我们知道了自动装箱主要发生在两种情况,一种是赋值时,另一种是在方法调用的时候。
赋值时
这是最常见的一种情况,在 Java 1.5 以前我们需要手动地进行转换才行,而现在所有的转换都是由编译器来完成。
// before autoboxing Integer iObject = Integer.valueOf(3); int iPrimitive = iObject.intValue() //after java5 Integer iObject = 3; // autobxing - primitive to wrapper conversion int iPrimitive = iObject; // unboxing - object to primitive conversion 复制代码
方法调用时
这是另一个常用的情况,当我们在方法调用时,我们可以传入原始数据值或者对象,同样编译器会帮我们进行转换。
public static Integer show(Integer iParam){ System.out.println("autoboxing example - method invocation i: " + iParam); return iParam; } // autoboxing and unboxing in method invocation show(3); // autoboxing int result = show(3); // unboxing because return type of method is Integer 复制代码
show() 方法接受 Integer 对象作为参数,当调用 show(3) 时,会将 int 值转换成对应的 Integer 对象,这就是所谓的自动装箱,show() 方法返回 Integer 对象,而 int result = show(3); 中 result 为 int 类型,所以这时候发生自动拆箱操作,将 show() 方法返回的 Integer 对象转换成 int 值。
自动装拆箱的弊端
自动装箱有一个问题,那就是在一个循环中进行自动装箱操作的情况,如下面的例子就会创建多余的对象,影响程序的性能。
Integer sum = 0; for(int i=1000; i<5000; i++){ sum += i; } 复制代码
上面的代码 sum += i 可以看成 sum = sum + i,但是 + 这个操作符不适用于 Integer 对象,首先 sum 进行自动拆箱操作,然后进行数值相加操作,最后发生自动装箱操作转换成 Integer 对象。其内部变化如下:
int result = sum.intValue() + i; Integer sum = new Integer(result); 复制代码
由于我们这里声明的 sum 为 Integer 类型,在上面的循环中会创建将近 4000 个无用的 Integer 对象,在这样庞大的循环中,会降低程序的性能并且加重了垃圾回收的工作量。因此在我们编程时,需要注意到这一点,正确地声明变量类型,避免因为自动装箱引起的性能问题。
总结
因为自动装箱会隐式地创建对象,像前面提到的那样,如果在一个循环体中,会创建无用的中间对象,这样会增加 GC 压力,降低程序的性能。所以在写循环时一定要注意,避免引入不必要的自动装箱操作。
总的来说,自动装箱和拆箱着实为开发者带来了很大的方便,但是在使用时也是需要格外留意,避免引起出现文章提到的问题。