likes
comments
collection
share

自动装箱和拆箱的原理

作者站长头像
站长
· 阅读数 18

我们在面试中经常会被问到什么是自动装箱和拆箱,今天,我就通过代码来讲解一下自动装箱背后的原理

学过Java的都知道,自动装箱呢,就是将基本数据类型自动转换成Integer、Character、Long、Double、Boolean这些包装器类型,那么它们背后的原理是什么呢?我们用下面这些代码示例来做讲解:

首先来看这个例子:

public class Main {

    public static void main(String[] args) throws IOException {
        
        Integer a = 10;
        Integer b = 10;
        Integer c = 200;
        Integer d = 200;

        System.out.println(a == b);
        System.out.println(c == d);

    }
}

我们可以看到输出的结果:

自动装箱和拆箱的原理

一个是true,一个是false,是不是很奇怪呢?明明两个对象指代的值是相同的,为什么最后的结果不同呢?

这里我们就可以引出来自动装箱的原理了~

如果我们对这些代码进行反编译的话,我们就会发现,编译器在底层自动调用了Integer类的valueOf()方法,那么什么是valueOf()方法呢?我们来点进去看看源码:

@HotSpotIntrinsicCandidate
public static Integer valueOf(int i) {
    return i >= -128 && i <= Integer.IntegerCache.high ? Integer.IntegerCache.cache[i + 128] : new Integer(i);
}

这段代码是什么意思呢?

简单来说:就是,在Integer类中本身其实存在一个cache机制,它本身是一个常量,存在于Java的方法区中,当我们给定的整数在[-128,127]之间的话,那么会直接从cache中返回对应的Integer类的引用(或者说地址),如果整数的大小不在这个范围内的话,那么valueof()方法会新new一个Integer类型的对象,并返回。

因此,200大于127,所以每一次返回的包装类型的地址都是不一样的,我们分析出结果原因了~

同样地,我们来看看Double和Boolean类型的自动装箱的背后原理

我们用类似的例子:

public class Main {

    public static void main(String[] args) throws IOException {

        Double a = 1.0;
        Double b = 1.0;
        Double c = 2.0;
        Double d = 2.0;

        System.out.println(a == b);
        System.out.println(c == d);


    }
}

运行之后,我们可以看到:

两个返回的都是false,我们同样去查看Double的源码:

@HotSpotIntrinsicCandidate
public static Double valueOf(double d) {
    return new Double(d);
}

我们可以看到,每一次valueof都会new一个包装类对象,因此每一次对象的引用地址都是新的,所以两个进行比较的话,是不同的

接下来就是Boolean类型:

public class Main {

    public static void main(String[] args) throws IOException {

        Boolean a = true;
        Boolean b = true;
        Boolean c = false;
        Boolean d = false;

        System.out.println(a == b);
        System.out.println(c == d);

    }
}

话不多说:上结果

自动装箱和拆箱的原理

可以看到,用Boolean包装类型,返回的结果居然都是true,惯例,去看valueof方法

@HotSpotIntrinsicCandidate
public static Boolean valueOf(boolean b) {
    return b ? TRUE : FALSE;
}

我们在源码中发现了两个变量TRUE和FALSE,这两个是个什么东西呢?我们往上面找

    public static final Boolean TRUE = new Boolean(true);
    public static final Boolean FALSE = new Boolean(false);

原来是两个final关键字修饰的静态变量,这样说的话,那么这两个变量的地址都是不变的,也就是说,我们valueof里面return的对象的引用都是相同的~

剩下的Character、Short、Long、Byte基本上和Integer的valueof是类似的,Double、Float的是类似的,Boolean的valueof是单独一组的

以上就是自动装箱的原理了~

接下来我们就讲讲什么是自动拆箱,同样用代码来做讲解,看以下的示例:

public class Main {

    public static void main(String[] args) throws IOException {
        
        Integer a = 1;
        Integer b = 2;
        Integer c = 3;
        Long g = 3L;
        int int1 = 12;
        int int2 = 12;
        Integer integer1 = new Integer(12);
        Integer integer2 = new Integer(12);
        Integer integer3 = new Integer(1);
        System.out.println("c==(a+b)"+(c == (a+b)));
        System.out.println("g==(a+b)"+(g == (a+b)));
        System.out.println("c.equals(a+b)"+c.equals(a+b));
        System.out.println("g.equals(a+b)"+g.equals(a+b));
        System.out.println("int1 == int2"+(int1 == int2));
        System.out.println("int1 == integer1"+(int1 == integer1));
        System.out.println("integer1 == integer2"+(integer1 == integer2));
        System.out.println("integer3 == a"+(integer3 == a));
    }
}

在这里我们先铺垫一下==和equals的区别

对于==来说,又分为了基本数据类型和引用数据类型(包括包装类)还有拆箱(这个等会说)

  • 基本数据类型:比较的是值是否形同

  • 引用数据类型:比较的是引用是否相同

对于equals来说,也有两种情况,一种是默认情况,还有一种是重写的情况

  • 默认情况:比较的是引用是否相同

  • 重写情况:有些类在内部对Object类的equals方法进行了重写,比如String类和Integer类,都对该方法进行了重写,因此这种情况下比较的就是值是否相同

我们进到Integer的equals看一看:

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

会发现内部使用的是用==进行比较,这里==号两边一个是基本数据类型,一个是Integer包装类型,因此会进行一个自动拆箱的过程,编译器会调用Integer的intValue()方法,然后比较两个的值,所以这里的equals比较的就是值

其余和Integer同类型的包装类内部有各自对equals方法的重写方式,例如Long:

public boolean equals(Object obj) {
   if (obj instanceof Long) {
       return this.value == (Long)obj;
   } else {
       return false;
   }
}

铺垫完了==和equals的不同,我们就可以继续来分析自动拆箱了~

  1. c == (a+b):这里,== 左边是一个包装类型,右边是两个包装类型之和,我们之前说==如果左右两边是包装类型,那么比较的是引用,但是这里很特殊啊,这里右边是一个操作数表达式,因此这里比较的就是值了

  2. g == (a+b):同样的,这里==右边的是一个操作数表达式:因此呢,比较的是值,所以返回true

  3. c.equals(a+b):这里用到了equals,equals又比较特殊了,我们说过,Integer和String是重写了equals的,内部是使用了intValue转换成int基本数据类型的,因此比较的是值,而这里由于存在操作数表达式,因此会触发一个自动拆箱和装箱的过程,a和b各自先拆成int类型进行求和,然后编译器会再调用valueof,对求和后的结果进行装箱,然后再回到equals内部比较,由于我们的c是一个Integer类型,我们根据上面对Integer源码的分析可以看到,它内部的equals会判断这个传入进来的类型是否属于Integer,如果也是Integer类型的话,那么就比较值了,所以最后的结果就是true

  4. g.equals(a+b):这个和上面差不多,也会触发自动拆箱和装箱的过程,但是呢,这里的g是一个Long包装类型,它的内部也重写了equals,不过它判断的是,传入进来的参数是否是Long类型,如果不是Long包装类型,那么直接就返回false了

  5. int1 == int2:这两个都是基本数据类型,那么比较的就是值了,没什么好说的

  6. int1 == integer1:这里==左边的是基本数据类型,右边的是包装类型,这个时候,会触发编译器的自动拆箱,调用Integer的intValue方法,因此比较的是值,返回的是true

  7. integer1 == integer2:我们在上面分析==和equals的区别的时候讲过,当==左右两边都是包装类型的时候,比较的就是引用,由于这里的integer1和integer2都是new出来的对象,两个的地址肯定是不同的,所以返回的就是false

  8. integer3 == a:这里integer3是一个new出来的对象,而a是一个存在于Integer包装类型cache中的常量,他们两个在内存中的位置是不一样的,因此返回false

以上就是关于自动装箱和自动拆箱的原理的解析了~

下一节,我们就来讲讲什么是Java的方法区以及Java的内存模型