目录
为什么Java需要自动装箱和拆箱这一机制呢?
简单来说,就是有些历史原因的。
比如,泛型(Java5引入),Java中的泛型只能使用引用类型,不能使用基本数据类型。还有集合框架(Java2引入),Java 的集合框架(如 List、Set、Map 等)只能存储对象(引用类型),不能存储基本数据类型。因此为了API的兼容性,代码的灵活性和便利性,就有了自动装箱和拆箱机制。
下边一起来体会下这个机制。
1、Java 中的自动装箱
自动装箱是 Java 编译器在基本类型和它们对应的包装类之间进行的自动转换。例如,将 int 类型转换为 Integer 类型,将 double 类型转换为 Double 型等等。
如果转换以相反的方式进行,则称为拆箱。// 包装类和基本数据类型的相互转换
下面是一个最简单的自动装箱例子:
Character ch = 'a';
例如,下面的代码:
List<Integer> li = new ArrayList<>();
for (int i = 1; i < 50; i += 2){
li.add(i);
}
尽管将 int 值作为基本类型而不是 Integer 对象添加到 li 列表,但是代码仍然可以编译。这里 li 是一个 Integer 对象的列表,而不是 int 值的列表,为什么 Java 编译器不会发出编译时错误呢?
编译器之所以不会产生错误,是因为它从 i 创建了一个 Integer 对象,并将该对象添加到 li 列表。因此,编译器在运行时实际是将前面的代码转换为以下代码:
List<Integer> li = new ArrayList<>();
for (int i = 1; i < 50; i += 2){
li.add(Integer.valueOf(i)); //装箱
}
Integer.valueOf(i) 这段代码里有什么呢?//自动装箱
//源码版本:java17
public static Integer valueOf(int i) {
if (i >= IntegerCache.low && i <= IntegerCache.high)
return IntegerCache.cache[i + (-IntegerCache.low)];
return new Integer(i);
}
Java 在拆箱的过程中,创建了一个新的对象(new Integer),既然创建了额外的对象,就增加内存开销和垃圾回收的负担,这也是为什么要避免频繁进行大量的自动装箱和拆箱操作的原因。//自动装箱和拆箱涉及到创建和销毁额外的对象,即包装类对象
在以下情况下,Java 编译器会对基本类型的值进行自动装箱:
- 把基本类型的值作为参数传递给需要相应包装类的对象的方法。
- 把基本类型的值赋给相应包装类的变量。
2、Java 中的自动拆箱
接下来,看一下Java中的自动拆箱,例如下边的代码:
public static int sumEven(List<Integer> li) {
int sum = 0;
for (Integer i: li){
if (i % 2 == 0){
sum += i;
}
return sum;
}
}
求余运算符 (%) 和一元运算符 (+=) 并不适用于 Integer 对象,但 Java 编译器在编译该方法时却不会产生任何错误,因为在调用 intValue() 方法时,Java 会将 Integer 转换为 int:
public static int sumEven(List<Integer> li) {
int sum = 0;
for (Integer i : li)
if (i.intValue() % 2 == 0) //拆箱操作
sum += i.intValue();
return sum;
}
将包装类型(Integer)的对象转换为其对应的基本类型(int)称为拆箱。在以下情况下,Java 编译器会对包装类型的值进行自动拆箱:
- 把包装类型的值作为参数传递给需要相应基本类型的值的方法。
- 把包装类型的值赋值给相应基本类型的变量。
下边的例子展示了拆箱是如何工作的:
import java.util.ArrayList;
import java.util.List;
public class Unboxing {
public static void main(String[] args) {
Integer i = new Integer(-8);
// 1. 通过方法调用进行拆箱
int absVal = absoluteValue(i);
System.out.println("absolute value of " + i + " = " + absVal);
List<Double> ld = new ArrayList<>();
ld.add(3.1416); // Π 通过方法调用自动装箱。
// 2. 通过赋值进行拆箱
double pi = ld.get(0);
System.out.println("pi = " + pi);
}
public static int absoluteValue(int i) {
return (i < 0) ? -i : i;
}
}
该程序会打印以下内容:
absolute value of -8 = 8
pi = 3.1416
3、自动装箱和拆箱总结
自动装箱和拆箱可以让开发人员编写更加清晰的代码,使代码更加易于阅读。下表列出了基本类型及其对应的包装类,Java 编译器会使用它们进行自动的装箱和拆箱:
基本类型 | 包装类型 |
boolean | Boolean |
byte | Byte |
char | Character |
float | Float |
int | Integer |
long | Long |
short | Short |
double | Double |
自动装箱和拆箱的实现是通过 Java 编译器在编译时进行的。具体来说,Java 编译器会将自动装箱和拆箱操作转换为对应的方法调用,以实现基本数据类型和包装类型之间的转换。
但这在性能方面是要付出代价的。装箱后的值本质上就是把原始类型包裹起来,并保存在堆里。因此,装箱后的值需要更多的内存,并需要额外的内存搜索来获取被包裹的原始值。//所以,应该尽量避免不必要的装箱
4、在 Java 中怎样避免自动装箱和拆箱?
就一条原则:尽量使用基本数据类型
尽可能地使用基本数据类型,而不是对应的包装类型。基本数据类型的数据存储在栈中,而包装类型的对象存储在堆中,因此基本数据类型的操作比包装类型的操作更加高效。
至此,全文结束。