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

本文涉及的产品
服务治理 MSE Sentinel/OpenSergo,Agent数量 不受限
可观测可视化 Grafana 版,10个用户账号 1个月
简介: 在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

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

相关文章
|
6天前
|
安全 Java API
JDK 11中的动态类文件常量:探索Java字节码的灵活性与动态性
在JDK 11中,Java语言引入了一个新的特性,允许在运行时动态地修改类文件常量。这一特性为Java开发者提供了更大的灵活性,使他们能够根据需要在运行时更改类文件中的常量值。本文将深入探讨动态类文件常量的工作原理、优点、限制以及在实际项目中的应用。
57 11
|
1天前
|
算法 Java 编译器
从Java字节码到JIT编译器,深入理解Java虚拟机
Java虚拟机(JVM)是Java程序运行的关键。想深入理解Java虚拟机,我们需要了解Java字节码、类加载机制、垃圾回收算法、JIT编译器等方面的知识。本文将介绍这些关键知识点,并通过示例代码加深理解。
|
6天前
|
存储 安全 Java
深入理解Java字节码与反编译技术
深入理解Java字节码与反编译技术
20 0
|
6天前
|
XML 前端开发 Oracle
16:JSP简介、注释与Scriptlet、Page指令元素、Include操作、内置对象、四种属性-Java Web
16:JSP简介、注释与Scriptlet、Page指令元素、Include操作、内置对象、四种属性-Java Web
15 2
|
6天前
|
存储 缓存 安全
Java并发基础之互斥同步、非阻塞同步、指令重排与volatile
在Java中,多线程编程常常涉及到共享数据的访问,这时候就需要考虑线程安全问题。Java提供了多种机制来实现线程安全,其中包括互斥同步(Mutex Synchronization)、非阻塞同步(Non-blocking Synchronization)、以及volatile关键字等。 互斥同步(Mutex Synchronization) 互斥同步是一种基本的同步手段,它要求在任何时刻,只有一个线程可以执行某个方法或某个代码块,其他线程必须等待。Java中的synchronized关键字就是实现互斥同步的常用手段。当一个线程进入一个synchronized方法或代码块时,它需要先获得锁,如果
26 0
|
6天前
|
存储 Java 索引
《深入浅出Java虚拟机 — JVM原理与实战》带你攻克技术盲区,夯实底层基础 —— 吃透class字节码文件技术基底和实现原理(核心结构剖析)
《深入浅出Java虚拟机 — JVM原理与实战》带你攻克技术盲区,夯实底层基础 —— 吃透class字节码文件技术基底和实现原理(核心结构剖析)
36 0
|
6月前
|
安全 算法 Java
从零开发基于ASM字节码的Java代码混淆插件XHood
因在公司负责基础框架的开发设计,所以针对框架源代码的保护工作比较重视,之前也加入了一系列保护措施,例如自定义classloader加密保护,授权license保护等,但都是防君子不防小人,安全等级还比较低,经过调研各类加密混淆措施后,决定自研混淆插件,自主可控,能够贴合实际情况进行定制化,达到框架升级后使用零感知,零影响
78 1
从零开发基于ASM字节码的Java代码混淆插件XHood
|
6天前
|
缓存 监控 Java
Javaassist:编写字节码,改变Java的命运
Javaassist:编写字节码,改变Java的命运
34 0
|
6天前
|
Java
Idea如何方便的查看Java字节码文件😁
Idea如何方便的查看Java字节码文件😁
63 0
|
6天前
|
Java
Java【付诸实践 04】Jar包class文件反编译、修改、重新编译打包方法(含反编译工具jd-gui-windows-1.6.6.zip百度云资源)
Java【付诸实践 04】Jar包class文件反编译、修改、重新编译打包方法(含反编译工具jd-gui-windows-1.6.6.zip百度云资源)
210 0