JavaSE的自动装箱和自动拆箱

回顾一下Java基础数据类型:

Java数据类型

基础类型字节包装类型
int4字节Integer
byte1字节Byte
short2字节Short
long8字节Long
float4字节Float
double8字节Double
char2字节Character
boolean未定Boolean

Java属于面向对象语言那么为什么会出现非对象类型数据(基础类型),因为基础数据类型是的虚拟机的运行速度更快而且占用内存更少。详情内容可以参见:Java为什么需要保留基本数据类型

为什么要有装箱&拆箱

JavaSE5之前我们创建Integer对象:

Integer i = new Integer(10);

JavaSE5提供了自动装箱的特性时,我们可以更简单的创建基础类型的对象:

Integer a = 10;
int b = a;

从上面的代码我们可以简单的看出装箱、拆箱的操作:

Integer a = 10;我们将10【装箱】生成Integer对象。
int b = a;我们将Integer【拆箱】转成int基础类型。

装箱和拆箱是如何实现的

我们这里先写一个简单的类,然后反编译看看它的字节码文件

public class Main {
    public static void main(String[] args) {
        Integer a = 10;
        int b = a;
    }
}

反编译出来的字节码文件:

Main字节码.jpg

结论:

装箱操作:

Integer a = 10;
//实际执行的是Integer a = Integer.valueOf(10);

拆箱操作:

int b = a;
//实际执行的是int b = a.intValue();

其他&扩展

我们先来看一道面试题:

public class Main {
    public static void main(String[] args) {
        Integer a = 10;
        Integer b = 10;
        Integer c = 128;
        Integer d = 128;
        System.out.println(a == b);
        System.out.println(c == d);
    }
}

内心怀揣自己的真是答案,我们看看下边的源代码:
先看看Integer装箱和拆箱的函数源码:

/**
 * Returns the value of this {@code Integer} as an
 * {@code int}.
 */
public int intValue() {
	return value;
}

/**
 * Returns an {@code Integer} instance representing the specified
 * {@code int} value.  If a new {@code Integer} instance is not
 * required, this method should generally be used in preference to
 * the constructor {@link #Integer(int)}, as this method is likely
 * to yield significantly better space and time performance by
 * caching frequently requested values.
 *
 * This method will always cache values in the range -128 to 127,
 * inclusive, and may cache other values outside of this range.
 *
 * @param  i an {@code int} value.
 * @return an {@code Integer} instance representing {@code i}.
 * @since  1.5
 */
public static Integer valueOf(int i) {
	if (i >= IntegerCache.low && i <= IntegerCache.high)
		return IntegerCache.cache[i + (-IntegerCache.low)];
	return new Integer(i);
}
  • 拆箱操作:直接返回Integer内的数值
  • 装箱操作:在i大于IntegerCache.low或者i小于IntegerCache.high时返回缓存的Integer对象,否则创建新的Integer对象。
/**
 * Cache to support the object identity semantics of autoboxing for values between
 * -128 and 127 (inclusive) as required by JLS.
 *
 * The cache is initialized on first usage.  The size of the cache
 * may be controlled by the {@code -XX:AutoBoxCacheMax=<size>} option.
 * During VM initialization, java.lang.Integer.IntegerCache.high property
 * may be set and saved in the private system properties in the
 * sun.misc.VM class.
 */
private static class IntegerCache {
	/*********/
}

通过源码码我们可以发现,Integer在数据为[-128,127]之间时。使用了IntegerCache返回缓存中对象的引用,否则new一个新的对象。

看到上面这个答案,有些同学就会想到:除过Integer之前还有其他的基础数据类型,那么其他的类型是否也是专业那个的呢?答案:是也不是。原理想想大家也都明白:

  • Boolean内部有true&false两个静态变量,最后装箱得到的值都是这两个静态变量的引用。
  • Long&Integer&Short&Byte在数值为[-128,127]之间都有Cache
  • Double&Float则都没有。

所以上面问题的正确答案分别是:truefalse

见识了==比较,现在看equals比较结果:

同样我们也先看一道题目:

public class Main {
    public static void main(String[] args) {
        Integer a = 1;
        Integer b = 2;
        Long c = 3L;
        System.out.println(c == (a + b));
        System.out.println(c.equals(a + b));
    }
}

然后我们再看看源码:

//Long.java
/**
 * Compares this object to the specified object.  The result is
  * {@code true} if and only if the argument is not
  * {@code null} and is a {@code Long} object that
  * contains the same {@code long} value as this object.
  *
  * @param   obj   the object to compare with.
  * @return  {@code true} if the objects are the same;
  *          {@code false} otherwise.
  */
 public boolean equals(Object obj) {
     if (obj instanceof Long) {
         return value == ((Long)obj).longValue();
     }
     return false;
 }

在Java中我们知道操作==的两个数都是数据包装类型对象的引用的话,那么则是用来比较两个引用所指向的对象是不是同一个;而如果其中有一个操作数是表达式(即包含算术运算)则比较的是数值(即会触发自动拆箱的过程)。

为什么呢,因为==两边引用数据类型必须一致,要不然无语错误。

所以我们得到上边题目的答案是:truefalse
因为第一次比较实际是先对数据进行拆箱然后比较,所以得到的结果是true;第二次比较实际是先拆箱(两个Integer对象拆箱)后装箱(将拆箱且计算后的数据再装箱),然后同Long对象比较,显然不是同一类型所以得到false

以上问题及答案都是作者亲自敲出来的,想实际操作同学也可以反编译class文件看看【真相】。


三目运算符的装箱拆箱

这一小节内容来源于2020年某一天在浏览微博的时候,发现一个博主分享的一个段代码引起的学习和思考。

我们先看以下代码,flag为真则返回a*b,否则返回c;

public class Main {
    public static void main(String[] args) {
        Integer a = 1;
        Integer b = 2;
        Integer c = null;
        Boolean flag = false;
        Integer result = flag ? a * b : c;
        System.out.println(result);
    }
}

结果:Exception in thread "main" java.lang.NullPointerException

这里也许有人很好奇为什么会报空指针~!

这里就需要说一下Java SE 1.7 JLS对三目运算符的描述:

The type of a conditional expression is determined as follows: 
● If the second and third operands have the same type (which may be the null type),then that is the type of the conditional expression. 
● If one of the second and third operands is of primitive type T, and the type of the other is the result of applying boxing conversion (§5.1.7) to T, then the type of the conditional expression is T.

翻译一下:

  • 当第二位和第三位操作数的类型相同时,则三目运算符表达式的结果和这两位操作数的类型相同。
  • 当第二,第三位操作数分别为基本类型和该基本类型对应的包装类型时,那么该表达式的结果的类型要求是基本类型。

Java SE 1.8 JLS中,关于这部分描述又做了一些细分,再次把表达式 区分成布尔型条件表达式(Boolean Conditional Expressions)、数值型条件表 达式(Numeric Conditional Expressions)和引用类型条件表达式(Reference Conditional Expressions)
并且通过表格的形式明确的列举了第二位和第三位分别是不同类型时得到的表达式结果值应该是什么。

使用JD-GU工具查看class文件:
在这里插入图片描述
查看源代码我们发现,既有intValue()拆箱,又有Interger.valueOf()装箱。

  • 拆箱是因为上面所描述的三目运算的运算规则,第二和第三操作数分别为基础类型和该基础类型的对应包装类时需要进行拆箱。
  • 装箱是因为三目运算符返回的结果需要是基础类型的包装类。

这里可能又有人发现代码中第二操作数和第三操作数都是Integer,为什么需要拆箱。如果有这个疑问的回到第一行看拆箱装箱的原因。

基础类型之间进行操作的时候都会先进行拆箱。

上面的代码我们可以稍微修改一下:

public class Main {
    public static void main(String[] args) {
        Integer a = 1;
        Integer b = 2;
        Integer c = null;
        Boolean flag = false;
        Integer result = flag ? Integer.valueOf(a * b) : c;
        System.out.println(result);
    }
}

结果:
在这里插入图片描述

扩展思考

为了方便大家理解,我使用了简单的布尔类型的例子说明了 NPE 的问题。但是 实际在代码开发中,遇到的场景可能并没有那么简单,比如说以下代码,大家猜一下 能否正常执行:

public class Main {
    public static void main(String[] args) {
        Map<String,Boolean> map = new HashMap<String, Boolean>();
        Boolean b = (map!=null ? map.get("Hello") : false);
        System.out.println(b);
    }
}

如果你的答案是 " 不能,这里会抛 NPE" 那么说明你看懂了本文的内容,但是, 我只能说你只是答对了一半。

因为以上代码,在小于 JDK 1.8 的版本中执行的结果是 NPE,在 JDK 1.8 及 以后的版本中执行结果是null

看到这里是不是有时候会想,还要区分运行时java的版本好难记啊~!

其实如果我们在平时开发过程中做好NPE的判断,不要将其放置的三目运算符中。这样不管以后的规则发生什么变化,我们的代码都不会发生不可预知的错误。

参考资料:
《Java-灵魂13问》
链接: https://pan.baidu.com/s/1oSPGn5Dj3Ql7JX4NMP0GgQ 提取码: 9uat
《The Java Language Specification Java SE 8 Edition》
链接: https://pan.baidu.com/s/1llAgEhorzCHZ9XIYYliqAg 提取码: xvp2

想阅读作者的更多文章,可以查看我的公共号:
振兴书城

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值