思维导图:
在 Java 中,自动装箱(autoboxing)和拆箱(unboxing)是一种自动将基本数据类型和对应的包装类进行转换的机制。
一:基本数据类型和对应的包装类
二:装箱和拆箱
int i = 10;
// 装箱操作,新建一个 Integer 类型对象,将 i 的值放入对象的某个属性中
Integer ii = Integer.valueOf(i);
Integer ij = new Integer(i);
// 拆箱操作,将 Integer 对象中的值取出,放到一个基本数据类型中
int j = ii.intValue();
2.1自动装箱和自动拆箱
通过上述代码可以看到在使用过程中,装箱和拆箱带来不少的代码量,为了减少开发者的负担,java 提供了自动机制。
int i = 10;
Integer ii = i; // 自动装箱
Integer ij = (Integer)i; // 自动装箱
int j = ii; // 自动拆箱
int k = (int)ii; // 自动拆箱
2.2自动装箱和自动拆箱的原理
1. 自动装箱:
• 当把一个基本数据类型的值赋给对应的包装类对象时,会自动进行装箱操作。例如,将int类型的值赋给Integer对象,Integer i = 10;,在这个过程中,Java 会自动创建一个Integer对象,并将基本数据类型int的值10包装到这个对象中。
•原理: 实际上,Java 编译器会在后台调用Integer.valueOf(int)方法来完成装箱操作。对于在一定范围内的值(通常是 -128 到 127),Java 会使用缓存的对象,以提高性能。
2. 自动拆箱:
• 当从包装类对象中获取基本数据类型的值时,会自动进行拆箱操作。例如,从Integer对象中获取int类型的值,int n = i;,这里i是一个Integer对象,Java 会自动将Integer对象中的值拆箱为基本数据类型int并赋给变量n。
• 原理:编译器会在后台调用包装类的xxxValue()方法来实现拆箱,对于Integer类,会调用intValue()方法。
原理图:
易错点:看如下代码
public static void main(String[] args) {
Integer a = 127;
Integer b = 127;
Integer c = 128;
Integer d = 128;
System.out.println(a == b); //true
System.out.println(c == d); //false 为什么????
}
在 Java 中,Integer对于值在 -128 到 127 之间会进行缓存,主要有以下原因:
一、提高性能
1. 频繁使用小范围整数:在很多程序中,小范围的整数(如 -128 到 127)被频繁使用。通过缓存这些值,可以避免频繁地创建新的Integer对象,减少内存分配和垃圾回收的开销,从而提高程序的性能。
2. 减少对象创建:当需要一个在这个范围内的Integer对象时,可以直接从缓存中获取,而不是通过调用构造函数创建新的对象。这样可以快速返回已经存在的对象,节省时间和资源。
二、优化内存使用
1. 重复利用对象:缓存机制使得相同的值在内存中只存在一个Integer对象,避免了多个相同值的对象占用额外的内存空间。特别是在处理大量小范围整数的场景下,可以显著减少内存的使用。
2. 空间效率:对于小范围的整数,通过缓存可以有效地管理内存,避免不必要的对象创建和内存浪费。
三、常见场景适用性
1. 一般业务逻辑:在很多常规的业务逻辑中,小范围的整数经常出现,例如循环计数器、数组索引等。缓存机制可以很好地适应这些常见的场景,提高程序的运行效率。
2. 与数据库交互:在与数据库交互时,一些数据库返回的整数字段值可能在小范围内。缓存机制可以在处理这些返回值时提高性能,减少对象创建和内存占用。
在 Java 中,除了Integer类,以下几个类也进行了类似的缓存优化:
一、Byte 类
缓存了-128到127之间的Byte对象。这是因为在实际应用中,小范围的字节值经常被使用,缓存可以提高性能并减少内存占用。
二、Short 类
同样缓存了-128到127之间的Short对象。对于小范围的短整型值,这种缓存机制可以避免不必要的对象创建。
三、Long 类
缓存了-128到127之间的Long对象。与其他整数类型类似,在处理小范围的长整型值时,缓存可以提高效率。
四、Character 类
缓存了0到127之间的Character对象,这个范围涵盖了 ASCII 字符集。因为 ASCII 字符在很多程序中被频繁使用,缓存这些字符对象可以提高性能。
三:应用场景
3.1集合框架的使用
• 在 Java 的集合框架中,如List、Set、Map等,只能存储对象类型,不能直接存储基本数据类型。这时自动装箱和拆箱就非常有用。
• 请看如下代码:
List<Integer> list = new ArrayList<>();
list.add(5);
说明:这里先将基本数据类型5自动装箱为Integer对象,然后添加到List中。当从集合中取出元素时,又会自动拆箱为基本数据类型。
• 在使用Map时也类似:
Map<String, Integer> map = new HashMap<>();
map.put("key", 10);
说明:这里将基本数据类型10装箱为Integer对象作为值存入Map中。
3.2方法参数和返回值
• 当一个方法的参数是包装类类型,而传入的是基本数据类型时,会自动进行装箱操作。同样,当方法的返回值是包装类类型,而实际返回的是基本数据类型时,会自动进行拆箱操作。
如下代码:
public Integer add(Integer a, Integer b) {
return a + b;
};
int result = add(3, 5);//调用add方法,接收返回值
说明:调用int result = add(3, 5);,这里3和5会自动装箱为Integer对象传入方法,方法返回的Integer对象又会自动拆箱为int类型赋给result变量。
3.3 泛型的使用
• 在泛型中,只能使用对象类型,不能使用基本数据类型。自动装箱和拆箱使得可以在泛型中使用基本数据类型的包装类,从而提供了更大的灵活性。
• 例如:
List<Integer> list = new ArrayList<>();
这里使用了泛型Integer,可以存储包装后的基本数据类型。
3.4条件判断和循环
• 在条件判断和循环中,经常会涉及到基本数据类型和包装类的转换。
• 例如:for (Integer i = 0;i < 10;i++) {... },这里的循环变量i是Integer类型,在每次循环中,会自动拆箱为基本数据类型进行比较和自增操作。
总结:自动装箱和拆箱机制使得 Java 代码在处理基本数据类型和包装类之间的转换更加简洁和方便,提高了代码的可读性和可维护性。
四:自动装箱和拆箱的缺陷:
4.1性能开销
4.1.1 频繁的自动装箱和拆箱操作可能会带来性能问题,尤其是在性能敏感的代码段中。每次装箱和拆箱都涉及到方法调用,对于大量的操作来说,这些方法调用的累积开销可能会变得显著。
4.1.2 创建对象的开销:自动装箱可能会创建新的对象,如果这些对象没有被及时回收,会占用内存空间,增加垃圾回收的压力。
4.2可能导致的错误
4.2.1 空指针异常:由于包装类可以为null,而基本数据类型不能为null。在自动拆箱时,如果一个包装类对象为null,进行自动拆箱操作就会导致空指针异常。
Integer num = null;
int n = num; // 此处会抛出空指针异常
4.2.2 不易察觉的错误:自动装箱和拆箱可能会掩盖一些类型转换的问题,使得错误难以在代码审查时被发现。例如,在一个复杂的表达式中,自动装箱和拆箱可能会导致意外的结果,但开发人员可能不容易察觉问题的根源。
4.3可读性的影响(在某些复杂情况下)
在一些非常复杂的代码中,自动装箱和拆箱可能会使代码的可读性降低。因为自动进行的类型转换可能会让读者难以确定实际发生的操作,需要仔细分析才能理解代码的意图。
五:如何避免自动装箱和拆箱的缺陷:
5.1性能开销方面
注意代码优化:
• 在性能关键的代码区域,尽量避免频繁的自动装箱和拆箱操作。例如,在大量循环或频繁调用的方法中,直接使用基本数据类型而不是依赖自动装箱。
• 对于集合操作,如果确定集合中存储的是基本数据类型的值,可以考虑使用基本数据类型的数组或者专门为基本数据类型设计的集合类,如java.util.IntSummaryStatistics等,避免自动装箱后放入普通集合中带来的性能开销。
合理使用缓存:
• 对于像Integer等包装类,了解其缓存机制的范围(通常是 -128 到 127)。如果在这个范围内操作,可以利用缓存减少对象创建。如果操作的值超出这个范围,要意识到可能会创建新的对象,谨慎评估性能影响。
5.2 可能导致的错误方面
空指针检查:
• 在进行自动拆箱之前,务必进行空指针检查,以避免空指针异常。可以使用条件判断语句来确保包装类对象不为null再进行拆箱操作。
Integer num = null;
if (num!= null) {
int n = num;
}
明确类型转换:
• 在代码中,对于可能涉及自动装箱和拆箱的地方,尽量明确地进行类型转换,以便更好地理解代码的行为和潜在风险。例如,使用intValue()方法进行显式的拆箱操作,而不是依赖自动拆箱。
Integer numObj = new Integer(10);
int num = numObj.intValue();
5.3可读性影响方面
清晰的代码结构:
• 在复杂的代码中,尽量使用清晰的变量命名和代码结构,以便更好地理解自动装箱和拆箱的发生位置和目的。避免在复杂表达式中过度依赖自动装箱和拆箱,以免使代码难以理解。
• 可以添加适当的注释来解释自动装箱和拆箱的目的和潜在风险,提高代码的可读性和可维护性。
单元测试:
• 编写全面的单元测试,覆盖可能涉及自动装箱和拆箱的代码路径。通过单元测试可以更容易地发现由于自动装箱和拆箱导致的错误,确保代码的正确性。