专栏原创出处:github-源笔记文件 (opens new window)github-源码 (opens new window),欢迎 Star,转载请附上原文出处链接和本声明。

Java 核心知识专栏系列笔记,系统性学习可访问个人复盘笔记-技术博客 Java 核心知识 (opens new window)

# 一、为什么要自动装箱拆箱

基础数据类型不是对象,或者说基础数据类型不是 Object 的子类。所以不支持基础数据类型与 Object 之间的强制转型。

JDK 5 后出现了泛型,但是泛型类型在编译时被擦除了,在需要的时候进行强制转换操作,但是基础数据类型与 Object 之间无法强制转型,所以引入了基本数据类型的包装类,Integer、Long...等。

如果不支持自动装箱拆箱,我们平时写代码就需要 Integer i = new Integer(10);,比较繁琐,因此出现了语法糖自动装箱拆箱。

# 二、自动装箱拆箱实现

  • 装箱过程是通过调用包装器的 valueOf 方法实现的
  • 拆箱是通过调用包装器的 xxxValue 方法实现的。(xxx 代表对应的基本数据类型)

编译器在编译期间根据条件判断自动插入了装箱拆箱的字节码。

# 三、什么时候装箱

  1. 在基本类型的值赋值给包装类型时触发
Integer a = 1;
// 最终字节码调用 Integer a = Integer.valueOf(1);
  1. 包装器 equals(Object o) 方法,传入的基本类型自动装箱
Integer a = 1;
a.equals(1) // 最终字节码调用 a.equals(Integer.valueOf(1));

包装器的 equals 方法先判断对象类型是否一致,然后拆箱后用基础数据类型比较。比如下面的 Integer.equals 源码

public boolean equals(Object obj) {
    if (obj instanceof Integer) {
        return value == ((Integer)obj).intValue();
    }
    return false;
}

比如:Long 与 Integer 比较时,就算数值相等,但是对象类型不相等时直接返回 false。

# 四、什么时候拆箱

  1. 包装类型赋值给基本类型时触发
Integer a = 1;  // a 是 Integer 类型
int b = a;  // 将 Integer 类型赋值给 int 类型,触发拆箱
  1. 运算符运算时触发
Integer a = 1; 
Integer b = 2; 
System.out.print(a * b); //这时 a*b 在编译成 a.intValue() * b.intValue();
  1. == 运算时,两边类型一个为包装类一个为基本类型时触发

比如:

如果比较的类型都是 Integer 类型,则不会拆箱,因为 == 也可以直接比较对象。

如果比较的类型一个是 int 类型,另一个是 Integer 类型,则会触发拆箱,然后对两个 int 值进行比较。

Integer b = 321;
int c = 321;

System.out.println(a == c); // a 拆箱为 int 值进行比较(Integer.intValue)

# 四、自动装箱拆箱的字节码分析

        Integer a = 321;
        Integer b = 321;
        int c = 321;

--------------- 赋值过程关键字节码 ------------
 0 sipush 321
 3 invokestatic #2 <java/lang/Integer.valueOf> Integer a = 321; 装箱

 7 sipush 321
10 invokestatic #2 <java/lang/Integer.valueOf> Integer b = 321; 装箱

14 sipush 321
17 istore_3        // int c = 321; 直接保存至局部变量表,无需其他操作

--------------- System.out.println(a == b); == 运算时判断类型一致性------------
21 aload_1          // 加载 a
22 aload_2          // 加载 b
23 if_acmpne 30 (+7) // 比较栈顶两引用型数值, 当结果不相等时跳转

--------------- System.out.println(a * b); 运算符运算时触发------------
37 aload_1          // 加载 a
38 invokevirtual #5 // a * b 运算中的 a 拆箱(Integer.intValue)
41 aload_2          // 加载 b
42 invokevirtual #5 // a * b 运算中的 b 拆箱(Integer.intValue)
45 imul             // 将栈顶两 int 型数值相乘并将结果压入栈顶

--------------- System.out.println(a == c); == 运算时判断类型不一致时拆箱------------
52 aload_1          // 加载 a
53 invokevirtual #5 // a == c 运算中的 a 拆箱(Integer.intValue)
56 iload_3          // 加载 c
57 if_icmpne 64 (+7)// 比较栈顶两 int 型数值大小, 当结果不等于 0 时跳转

--------------- System.out.println(a * c); 运算符运算时触发------------
71 aload_1          // 加载 a
72 invokevirtual #5 // a * c 运算中的 a 拆箱(Integer.intValue)
75 iload_3          // 加载 c
76 imul             // 将栈顶两 int 型数值相乘并将结果压入栈顶

--------------- int d = a; 包装类型赋值给基本类型时触发------------
80 aload_1
81 invokevirtual #5 // int d = a; 中的 a 拆箱(Integer.intValue)
84 istore 4

# 五、包装类型的缓存

为了减少对象创建。 CharacterCache/ByteCache/IntegerCache/LongCache/ShortCache (除了 Float、Double)分别缓存了他们对于包装类 -128~127 之间的对象数据。

因此在使用 == 比较这些对象时会返回 true。

# 六、通过字节码分析跨数据类型比较过程

示例代码如下:

Integer a1 = 3;
long b1 = 3;

boolean b = (a1 == b1);

关键字节码如下,我们可以发现在过程中 a1 进行了装箱后拆箱操作(性能问题)。 对于不同数据类型的比较采用向宽数据范围强转后比较。

    Code:
         0: iconst_3            // 将 3 压入栈顶
         1: invokestatic  #2    // 装箱 Integer.valueOf(3)
         4: astore_1            // 放入槽位 1 
         5: ldc2_w        #3    // 将 long b1 = 3 常量值从常量池中推送至栈顶
         8: lstore_2            // 将 long b1 = 3 常量值放入槽位 2
         9: aload_1             // 加载 Integer = 3 的引用
        10: invokevirtual #5    // 拆箱 ,Integer.intValue
        13: i2l                 // 将栈顶 int 型数值 3 强制转换为 long 型数值并将结果压入栈顶
        14: lload_2             // 将 long b1 = 3 压入栈顶
        15: lcmp                // 比较栈顶两 long 型数值大小, 并将结果 (1, 0 或-1) 压入栈顶
        16: ifne          23    // 当栈顶 int 型数值不等于 0 时跳转
      LocalVariableTable:
        Start  Length  Slot  Name   Signature
            0      27     0  args   [Ljava/lang/String;
            5      22     1    a1   Ljava/lang/Integer;
            9      18     2    b1   J
           26       1     4     b   Z

# 总结

  • 为了让基础数据类型能与 Object 进行强转引入了包装类型

  • 基础类型与包装类型的转化可以是隐式的(自动有编译期间字节码完成)

  • 什么时候装箱?

    • 在基本类型赋值给包装类型时触发
    • 包装器 equals 方法,传入的基本类型自动装箱(equals 先判断对象类型,在拆箱为基本数据类型比较)
  • 什么时候拆箱?

    • 包装类型赋值给基本类型时触发
    • 运算符运算时触发
    • == 运算时,两边类型一个为包装类一个为基本类型时触发
  • 包装类除了 Float、Double ,其余包装类缓存了 -128~127 之间的对象,== 比较时返回 true。

  • 不同数据类型比较时,字节码会默认向宽精度转化,然后进行比较

尝试解答:

public static void main(String[] args) {
        Integer a = 1;
        Integer b = 2;
        Integer c = 3;

        Integer d = 3;
        Integer e = 321;
        Integer f = 321;
        Long g = 3L;

        System.out.println(c == d);
        System.out.println(c.equals(d));

        System.out.println(e == f);
        System.out.println(e.equals(f));

        System.out.println(c == (a + b));
        System.out.println(c.equals(a + b));
        System.out.println(g == (a + b));
        System.out.println(g.equals(a + b));

        Integer a1 = 3;
        long b1 = 3;

        System.out.println(a1 == b1);
    }

# 专栏更多文章笔记

最后修改时间: 2/17/2020, 4:43:04 AM