Java中自动装箱、拆箱引起的耗时详解

什么是自动装箱,拆箱

先抛出定义,Java中基础数据类型与它们的包装类进行运算时,编译器会自动帮我们进行转换,转换过程对程序员是透明的,这就是装箱和拆箱,装箱和拆箱可以让我们的代码更简洁易懂

耗时问题

在说 Java 的自动装箱和自动拆箱之前,我们先看一个例子。

这个错误我在项目中犯过(尴尬),拿出来共勉!

private static long getCounterResult() {
 Long sum = 0L;
 final int length = Integer.MAX_VALUE;
 for (int i = 0; i < length; i++) {
 sum += i;
 }
 return sum;
}
public static void main(String[] args) {
 long startCountTime = System.currentTimeMillis();
 long result = getCounterResult();
 long endCountTime = System.currentTimeMillis();
 System.out.println("result = " + result + ", and take up time : " + (endCountTime - startCountTime) / 1000 + "s");
}

在我的电脑(macOS 64位系统,配置较高),打印结果如下:

result = 2305843005992468481, and take up time : 12s

居然使用了 12s,是可忍叔不可忍,再正常不过的代码怎么会耗时这么久呢?如果在配置差一点的电脑上运行耗时会更久(惊呆了.jpg)。

我们不妨先阅读下面的内容,再来分析、解决上述耗时的问题。

基本概念

自从 jdk1.5 之后就有了自动装箱(Autoboxing)和自动拆箱(AutoUnboxing)。

自动装箱,就是 Java 自动将原始(基本)类型转换成对应的封装器(对象)类型的过程,比如将 int 的变量转换成 Integer 对象,这个过程叫做装箱。

自动拆箱,就是 Java 自动将封装器(对象)类型转换成基本类型的过程,如将 Integer 对象转换成 int 类型值,这个过程叫做拆箱。

之所以称之为自动装箱和拆箱,是因为这些操作并非人工(程序猿)操作的,而是 Java 自带的一个特性。

下表是 Java 中的基本类型和对应的封装类型的对应表:

基本类型 封装器类
int Integer
byte Byte
long Long
float float
double Double
char Character
boolean Boolean

自动装箱示例:

int a = 3;
Integer b = a;

自动拆箱示例:

Integer b = new Integer(7);
int a = b;

Integer/int 自动拆箱和装箱

下面这段代码是 Integer 的源码中 valueOf 方法。

/**
 * 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) {
 // 如果i的值大于-128小于127则返回一个缓冲区中的一个Integer对象
 if (i >= IntegerCache.low && i <= IntegerCache.high)
 return IntegerCache.cache[i + (-IntegerCache.low)];

 // 否则返回 new 一个Integer 对象
 return new Integer(i);
}

我们在执行下面的这句代码,如下:

Integer i = 100;

上面的代码等同于下面的代码:

Integer i = Integer.valueOf(100);

结合上面的源码可以看出来,如果数值在 [-128,127] 之间(双闭区间),不会重新创建 Integer 对象,而是从缓存中(常量池)直接获取,从常量池中获取而不是堆栈操作,读取数据要快很多。

我们再来看一下常见的基础面试题(请给出打印结果),如下:

public static void main(String[] args) {
 // ⓵
 Integer a = new Integer(121);
 Integer b = new Integer(121);
 System.out.println(a == b);

 // ⓶
 Integer c = 121;
 Integer d = 121;
 System.out.println(c == d);

 // ⓷
 Integer e = 129;
 Integer f = 129;
 System.out.println(e == f);

 // ⓸
 int g = 50;
 Integer h = new Integer(50);
 System.out.println(g == h);
}

分析结果:

⓵: false, 两个对象进行比较分别指向了不同堆内存

⓶: true, 自动装箱且数值在 [-128,127] 之间(双闭区间)

⓷: false, 自动装箱且数值不在 [-128,127] 之间(双闭区间)

⓸: true, 自动拆箱且数值在 [-128,127] 之间(双闭区间)

解析耗时问题

类 Long 对应的也有一个 valueof 方法,源码如下:

public static Long valueOf(long l) {
 final int offset = 128;
 if (l >= -128 && l <= 127) { // will cache
  return LongCache.cache[(int)l + offset];
 }
 return new Long(l);
}

这个和 Integer 的很像,道理上面说过,这里不再赘述。

在开篇的例子中,getCounterResult 方法有下面这句代码,如下:

Long sum = 0L;

很明显我们声明了一个 Long 的对象 sum,由于自动装箱,这句代码并没有语法上面的错误,编译器当然也不会报错。上面代码等同于如下代码:

Long sum = Long.valueof(0);

在 for 循环中,超过 [-128,127] 就会创建新的对象,这样不断的创建对象,不停的申请堆内存,程序执行自然也就比较耗时了。

修改一下代码,如下:

private static long getCounterResult() {
 // 修改为普通的基本类型数据
 long sum = 0L;
 final int length = Integer.MAX_VALUE;
 for (int i = 0; i < length; i++) {
  sum += i;
 }
 return sum;
}
public static void main(String[] args) {
 long startCountTime = System.currentTimeMillis();
 long result = getCounterResult();
 long endCountTime = System.currentTimeMillis();
 System.out.println("result = " + result + ", and take up time : " + (endCountTime - startCountTime) / 1000 + "s");
}

执行时间大大缩短。

总结

以上就是这篇文章的全部内容了,希望本文的内容对大家的学习或者工作具有一定的参考学习价值,谢谢大家对我们的支持。

(0)

相关推荐

  • Java中Connection timed out和Connection refused的区别讲解

    前言:这两个异常报出的时候,说明客户端没法正常连接到服务端,但是两者还是有区别的. 1:Connection timed out 在实际开发中经常会碰到Connection timed out的问题 java.net.ConnectException: Connection timed out (Connection timed out) at java.net.PlainSocketImpl.socketConnect(Native Method) at java.net.AbstractPl

  • 详解Java基础篇--面向对象1(构造方法,static、this关键字)

    面向对象,面向过程的区别.拿下五子棋来说: 面向过程分析: 开始游戏 黑棋先走 绘制画面 判断输赢 轮到白棋 绘制画面 判断输赢 返回步骤2 输出结果 面向对象分析: 黑白双方,双方行为是一模一样的 棋盘系统,负责绘制画面 规则系统,判断犯规.输赢 传统的面向过程编程是思考问题的解决步骤,这种思维方式适用于问题规模较小时.可是当问题规模大,要求程序有更好的可扩展性,能更快速地查错时面向对象设计思想就能体现出其优势.面向对象更接近人类地自然思维方式,将现实世界中的事物抽象为对象和对象的方法. 面向

  • Java Property类使用详解

    概念理解 Properties 继承于 Hashtable.表示一个持久的属性集,属性列表以key-value的形式存在,key和value都是字符串.Properties类被许多Java类使用.例如,在获取环境遍历时它就作为System.getProperties()方法的返回值.我们在很多需要避免硬编码的应用场景下需要使用Properties文件来加载程序需要配置的信息,比如JDBC.MyBatis框架等.Properties类则是Properties文件和程序的中间桥梁,不论是从prope

  • 带你深入概括Java!六、方法和方法重载!(推荐)

    一. 掌握方法和参数语法和反回值语 方法的分类: – 1. 无参无返(没有参数列表,没有返回值)单纯的作为 功能代码的聚合使用 便于功能复用. – 2.无参有返(没有参数列表,有返回值)例如: 我需要每次生成一个随机卡号 – 3.有参无返(有参数列表 没有返回值) 适用于功能需要根据参数来进行计算的情况,但是计算的最终结果又无需返回处理 – 4. 有参有返(有参数列表,有返回值)适用于功能需要根据参数来进行计算的情况,而且最终的结果需要被我们拿到(返回处理) 方法的形参和实参: 形参 :是定义在

  • Java的外部类为什么不能使用private和protected进行修饰的讲解

    Java的外部类为什么不能使用private和protected进行修饰 对于这个问题,一直没有仔细思考,今天整理一下: 对于顶级类(外部类)来说,只有两种修饰符:public和默认(default).因为外部类的上一单元是包,所以外部类只有两个作用域:同包,任何位置.因此,只需要两种控制权限:包控制权限和公开访问权限,也就对应两种控制修饰符:public和默认(default). 如果类使用了private修饰符,说明是个内部类.内部类的上一级是外部类,那么对应的有四种访问控制修饰符:本类(p

  • Java中值传递的深度分析

    前言 首先说观点:java只有值传递没有引用传递 然后再来看看值传递与引用传递两者的定义 值传递(pass by value)是指在调用函数时将实际参数复制一份传递到函数中,这样在函数中如果对参数进行修改,将不会影响到实际参数. 引用传递(pass by reference)是指在调用函数时将实际参数的地址直接传递到函数中,那么在函数中对参数所进行的修改,将影响到实际参数. 这里牢记值传递中将实际参数复制一份. 然后就是对于参数类型:值类型 和 引用类型. 结合起来理解就是:值类型传递,java

  • 详解java_ 集合综合案例:斗地主

    案例介绍 按照斗地主的规则,完成洗牌发牌的动作. 具体规则: 使用54张牌打乱顺序,三个玩家参与游戏,三人交替摸牌,每人17张牌,最后三张留作底牌. 案例分析 1.准备牌: 牌可以设计为一个ArrayList,每个字符串为一张牌. 每张牌由花色数字两部分组成,我们可以使用花色 集合与数字集合嵌套迭代完成每张牌的组装. 牌由Collections类的shuffle方法进行随机排序. 2.发牌 将每个人以及底牌设计为ArrayList,将最后3张牌直接存放于底牌,剩余牌通过对3取模依次发牌. 3.看

  • 详解JavaFX桌面应用开发-Group(容器组)

    1:Group的功能 Group可以管理一组节点 Group可以对管理的节点进行增删改查的操作 Group可以管理节点的属性 1.2:看看JDKSE1.9的API Group类有下列可以调用的方法 2:Group的使用 代码如下: package application; import javafx.application.Application; import javafx.scene.Group; import javafx.scene.Scene; import javafx.scene.

  • 浅谈Java中的参数传递问题

    /* 思考题1:看程序写结果,然后分析为什么是这个样子的.并画图讲解.最后总结Java中参数传递规律. Java中的参数传递问题: 基本类型:形式参数的改变对实际参数没有影响. 引用类型:形式参数的改变直接影响实际参数. */ class ArgsDemo { public static void main(String[] args) { int a = 10; int b = 20; System.out.println("a:"+a+",b:"+b); //a

  • 详解Java变量与常量

    一.常量 用final修饰(也称最终变量) 常量在声明时必须赋初值,赋值后不能再修改值 常量名通常用全大写字母表示 声明时需要添加final或static final类型修饰符,例如: private final int PI=3.141596; //常量,类加载时确定或者更靠后确定值 private static final int PI=3.14159;//静态常量(编译期常量),编译时就确定值(编译为class文件) 二.变量 1.变量类型不同,分配的内存类型也不同 2.无初始化成员变量的

随机推荐