概述

自JDK1.5开始, 引入了自动装箱/拆箱这一语法糖, 它使程序员的代码变得更加简洁, 不再需要进行显式转换。基本类型与包装类型在某些操作符的作用下, 包装类型调用valueOf()方法将原始类型值转换成对应的包装类对象的过程, 称之为自动装箱; 反之调用xxxValue()方法将包装类对象转换成原始类型值的过程, 则称之为自动拆箱。

实现原理

首先我们用javap -c AutoBoxingDemo命令将下面代码反编译:

public class AutoBoxingDemo {
public static void main(String[] args) {
Integer m = 1;
int n = m;
}
}

反编译后结果:

从反编译后的字节码指令中可以看出, Integer m = 1; 其实底层就是调用了包装类Integer的valueOf()方法进行自动装箱, 而 int n = m; 则是底层调用了包装类的intValue()方法进行自动拆箱。

其中Byte、Short、Integer、Long、Boolean、Character这六种包装类型在进行自动装箱时都使用了缓存策略, 下面是Integer类的缓存实现机制:

/**
* This method will always cache values in the range -128 to 127,
* inclusive, and may cache other values outside of this range.
*/
public static Integer valueOf(int i) {
assert IntegerCache.high >= 127;
if (i >= IntegerCache.low && i <= IntegerCache.high)
return IntegerCache.cache[i + (-IntegerCache.low)];
return new Integer(i);
} private static class IntegerCache {
static final int low = -128;
static final int high;
static final Integer cache[]; static {
// high value may be configured by property
int h = 127;
String integerCacheHighPropValue =
sun.misc.VM.getSavedProperty("java.lang.Integer.IntegerCache.high");
if (integerCacheHighPropValue != null) {
int i = parseInt(integerCacheHighPropValue);
i = Math.max(i, 127);
// Maximum array size is Integer.MAX_VALUE
h = Math.min(i, Integer.MAX_VALUE - (-low) -1);
}
high = h; cache = new Integer[(high - low) + 1];
int j = low;
for(int k = 0; k < cache.length; k++)
cache[k] = new Integer(j++);
} private IntegerCache() {}
}

从Integer的源代码我们能得知, 当进行自动装箱的数值在[-128, 127]之间时, 调用valueOf()方法返回的是Integer缓存中已存在的对象引用。否则每次都是new一个新的包装类实例。

而Double、Float这两种包装类型因为是浮点数, 不像整数那样在某个范围内的数值个数是有限的, 所以它们没有使用缓存实现机制, 下面是Double包装类的自动装箱的源代码:

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

举例说明

public class AutoBoxingDemo {
public static void main(String[] args) {
Integer a = 1;
Integer b = 2;
Integer c = 3;
Integer d = 3;
Integer e = 321;
Integer f = 321;
Long g = 3L;
Long h = 2L;
Double i = 1.0;
Double j = 1.0;
Boolean k = true;
Boolean l = true;
//数值在[-128, 127]范围内,自动装箱时都是从缓存中获取对象引用,所以结果为true
System.out.println(c==d);
//数值在[-128, 127]范围外,自动装箱时每次都是new新的对象,所以结果为false
System.out.println(e==f);
//当"=="运算符的两个操作数都是包装器类型的引用,则比较指向的是否是同一个对象,而如果其中有一个操作数是表达式(即包含算术运算)则比较的是数值(即会触发自动拆箱的过程), 所以结果为true
System.out.println(c==(a+b));
//对于包装类型,当equals()方法比较的是同一类型时(比如Integer与Integer比较),实际比较的是他们的数值是否相等。如比较的不是同一类型,则不会进行类型转换,直接返回false。所以结果为true
System.out.println(c.equals(a+b));
//因为有算术运算,自动拆箱后再比较数值,所以结果为true
System.out.println(g==(a+b));
//因为equals()方法比较的是不同包装类型,不会进行类型转换,所以结果为false
System.out.println(g.equals(a+b));
//因为a+h先触发自动拆箱,a转为int类型后,需要隐式向上提升类型为long后再进行运算,最后再自动装箱转为Long包装类型,且两边数值相等,所以结果为true
System.out.println(g.equals(a+h));
//Double类没有缓存,每次都是new一个新的实例,所以结果为false
System.out.println(i == j);
//Boolean自动装箱,指向的都是同一个实例,所以结果为true
System.out.println(k == l);
}
}

在上面示例中, 关于结果的解析已经阐述的很清楚了, 主要有两个地方具有迷惑性。当"=="运算符的两个操作数都是包装器类型的引用,则比较指向的是否是同一个对象,而如果其中有一个操作数是表达式(即包含算术运算)则比较的是数值(即会先触发自动拆箱的过程)。

对于包装类型,当equals()方法比较的是同一类型时(比如Integer与Integer比较), 实际比较的是他们的数值是否相等; 如比较的不是同一类型(比如Integer与Long比较), 则不会进行类型转换,直接返回false。下面是Integer类的equals()方法的源代码:

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

另外我们也可以反编译以上代码, 穿透语法糖的糖衣能帮助我们更容易了解这些具有迷惑性现象的背后原理:

自动装箱/拆箱带来的问题

自动拆箱下算术运算引起的空指针问题

private Double distinct;
private void setParam(Double dSrc, boolean flag) {
this.distinct = (flag) ? dSrc : 0d;
}

上面这段代码乍一看是没问题的, 但实际当dSrc为null时, 调用该方法会抛出空指针异常, 我们对其进行反编译:

可以看出, 当对包装类进行诸如三目运算符的算术运算时, 当数据类型不一致时, 编译器会自动拆箱转换为基本类型再进行运算, 所以当dSrc传入null值时, 调用doubleValue()方法拆箱就会报NP空指针异常。

这里我们可以在进行算术运算时, 统一数据类型, 避免编译器进行自动拆箱, 来解决拆箱下三目运算符的空指针问题。还是上面这个栗子, 我们将 this.distinct = (flag) ? dSrc : 0d; 修改成 this.distinct = (flag) ? dSrc : Double.valueOf(0); 即可解决, 重新反编译后如下, 因为类型一致, 没有再进行自动拆箱:

自动装箱的弊端

Integer sum = 0;
for(int i=1000; i<10000; i++){
sum+=i;
}

如上代码, 当在循环中对包装类型进行算术运算 sum = sum + i; 时, 会先触发自动拆箱, 进行加法运算后, 再进行自动装箱,  且因为运算后的sum数值不在缓存范围之内, 所以每次都会new一个新的Integer实例。所以上面的循环结束后, 将会在内存中创建9000个无用的Integer实例对象, 这样会大大降低程序的性能, 增加GC的开销, 所以我们在写循环语句时一定要正确的声明变量类型, 避免因为自动装箱而引起不必要的性能问题。

重载与自动装箱

在JDK1.5之前, 没有引入自动装箱/拆箱这一语法糖, 当方法重载时,  test(int num) 与 test(Integer num) 的形参没有任何关系。JDK1.5之后, 当调用重载的方法时, 编译器不会进行自动装箱操作, 我们可以通过运行下面的代码示例来演示。

public static void testAutoBoxing(int num) {
System.out.println("方法形参为原始类型");
} public static void testAutoBoxing(Integer num) {
System.out.println("方法形参为包装类型");
} public static void main(String[] args) {
int m = 2;
testAutoBoxing(m);
Integer n = m;
testAutoBoxing(n);
}

运行结果如下:

很明显, 当调用重载的方法时, 编译器不会对传入的实参进行自动装箱操作。

参考资料

Autoboxing and Unboxing (The Java Tutorials > Lea...

深入剖析Java中的装箱和拆箱 - 海 子 - 博客园

Java 自动装箱与拆箱的实现原理 - 简书

Java自动拆箱下, 三目运算符的潜规则

Java中的自动装箱与拆箱

作者:张小凡
出处:https://www.cnblogs.com/qingshanli/

本文版权归作者和博客园共有,欢迎转载,但未经作者同意必须保留此段声明,且在文章页面明显位置给出原文连接,否则保留追究法律责任的权利。如果觉得还有帮助的话,可以点一下右下角的【推荐】。

Java的自动装箱/拆箱的更多相关文章

  1. Java 的自动装箱拆箱

    Java 是面向对象的语言,其基本数据类型也就有了相对应的类,称为包装类.以下是基本数据类型对应的包装类: 基本数据类型 包装类 byte(1字节) Byte short(2字节) Short int ...

  2. JAVA的自动装箱拆箱

    转自:http://www.cnblogs.com/danne823/archive/2011/04/22/2025332.html 蛋呢  的空间 ??什么是自动装箱拆箱 基本数据类型的自动装箱(a ...

  3. 通过源码了解Java的自动装箱拆箱

    什么叫装箱 & 拆箱? 将int基本类型转换为Integer包装类型的过程叫做装箱,反之叫拆箱. 首先看一段代码 public static void main(String[] args) ...

  4. java自动装箱拆箱总结

    对于java1.5引入的自动装箱拆箱,之前只是知道一点点,最近在看一篇博客时发现自己对自动装箱拆箱这个特性了解的太少了,所以今天研究了下这个特性.以下是结合测试代码进行的总结. 测试代码: int a ...

  5. Java八种基本数据类型的大小,以及封装类,自动装箱/拆箱的用法?

    参考:http://blog.csdn.net/mazhimazh/article/details/16799925 1. Java八种基本数据类型的大小,以及封装类,自动装箱/拆箱的用法? 原始类型 ...

  6. JAVA自动装箱拆箱与常量池

    java 自动装箱与拆箱 这个是jdk1.5以后才引入的新的内容,作为秉承发表是最好的记忆,毅然决定还是用一篇博客来代替我的记忆: java语言规范中说道:在许多情况下包装与解包装是由编译器自行完成的 ...

  7. java基础1.5版后新特性 自动装箱拆箱 Date SimpleDateFormat Calendar.getInstance()获得一个日历对象 抽象不要生成对象 get set add System.arrayCopy()用于集合等的扩容

    8种基本数据类型的8种包装类 byte Byte short Short int Integer long Long float Float double Double char Character ...

  8. Java中的自动装箱拆箱

    Java中的自动装箱拆箱 一.自动装箱与自动拆箱 自动装箱就是将基本数据类型转换为包装类类型,自动拆箱就是将包装类类型转换为基本数据类型. 1 // 自动装箱 2 Integer total = 90 ...

  9. Java 自动装箱/拆箱

    自动装箱/拆箱大大方便了基本类型(8个基本类型)数据和它们包装类的使用 自动装箱 : 基本类型自动转为包装类(int >> Integer) 自动拆箱: 包装类自动转为基本类型(Integ ...

随机推荐

  1. Linux 中文件和文件夹获取 MySQL 权限(SELinux)

    今天在 Linux 系统上移动 MySQL 的数据库目录 配置如下: /etc/my.cnf [mysqld]datadir=/home/mysqlsocket=/var/lib/mysql/mysq ...

  2. 正确写网站title、keywords、description

    一.title title,也就是标题,是在浏览器上面显示出来的,方便用户了解这个页面的内容;特别是搜索引擎判断你网页内容的主要根据.搜索引擎就很大部分是依靠网站title来判断你网站是关于什么内容的 ...

  3. 【JavaScript】深入理解call,以及与apply、bind的区别

    一.call call有两个妙用 1.继承(我前面的文章有提到用call实现call继承,有兴趣可以看下.https://www.cnblogs.com/pengshengguang/p/105476 ...

  4. 打开pycharm,提示invalid Log Path【已解决】

     问题:打开pycharm,提示invalid Log Path 解决: 网上其他方法都说重装,这个成本有点高,所以我不去尝试. 因为我下载的是免安装版,所以使用时生成的文件是后来才生成的,所以我尝试 ...

  5. python爬虫笔记之re.compile.findall()

    re.compile.findall原理是理解了,但输出不大理解(主要是加了正则表达式的括号分组) 一开始不懂括号的分组及捕捉,看了网上这个例子(如下),然而好像还是说不清楚这个括号的规律(还是说我没 ...

  6. 远程调试出现DEP0600: 部署失败。无法通过新部署管道进行部署错误解决

    昨天我连接树莓派调试没问题,今天来的时候却总是出现DEP0600: 部署失败.无法通过新部署管道进行部署.错误 我怀疑是环境问题,然后发现蓝莓派上面没有远程调试监视器(MSVSMON.EXE)进程,怀 ...

  7. Elasticsearch 技术分析(九):Elasticsearch的使用和原理总结

    前言 之前已经分享过Elasticsearch的使用和原理的知识,由于近期在公司内部做了一次内部分享,所以本篇主要是基于之前的博文的一个总结,希望通过这篇文章能让读者大致了解Elasticsearch ...

  8. Android自定义的属性的使用

    获取引用类型的属性值 private void init(Context context, AttributeSet attrs) { //int textId = attrs.getAttribut ...

  9. [小米OJ] 7. 第一个缺失正数

    思路: 参考这个思路 即:将每个数字放在对应的第几个位置上,比如1放在第1个位置上,2放在第2个位置上. 注意几个点:将每个数放在它正确的位置,前提是该数是正数,并且该数小于序列长度,并且交换的两个数 ...

  10. 微信小程序post 服务端无法获得参数问题

    header中需要改为 "Content-Type": "application/x-www-form-urlencoded"