澳门新葡萄京官网注册:Java 性能要点:自动装箱/ 拆箱 (Autoboxing / Unboxing)

本文由码农网 –
袁延勇原创翻译,转载请看清文末的转载要求,欢迎参与我们的付费投稿计划!

Java的自动拆装箱介绍,Java拆装箱介绍

在面试过程中,常常会有面试官问到基础的问题的时候都会问到Java的拆装箱,关于这个问题其实不是很难,但是如果平时进行自学的时候不是注意,就可能一脸懵逼,所以笔者就这个问题进行一些总结,共同促进!

【编者按】本文作者为 Ali Kemal
TASCI,最早于2016年4月9日发布于DZONE社区。文章主要介绍通过改进 Java 1.5
就已存在的骨灰级特性大幅度提高应用性能。

如果我说“仅仅修改下面代码中的一个字符就可以获得五倍于当前的运行速度”你会怎么想呢?

一、拆装箱概念

澳门新葡萄京官网注册 ,所谓的拆装箱,就是自从JDK1.5之后,java的基本类型和引用类型之间的相互转换。

本文系 OneAPM
工程师编译呈现,以下为正文。

long t = System.currentTimeMillis();
Long sum = 0L;
for (long i = 0; i < Integer.MAX_VALUE; i++) {
sum += i;
}
System.out.println("total:" + sum);
System.out.println("processing time: " + (System.currentTimeMillis() - t) + " ms") ;

1.1拆箱

拆箱就是把Long,Integer,Double,Float
等将基本数据类型的首字母大写的相应的引用类型转化为基本数据类型的动作就叫拆箱。

如果我告诉你:“只要修改一个字符,下面这段代码的运行速度就能提高5倍。”,你觉得可能么?

结果如下:

1.2装箱

装箱就是把byte ,int ,short, long ,double,float,boolean,char
这些Java的基本数据类型在定义数据类型时不声明为相对应的引用类型,在编译器的处理下自动转化为引用类型的动作就叫做装箱。

long t = System.currentTimeMillis();
Long sum = 0L;for (long i = 0; i < Integer.MAX_VALUE; i++) {
    sum += i;
}
System.out.println("total:" + sum);
System.out.println("processing time: " + (System.currentTimeMillis() - t) + " ms");
total:2305843005992468481
processing time: 6756 ms

二、拆装箱的相关应用

在JDK1.5后,当我们进行基本类型和引用类型的转换的时候就会方便:

package com.hzp.CZX;
/**
 * 测试拆装箱
 * @author 夜孤寒
 * @version 1.1.1
 */
public class TestDemo {
    /**
     * 拆装箱JDK1.5后
     */
    public static void first(){
        Integer i=7;//基本类型-->引用类型
        int j=i;//引用类型-->基本类型
        System.out.println(j);
    }
    /**
     * 拆装箱JDK1.4
     */
    public static void second(){
        Integer i=new Integer(78);
        int j=i.intValue();
        System.out.println(j);
    }
    /**
     * 测试方法
     * @param args
     */
    public static void main(String[] args) {
        first();
        second();
    }
}

上面介绍了关于拆装箱的一些基本点和使用方式,但是要使用拆装箱的话还有一些注意点需要注意,下面将这些注意点进行一些总结。

输出结果:
总数:2305843005992468481
处理时间:6756 ms

译者本机测试结果:

三、注意点

首先贴一段代码如下:

package com.ygh.CZX;
/**
 * 关于java的拆装箱范围剖析
 * @author 夜孤寒
 * @version 1.1.1
 */
public class Test {
    /**
     * 以Integer类型为例
     */
    public static void first(){
        Integer i=new Integer(124);
        Integer j=new Integer(124);
        System.out.println(i==j);//false
        Integer a1=-128;
        Integer a2=-128;
        System.out.println(a1==a2);//true
        Integer b1=-129;
        Integer b2=-129;
        System.out.println(b1==b2);//false
        Integer c1=127;
        Integer c2=127;
        System.out.println(c1==c2);//true
        Integer d1=128;
        Integer d2=128;
        System.out.println(d1==d2);//false
    }
    public static void main(String[] args) {
        first();

    }
}

简单解释一下:

第一个结果为false的原因是因为创建了不同的对象,所以两者不一样;

但是第二个和第三个的结果为什么不一样?

下面贴出关于Integer类的源码,从源码的角度来分析这个问题:

    /**
     * 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类中的一个内部私有类IntegerCache可以体现出来,源码如下:

 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) {
                try {
                    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);
                } catch( NumberFormatException nfe) {
                    // If the property cannot be parsed into an int, ignore it.
                }
            }
            high = h;

            cache = new Integer[(high - low) + 1];
            int j = low;
            for(int k = 0; k < cache.length; k++)
                cache[k] = new Integer(j++);

            // range [-128, 127] must be interned (JLS7 5.1.7)
            assert IntegerCache.high >= 127;
        }

        private IntegerCache() {}
    }

从这里我们可以看出,范围值为[-128,127]之间。

注意,Integer、Short、Byte、Character、Long这几个类的valueOf方法的实现是类似的。
Double、Float的valueOf方法的实现是类似的。

总结:这些进行自动拆装箱的基本类型的范围如下:

  1. boolean类型的值

2.所有的byte的值

3.在-128~127的short类型的值

4.在-128~127的int类型的值

5.在 u0000~ u00ff 之间的char类型的值

而其中double和float又有所不同,我们就以double为例子,贴出代码讨论:

package com.ygh.CZX;

/**
 * 关于java的拆装箱范围剖析
 * 
 * @author 夜孤寒
 * @version 1.1.1
 */
public class Test {
    /**
     * Double
     */
    public static void first() {
        Double i1 = 100.0;
        Double i2 = 100.0;
        Double i3 = 200.0;
        Double i4 = 200.0;
        System.out.println(i1 == i2);//false
        System.out.println(i3 == i4);//false
    }
    /**
     * 测试方法
     */
    public static void main(String[] args) {
        first();
    }
}

注意为什么上面的代码的输出结果都是false呢?同样的我们依旧以Double类中的valueOf方法来讨论,贴出源码就一目了然了:

    /**
     * Returns a {@code Double} instance representing the specified
     * {@code double} value.
     * If a new {@code Double} instance is not required, this method
     * should generally be used in preference to the constructor
     * {@link #Double(double)}, as this method is likely to yield
     * significantly better space and time performance by caching
     * frequently requested values.
     *
     * @param  d a double value.
     * @return a {@code Double} instance representing {@code d}.
     * @since  1.5
     */
    public static Double valueOf(double d) {
        return new Double(d);
    }

也就是说不管你的double是什么范围的值,他都是给你返回一个新的对象。float同double,就不过多赘述了。

以上就是笔者对于拆装箱的一些整理,如果读者有不同的看法可以在评论区提出,笔者再进行修改!

在面试过程中,常常会有面试官问到基础的问题的时候都会问到Java的拆装箱,关于这个问题其实不是…

仔细琢磨一下,你可能会想到下面这种执行速度更快的实现方法:

total:2305843005992468481
processing time: 8369 ms
long t = System.currentTimeMillis();//Long sum = 0L;long sum = 0L;for (long i = 0; i < Integer.MAX_VALUE; i++) {
    sum += i;
}
System.out.println("total:" + sum);
System.out.println("processing time: " + (System.currentTimeMillis() - t) + " ms") ;

一番思考之后,我们可以采用如下更加快速的代码:

输出结果:
总数:2305843005992468481
处理时间:1248 ms

long t = System.currentTimeMillis();
//Long sum = 0L;
long sum = 0L;
for (long i = 0; i < Integer.MAX_VALUE; i++) {
sum += i;
}
System.out.println("total:" + sum);
System.out.println("processing time: " + (System.currentTimeMillis() - t) + " ms") ;

其实,自动装箱(Autoboxing)的草率使用是造成速度差异的根本原因,而这一特性从
Java 1.5 开始就已出现了。

结果如下:

在继续解释造成差异的细节之前,让我们仔细回味一下 Java
中的这两个概念:自动装箱(Autoboxing)与 拆箱(Unboxing)。

total:2305843005992468481
processing time: 1248 ms

Java
中的变量分为两种:原始型与引用型。一共存在8个原始型变量以及与各个原始变量对应的8个引用变量(包装类)。

译者本机测试结果:

Primitive Types(原始型) Reference Types(Wrapper Class)(引用型,(包装类))
boolean Boolean
byte Byte
char Character
float Float
int Integer
long Long
short Short
double Double
total:2305843005992468481
processing time: 779 ms

下面的代码会介绍”Autoboxing“与”Unboxing“的用例。在这段代码中,一个类型为”long”的值被添加到类型为”Long“的List集合中。在
Java 1.4
中,为了实现此操作,我们必须将原始变量赋值到合适的引用类中(也即装箱,boxing)。从
Java 1.5
开始,编译器会帮我们完成这一操作。所以,我们不再需要写那么多代码。

我们可以将这种差异解释为对自动装箱功能的滥用,而此功能自JDK1.5我们就已开始使用。先不管造成差异的原因,让我们来仔细琢磨下Java中“自动装箱”和“自动拆箱”的概念。

List<Long> longList = new ArrayList<>();      
long i = 4;
longList.add( i ); //autoboxing      long j = longList.get( 0 ); //unboxing

Java中变量被分为两类:基础数据类型和引用数据类型。Java中有8种基础数据类型和与每一种基础数据类型相对应的8种引用数据类型(包装类)

从 Java 1.5 开始,编译器会自动将上面的代码段转化成如下代码:

Primitive Types Reference Types(Wrapper Class)
boolean Boolean
byte Byte
char Character
float Float
int Integer
long Long
short Short
double Double
List<Long> longList = new ArrayList<>();      
long i = 4;
longList.add(Long.valueOf( i ) );      
long j = longList.get( 0 ).longValue();

如下代码片段列举了“自动装箱”和“自动拆箱”的例子。代码片段中,一个“long”类型的值被添加到“Long”类型值的列表中。在JDK1.4中,要想执行以上操作,我们必须将基础数据类型放到与之对应的引用数据类型中(装箱)。JDK1.5以后,编译器帮我们执行以上操作,所以我们节省了不少代码量。

因此,我们也可以说,前文出现的第一段代码段会自动转化为如下代码。所以,导致处理时间较长的原因也就水落石出了:不必要地创建了2147483647个”Long“类型实例。

List<Long> longList = new ArrayList<>();      
long i = 4;
longList.add( i ); //autoboxing      
long j = longList.get( 0 ); //unboxing
long t = System.currentTimeMillis();
Long sum = 0L;for (long i = 0; i < Integer.MAX_VALUE; i++) {
sum += new Long(i);
}
System.out.println("total:" + sum);
System.out.println("processing time: " + (System.currentTimeMillis() - t) + " ms") ;

JDK1.5以后,编译器已经自动改变上述代码段为以下代码:

由此可知,想要编写速度更快的 Java
代码,我们也需要考虑”Autoboxing”与”Unboxing”这样的基础概念。

List<Long> longList = new ArrayList<>();      
long i = 4;
longList.add(Long.valueOf( i ) );      
long j = longList.get( 0 ).longValue();

相关资源集锦

Autoboxing and
Unboxing
Autoboxing
Efective Java 2nd Edition, J.
Bloch

OneAPM
为您提供端到端的 Java
应用性能解决方案,我们支持所有常见的
Java
框架及应用服务器,助您快速发现系统瓶颈,定位异常根本原因。分钟级部署,即刻体验,Java
监控从来没有如此简单。想阅读更多技术文章,请访问 OneAPM
官方技术博客。

本文转自 OneAPM 官方博客

原文地址:

因此,我们可以说我们的第一段代码已经被修改为下面的代码。所以我们可以利用转化后的代码(创建不必要的“Long”实例)来解释处理速度缘何变慢。

long t = System.currentTimeMillis();
Long sum = 0L;
for (long i = 0; i < Integer.MAX_VALUE; i++) {
sum += new Long(i);
}
System.out.println("total:" + sum);
System.out.println("processing time: " + (System.currentTimeMillis() - t) + " ms") ;

结论,如果我们想要写出处理速度更快的代码,我们需要仔细琢磨“自动装箱”和“自动拆箱”的概念。

译者注:本机使用JDK1.8,可能与笔者使用JDK版本存在差异,多次测试均保持10倍左右的处理速度差异,可见影响还是比较大的。

发表评论

电子邮件地址不会被公开。 必填项已用*标注