概述

自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. Git项目迁移

    代码项目迁移步骤 1.将原有项目重命名,old 2.新建一个项目,名字为原本的项目名称,new 3.使用特殊方式克隆代码 # old.git为原项目重命名后的git链接 git clone --mir ...

  2. Product Backlog:终极任务清单

    健康的Product Backlog就像一个健康的人那样:整洁有序.组织合理.公开透明.一个按照优先级顺序排好的敏捷Backlog不仅能够简化发版和迭代计划,还能够对团队计划去做的所有工作进行细致规划 ...

  3. 如何使用JavaScript导入和导出Excel文件

    本文由葡萄城技术团队于原创并首发 转载请注明出处:葡萄城官网,葡萄城为开发者提供专业的开发工具.解决方案和服务,赋能开发者. JavaScript是一个涵盖多种框架.直译式.可以轻松自定义客户端的脚本 ...

  4. 设计和编写一个异步通用Picker选择器,用于时间日期、城市、商品分类的选择

    目录 一.功能规划 二.最底层基础实现 (1)Picker界面和功能实现 (2)不同类型的选择器基础实现 三.数据源层 (1)时间日期 (2)多级同步分类,如:城市 (3)多级异步分类,如:城市 四. ...

  5. jieba GitHUb 结巴分词

    1.GitHub jieba-analysis 结巴分词: https://github.com/fxsjy/jieba 2.jieba-analysis 结巴分词(java版): https://g ...

  6. 那些有实力进入 BAT 的本科生,都做对了什么事?

    作者:黄小斜 文章来源:微信公众号[黄小斜] 最近这段时间,我们部门来了几个年纪轻轻的本科生,最小的比我们小五岁左,这对于我来说还是比较有冲击力的. 想想我也是九0出头的老腊肉了,想当年我上大学的时候 ...

  7. Python的函数, 返回值, 参数

    1. 函数 函数是对功能的封装 语法: def 函数名(形参): 函数体(代码块,return) 调用: 函数名(实参) 2. 返回值 return:在函数执行的时候, 遇到return 就直接返回, ...

  8. 数字IC前后端设计中的时序收敛(五)--Max Transition违反的修复方法

    本文转自:自己的微信公众号<数字集成电路设计及EDA教程> 里面主要讲解数字IC前端.后端.DFT.低功耗设计以及验证等相关知识,并且讲解了其中用到的各种EDA工具的教程. 考虑到微信公众 ...

  9. 自己实现定制自己的专属java锁,来高效规避不稳定的第三方

    java juc 包下面已经提供了很多并发锁工具供我们使用,但在日常开发中,为了各种原因我们总是会用多线程来并发处理一些问题,然而并不是所有的场景都可以使用juc 或者java本身提供的锁来方便的帮助 ...

  10. flink window实例分析

    window是处理数据的核心.按需选择你需要的窗口类型后,它会将传入的原始数据流切分成多个buckets,所有计算都在window中进行. flink本身提供的实例程序TopSpeedWindowin ...