💪🏻 制定明确可量化的目标,坚持默默的做事。
什么是Java自动装箱?
什么是Java自动拆箱?
一、基本概念
Java基本数据类型和其对应的包装类
bit(1字节) | Byte |
char(2字节) | Character |
short(2字节) | Short |
int(4字节) | Integer |
float(4字节) | Float |
double(8字节) | Double |
long(8字节) | Long |
boolean() | Boolean |
1.1 自动装箱
自动将基本数据类型转换为包装器类型
// 自动装箱 相当于 Integer a = Integer.valueOf(66); Integer a = 66;
1.2 自动拆箱
自动将包装器类型转换为基本数据类型
Integer i = new Integer(100); // 自动拆箱 相当于 int n = i.intValue(); int n = i;
注:在jdk1.5之前是无自动装箱的,如果要创建一个包装类型整型只能这样 Integer i = new Integer(9)来创建。
二、深入字节码理解如何实现自动装箱和自动拆箱
2.1 示例代码
写一段简单的代码,然后看看二进制码是什么样子
/** * 自动装箱和自去拆箱 <br/> * * @author yubaba * @date 2023/7/15 08:53:47 */ public class AutoPack { public static void main(String[] args) { Integer i = 66; int n = i; } }
2.2 分析自动装箱和自动拆箱原理
2.2.1 编译反汇编
先编译,然后用javap -c,反编译查看字节码是什么样子
- javap:
javap
是Java开发工具包(JDK)中的一个命令行工具,它可以反编译Java类文件并输出类的字节码 - -c: 选项可以输出类的字节码指令
使用命令 javac AutoPack.java,得到AutoPack.class,如下图:
而想看懂AutoPack.class文件,可以用反编译工具(如jd-gui),也可以用javap命令查看。
javap命令如下:
主要关注两个 -v 和 -c
- -v: 是对代码进行反汇编同时输出更多附加信息(有常量池信息、内存分配等信息)
- -c: 是对代码进行反汇编
用命令javap -v AutoPack.class,输下如下:
Classfile /Users/changx/Documents/ws/test/AutoPack.class Last modified 2023-7-15; size 375 bytes MD5 checksum 2d1985a02dfdf427efd0659a02bfb263 Compiled from "AutoPack.java" public class AutoPack minor version: 0 major version: 52 flags: ACC_PUBLIC, ACC_SUPER Constant pool: #1 = Methodref #5.#14 // java/lang/Object."<init>":()V #2 = Methodref #15.#16 // java/lang/Integer.valueOf:(I)Ljava/lang/Integer; #3 = Methodref #15.#17 // java/lang/Integer.intValue:()I #4 = Class #18 // AutoPack #5 = Class #19 // java/lang/Object #6 = Utf8 <init> #7 = Utf8 ()V #8 = Utf8 Code #9 = Utf8 LineNumberTable #10 = Utf8 main #11 = Utf8 ([Ljava/lang/String;)V #12 = Utf8 SourceFile #13 = Utf8 AutoPack.java #14 = NameAndType #6:#7 // "<init>":()V #15 = Class #20 // java/lang/Integer #16 = NameAndType #21:#22 // valueOf:(I)Ljava/lang/Integer; #17 = NameAndType #23:#24 // intValue:()I #18 = Utf8 AutoPack #19 = Utf8 java/lang/Object #20 = Utf8 java/lang/Integer #21 = Utf8 valueOf #22 = Utf8 (I)Ljava/lang/Integer; #23 = Utf8 intValue #24 = Utf8 ()I { public AutoPack(); descriptor: ()V flags: ACC_PUBLIC Code: stack=1, locals=1, args_size=1 0: aload_0 1: invokespecial #1 // Method java/lang/Object."<init>":()V 4: return LineNumberTable: line 7: 0 public static void main(java.lang.String[]); descriptor: ([Ljava/lang/String;)V flags: ACC_PUBLIC, ACC_STATIC Code: stack=1, locals=3, args_size=1 0: bipush 66 2: invokestatic #2 // Method java/lang/Integer.valueOf:(I)Ljava/lang/Integer; 5: astore_1 6: aload_1 7: invokevirtual #3 // Method java/lang/Integer.intValue:()I 10: istore_2 11: return LineNumberTable: line 10: 0 line 11: 6 line 12: 11 } SourceFile: "AutoPack.java"
2.2.2 字节码指令说明
主要看main中的命令,指令说明:
- bipush: 将常量66压入栈中。JVM根据取值范围不同,用不同的指令入栈,如下:
- 取值 -1~5 采用
iconst
指令 - 取值 -128~127 采用
bipush
指令 - 取值 -32768~32767 采用 s
ipush
指令 - 取值 -2147483648~2147483647 采用 ldc 指令
- invokestatic: 是调用静态方法。调用的静态方法是 #2,#2指Constant pool中的#2
- #2 Methodref是方法引用 是#15.#16,即Integer.valueOf()方法
- #15是Class是类 Integer
- #16是valueOf()
- 即:invokestatic是调Integer.valueOf方法。
- astore_1: 将栈顶元素赋值给本地变量表的1位置上。
- aload_1: 把存放在局部变量表中索引1位置的对象引用压入操作栈。
- invokevirtual: 调用非私有实例方法。调用方法是#3,#3是Constant pool中的#3
- #3 Methodref是方法引用 是#15.#17,即Integer.intValue()方法
- #15是Class是类 Integer
- #17是intValue()
- 即:invokevirtual是调Integer.intValue()方法
- astore_2: 将栈顶元素赋值给本地变量表的2位置上。
2.2.3 结论
了解了反汇编指命的意思,大概应该就知道源码AutoPack.java在JVM是中如何执行的了。意思如下:
- Integer i = 66 是调Integer.valueOf(66)包装成包装类赋给i。即自动包装
- int n = i 是调Integer.intValue()方法即,i.intValue()方法拆箱成基本数据类型,赋值给n。即自动拆箱
三、面试中的问题解析
3.1 以下代码输出结果是什么?
3.1.1 问题与答案
public class Main { public static void main(String[] args) { Integer i1 = 66; Integer i2 = 66; Integer i3 = 166; Integer i4 = 166; System.out.println(i1==i2); System.out.println(i3==i4); } }
也许,我们都会说都是false,或者也有人说都是true。而事实结果是
true false
为什么?不都是调Integer.valueOf()返回Integer对象么?。。
3.1.2 分析
要准确回答正确这个问题,我们看看Integer.valueOf()方法是如何实现的,源码如下:
// jdk 1.8 public final class Integer extends Number implements Comparable<Integer> { ... private static class IntegerCache { static final int low = -128; static final int high; static final Integer cache[]; static { int h = 127; String integerCacheHighPropValue = sun.misc.VM.getSavedProperty("java.lang.Integer.IntegerCache.high"); if (integerCacheHighPropValue != null) { try { int i = parseInt(integerCacheHighPropValue); i = Math.max(i, 127); h = Math.min(i, Integer.MAX_VALUE - (-low) -1); } catch( NumberFormatException nfe) { // If the property cannot be parsed into an int, ignore it. } } high = h; cache = new Integer[(high - low) + 1]; int j = low; for(int k = 0; k < cache.length; k++) cache[k] = new Integer(j++); // range [-128, 127] must be interned (JLS7 5.1.7) assert IntegerCache.high >= 127; } private IntegerCache() {} } ... public static Integer valueOf(int i) { if (i >= Integer.IntegerCache.low && i <= Integer.IntegerCache.high) return Integer.IntegerCache.cache[i + (-Integer.IntegerCache.low)]; return new Integer(i); } ... }
说明:
- IntegerCache内部静态类
- 最小值low为-128
- 最大值high默认的最小值为127
- valueOf(int i)方法的参数i 在 [IntegerCache.low, IntegerCache.high] 区间时,从缓存中取对象。
3.1.3 结论
- Integer i1 = 66 是从缓存 IntegerCache.cache中取
- Integer i2 = 66 也是从缓存 IntegerCache.cache中取,所以 i1 和 i2 指同一个对象,i1 == i2 为 true
- Integer i3 = 166,(IntegerCache.high默认为127)166 > 127,所有 new Integer(166)给i3
- Integer i4 = 166,(IntegerCache.high默认为127)166 > 127,也是 new Integer(166)给i4,所以 i3 和 i4 指不同的对象,所以 i3 == i4 为 false
3.2 以下代码输出结果是什么?
public class Main { public static void main(String[] args) { Double d1 = 66.0; Double d2 = 66.0; Double d3 = 166.0; Double d4 = 166.0; System.out.println(i1==i2); System.out.println(i3==i4); } }
注:跟3.1思路 一样。其它数据类型也是如此。
3.3 以下代码输出结果是什么?
public class Main { public static void main(String[] args) { Integer a = 2; Integer b = 4; Integer c = 6; Integer d = 6; Integer e = 166; Integer f = 166; Long g = 6L; Long h = 4L; System.out.println(c==d); //1 System.out.println(e==f); //2 System.out.println(c==(a+b)); //3 System.out.println(c.equals(a+b));//4 System.out.println(g==(a+b)); //5 System.out.println(g.equals(a+b));//6 System.out.println(g.equals(a+h));//7 } }
- //1 和 //2 应该没有什么疑问。true,false
- //3 有算术运算,会自动拆箱(调 intValue()方法),因此是比较数据值。true
- //4 有算术运算,有对象比较。a + b 调intValue自动拆箱成基本数据类型相加,得运算后的数值再调用Integer.valueOf方法,再进行equals比较。true
- //5 跟 //3相同,是数值比较。true
- //6 跟 //4相似 (假设m为 a + b后的包装对象),最后是 Long的g.equals(m),而g.equals方法是先判断 m instanceof Long是否为真,所以是 false
- //7 a + h是long和int相加,为防止溢出返回的是long值 < 127(Long的缓存区间是-128-127),再执行equals比较,所以是 true
可以自己运行一下看输出结果。