likes
comments
collection
share

解锁Java自动拆装箱的神秘面纱【字节码级别深度解析】

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

什么是自动拆装箱

JDK 1.5开始增加了自动拆装箱机制,Java保留了一些原始的基本数据类型,但由于Java是强面向对象语言,数据类型当然也应该设置成对象才是,所以Java也推出了对于基本数据类型的对应的对象,将基本数据类型转换为对象就称为装箱,反之则是拆箱

八种基本数据类型

基本数据类型对象类型
byteByte
shortShort
intInteger
longLong
floatFloat
doubleDouble
charCharacter
booleanBoolean

Java的大部分字节码指令都没有支持类型byte、char和short,甚至没有任何指令支持boolean类型。

那么Java底层是如何实现这些数据类型的?

事实上编译器会在编译期或运行期搞事

  • 将byte和short类型的数据带符号扩展(Sign-Extend)为相应的int类型数据。
  • 将boolean和char类型数据零位扩展(Zero-Extend)为相应的int类型数据。

在处理boolean、byte、short和char类型的数组时,也会转换为使用对应的int类型的字节码指令来处理。因此,大多数对于boolean、byte、short和char类型数据的操作,实际上都是使用相应的对int类型作为运算类型来进行的

例如,下面代码每一行代码上面的注释为其部分字节码指令

public class Test {
    /*
     *   以下均为整型入栈指令
     *   iconst_1 (-1到5的数据使用iconst指令,-1使用iconst_m1指令)
     *   bipush 100 (-128到127[一个字节]的数据使用bipush指令)
     *   sipush 1000 (-32768到32767[二个字节]的数据使用sipush指令)
     *   ldc 40000 (-2147483648到2147483647[四个字节]的数据使用ldc指令)
     */
    public static void main(String[] args) {
        // bipush 100【将整型数据100压入栈】
        byte b1 = 100;
        // bipush 101【将整型数据101压入栈】
        short s1 = 101;
        // bipush 49【将整型数据49压入栈】
        char c1 = '1';
        // iconst_1【将整型数据1压入栈】
        boolean bl1 = false;
    }
}

自动拆装箱原理

byte与Byte

public class Test {
    public static void main(String[] args) {
        Byte num = 1;
        byte num3 = num;
    }
}

编译后代码

public class Test {
    public static void main(String[] args) {
        Byte num = Byte.valueOf(1);
        byte num3 = num.byteValue();
    }
}

从编译后代码可以看出,这两者的转换用了Byte类中的方法,看一下这两个方法

    // 静态内部类
	private static class ByteCache {
        private ByteCache(){}

        static final Byte cache[] = new Byte[-(-128) + 127 + 1];

        static {
            for(int i = 0; i < cache.length; i++)
                cache[i] = new Byte((byte)(i - 128));
        }
    }
	
	private final byte value;

    public static Byte valueOf(byte b) {
        // 数组是不存在负的下标的,所以加上偏移量
        final int offset = 128;
        return ByteCache.cache[(int)b + offset];
    }
	
	public Byte(byte value) {
        this.value = value;
    }

	public byte byteValue() {
        return value;
    }

Byte类在使用的时候就会将[-128,127]范围的Byte对象缓存进类中,这是用了享元模式的思想存取常用的热点Byte对象,重复利用该范围类的Byte对象,也就是说在这个数值范围的Byte对象一直是同一个对象

short与Short、long与Long、char与Character的转换原理和这个相同

(char保存的是ASCII码,所以只缓存[0,127]的对象)

int与Integer

public class Test {
    public static void main(String[] args) {
        Integer num = 1;
        Integer num2 = 200;
        int num3 = num;
    }
}

编译后代码

public class Test {
    public static void main(String[] args) {
        Integer num = Integer.valueOf(1);
        Integer num2 = Integer.valueOf(200);
        int num3 = num.intValue();
    }
}

我们可以看出自动拆装箱事实上是调用了Integer类中的方法,来看一下这两个方法,事实上和byte类似,默认[-128,127]的Integer对象被缓存了,但是IntegerCache.high是可能被人为配置过的,如果配置的缓存上限值大于127就会缓存[-128,配置的上限值],在此范围内直接返回缓存的对象,否则就new一个新的Integer对象

	public static Integer valueOf(int i) {
        // IntegerCache.low = -128,IntegerCache.high = 127(默认)
        if (i >= IntegerCache.low && i <= IntegerCache.high)
            return IntegerCache.cache[i + (-IntegerCache.low)];
        return new Integer(i);
    }

boolean与Boolean

同样是这两个方法,由于布尔型只有两个值,所以直接缓存两个对象

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

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

    private final boolean value;

    public Boolean(boolean value) {
        this.value = value;
    }

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

    public boolean booleanValue() {
        return value;
    }

float与Float以及double与Double

由于浮点数整数位相同时小数位可以有多种情况,比如整数位为1的有1.321332,1.543635,..................,数量太多所以没有某一个浮点数会被反复使用,所以没必要缓存,直接返回一个Float新对象

    public static Float valueOf(float f) {
        return new Float(f);
    }

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

测试

我们测试一下是否返回的是同一个对象

public class Test {
    public static void main(String[] args) {
        Integer b1 = 10;
        Integer b2 = 10;
        Integer b3 = new Integer(10);
        Integer b4 = new Integer(10);
        System.out.println(b1 == b2); // true
        System.out.println(b2 == b3); // false
        System.out.println(b3 == b4); // false
    }
}

引用类型使用等于比较,比较的是对象是否相同(基本数据类型使用 = 进行比较比较的是数值)

那么b1等于b2说明两个装箱类型返回的b1和b2两个Integer对象是同一个对象,10这个数值在缓存范围内

而使用构造方法返回的对象是两个不同的对象,所以不相同结果为false

public class Test {
    public static void main(String[] args) {
        Integer b1 = 1000;
        Integer b2 = 1000;
        System.out.println(b1 == b2);// false
    }
}

而1000这个数值已经没有缓存了,所以返回的是两个对象

public class Test {
    public static void main(String[] args) {
        Double b1 = 10.0;
        Double b2 = 10.0;
        System.out.println(b1 == b2);// false
    }
}

由于Double是没有用缓存的,所以两个对象一定是不同的

如果您觉得该文章有用,欢迎点赞、留言并分享给更多人。感谢您的支持!