深入理解Java自动装箱和自动拆箱(反编译字节码理解每条指令)

本文涉及的产品
云原生网关 MSE Higress,422元/月
Serverless 应用引擎免费试用套餐包,4320000 CU,有效期3个月
性能测试 PTS,5000VUM额度
简介: 在Java中,自动装箱(Autoboxing)是指将基本数据类型(如int、char等)自动转换为其对应的包装类(如Integer、Character等)的过程。而自动拆箱(Unboxing)则是将包装类的对象转换回其对应的基本数据类型的操作。这些特性从Java SE 5开始被引入,以方便开发者在处理基本类型和其包装类之间进行转换。下面是一个简短的摘要:- **自动装箱**:当基本类型赋值给包装类时,例如 `Integer i = 1;`,Java会自动调用Integer的`valueOf()`方法,将int转换为Integer对象。对于数值在-128到127之间的int,会使用Int


💪🏻 制定明确可量化的目标,坚持默默的做事。


什么是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;

image.gif

1.2 自动拆箱

       自动将包装器类型转换为基本数据类型

Integer i = new Integer(100);
// 自动拆箱 相当于 int n = i.intValue();
int n = i;

image.gif

注:在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;
    }
}

image.gif

2.2 分析自动装箱和自动拆箱原理

2.2.1 编译反汇编

先编译,然后用javap -c,反编译查看字节码是什么样子

  • javap: javap是Java开发工具包(JDK)中的一个命令行工具,它可以反编译Java类文件并输出类的字节码
  • -c: 选项可以输出类的字节码指令

使用命令 javac AutoPack.java,得到AutoPack.class,如下图:

image.png

而想看懂AutoPack.class文件,可以用反编译工具(如jd-gui),也可以用javap命令查看。

javap命令如下:

image.gif 1711820839307.png

主要关注两个 -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"

image.gif

2.2.2 字节码指令说明

主要看main中的命令,指令说明:

  • bipush: 将常量66压入栈中。JVM根据取值范围不同,用不同的指令入栈,如下:
  • 取值 -1~5 采用 iconst 指令
  • 取值 -128~127 采用 bipush 指令
  • 取值 -32768~32767 采用 sipush 指令
  • 取值 -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是中如何执行的了。意思如下:

  1. Integer i = 66 是调Integer.valueOf(66)包装成包装类赋给i。即自动包装
  2. 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);
    }
}

image.gif

也许,我们都会说都是false,或者也有人说都是true。而事实结果是

true
false

image.gif

为什么?不都是调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);
    }
    ...
}

image.gif

说明:

  • 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);
    }
}

image.gif

注:跟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
    }
}

image.gif

  • //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

可以自己运行一下看输出结果。

相关文章
|
1月前
|
Java
轻松上手Java字节码编辑:IDEA插件VisualClassBytes全方位解析
本插件VisualClassBytes可修改class字节码,包括class信息、字段信息、内部类,常量池和方法等。
120 6
|
4月前
|
存储 Java 编译器
Java零基础(12) - 自动装箱
【8月更文挑战第12天】🏆本文收录于「滚雪球学Java」专栏,专业攻坚指数级提升,希望能够助你一臂之力,帮你早日登顶实现财富自由🚀;同时,欢迎大家关注&&收藏&&订阅!持续更新中,up!up!up!!
37 2
|
2月前
|
存储 SQL 小程序
JVM知识体系学习五:Java Runtime Data Area and JVM Instruction (java运行时数据区域和java指令(大约200多条,这里就将一些简单的指令和学习))
这篇文章详细介绍了Java虚拟机(JVM)的运行时数据区域和JVM指令集,包括程序计数器、虚拟机栈、本地方法栈、直接内存、方法区和堆,以及栈帧的组成部分和执行流程。
45 2
JVM知识体系学习五:Java Runtime Data Area and JVM Instruction (java运行时数据区域和java指令(大约200多条,这里就将一些简单的指令和学习))
|
1月前
|
Java 数据格式 索引
使用 Java 字节码工具检查类文件完整性的原理是什么
Java字节码工具通过解析和分析类文件的字节码,检查其结构和内容是否符合Java虚拟机规范,确保类文件的完整性和合法性,防止恶意代码或损坏的类文件影响程序运行。
51 5
|
1月前
|
Java API Maven
如何使用 Java 字节码工具检查类文件的完整性
本文介绍如何利用Java字节码工具来检测类文件的完整性和有效性,确保类文件未被篡改或损坏,适用于开发和维护阶段的代码质量控制。
102 5
|
2月前
|
Java Maven 数据安全/隐私保护
如何实现Java打包程序的加密代码混淆,避免被反编译?
【10月更文挑战第15天】如何实现Java打包程序的加密代码混淆,避免被反编译?
259 2
|
2月前
|
Java
如何从Java字节码角度分析问题|8月更文挑战
如何从Java字节码角度分析问题|8月更文挑战
|
3月前
|
Arthas Java 测试技术
Java字节码文件、组成,jclasslib插件、阿里arthas工具,Java注解
Java字节码文件、组成、详解、分析;常用工具,jclasslib插件、阿里arthas工具;如何定位线上问题;Java注解
Java字节码文件、组成,jclasslib插件、阿里arthas工具,Java注解
|
4月前
|
Java
【Java基础面试九】、说一说自动装箱、自动拆箱的应用场景
这篇文章介绍了Java中的自动装箱和自动拆箱概念:自动装箱允许将基本类型赋值给对应的包装类对象,而自动拆箱允许将包装类对象赋值给基本类型,从而简化了两者之间的转换过程。
【Java基础面试九】、说一说自动装箱、自动拆箱的应用场景
|
3月前
|
Java API 开发者
【Java字节码操控新篇章】JDK 22类文件API预览:解锁Java底层的无限可能!
【9月更文挑战第6天】JDK 22的类文件API为Java开发者们打开了一扇通往Java底层世界的大门。通过这个API,我们可以更加深入地理解Java程序的工作原理,实现更加灵活和强大的功能。虽然目前它还处于预览版阶段,但我们已经可以预见其在未来Java开发中的重要地位。让我们共同期待Java字节码操控新篇章的到来!