zoukankan      html  css  js  c++  java
  • 详解Java的自动装箱与拆箱(Autoboxing and unboxing)

     

    详解Java的自动装箱与拆箱(Autoboxing and unboxing)

    标签: 自动装箱自动拆箱java性能
     分类:

    一、什么是自动装箱拆箱 
    很简单,下面两句代码就可以看到装箱和拆箱过程

    //自动装箱
    Integer total = 99;
    
    //自定拆箱
    int totalprim = total;
    • 1
    • 2
    • 3
    • 4
    • 5

    简单一点说,装箱就是自动将基本数据类型转换为包装器类型;拆箱就是自动将包装器类型转换为基本数据类型。

    下面我们来看看需要装箱拆箱的类型有哪些:

    这里写图片描述

    这里写图片描述

    这个过程是自动执行的,那么我们需要看看它的执行过程:

    public class Main {
        public static void main(String[] args) {
        //自动装箱
        Integer total = 99;
    
        //自定拆箱
        int totalprim = total;
        }
    }
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9

    反编译class文件之后得到如下内容:

    javap -c StringTest
    • 1

    这里写图片描述

    Integer total = 99; 
    执行上面那句代码的时候,系统为我们执行了: 
    Integer total = Integer.valueOf(99);

    int totalprim = total; 
    执行上面那句代码的时候,系统为我们执行了: 
    int totalprim = total.intValue();

    我们现在就以Integer为例,来分析一下它的源码: 
    1、首先来看看Integer.valueOf函数

    public static Integer valueOf(int i) {
        return  i >= 128 || i < -128 ? new Integer(i) : SMALL_VALUES[i + 128];
    }
    • 1
    • 2
    • 3

    它会首先判断i的大小:如果i小于-128或者大于等于128,就创建一个Integer对象,否则执行SMALL_VALUES[i + 128]。

    首先我们来看看Integer的构造函数:

    private final int value;
    
    public Integer(int value) {
        this.value = value;
    }
    
    public Integer(String string) throws NumberFormatException {
        this(parseInt(string));
    }
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9

    它里面定义了一个value变量,创建一个Integer对象,就会给这个变量初始化。第二个传入的是一个String变量,它会先把它转换成一个int值,然后进行初始化。

    下面看看SMALL_VALUES[i + 128]是什么东西:

    private static final Integer[] SMALL_VALUES = new Integer[256];
    • 1

    它是一个静态的Integer数组对象,也就是说最终valueOf返回的都是一个Integer对象。

    所以我们这里可以总结一点:装箱的过程会创建对应的对象,这个会消耗内存,所以装箱的过程会增加内存的消耗,影响性能。

    2、接着看看intValue函数

    @Override
    public int intValue() {
        return value;
    }
    • 1
    • 2
    • 3
    • 4

    这个很简单,直接返回value值即可。

    二、相关问题 
    上面我们看到在Integer的构造函数中,它分两种情况: 
    1、i >= 128 || i < -128 =====> new Integer(i) 
    2、i < 128 && i >= -128 =====> SMALL_VALUES[i + 128]

    private static final Integer[] SMALL_VALUES = new Integer[256];
    • 1

    SMALL_VALUES本来已经被创建好,也就是说在i >= 128 || i < -128是会创建不同的对象,在i < 128 && i >= -128会根据i的值返回已经创建好的指定的对象。

    说这些可能还不是很明白,下面我们来举个例子吧:

    public class Main {
        public static void main(String[] args) {
    
            Integer i1 = 100;
            Integer i2 = 100;
            Integer i3 = 200;
            Integer i4 = 200;
    
            System.out.println(i1==i2);  //true
            System.out.println(i3==i4);  //false
        }
    }
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12

    代码的后面,我们可以看到它们的执行结果是不一样的,为什么,在看看我们上面的说明。 
    1、i1和i2会进行自动装箱,执行了valueOf函数,它们的值在(-128,128]这个范围内,它们会拿到SMALL_VALUES数组里面的同一个对象SMALL_VALUES[228],它们引用到了同一个Integer对象,所以它们肯定是相等的。

    2、i3和i4也会进行自动装箱,执行了valueOf函数,它们的值大于128,所以会执行new Integer(200),也就是说它们会分别创建两个不同的对象,所以它们肯定不等。

    下面我们来看看另外一个例子:

    public class Main {
        public static void main(String[] args) {
    
            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
        }
    }
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12

    看看上面的执行结果,跟Integer不一样,这样也不必奇怪,因为它们的valueOf实现不一样,结果肯定不一样,那为什么它们不统一一下呢? 
    这个很好理解,因为对于Integer,在(-128,128]之间只有固定的256个值,所以为了避免多次创建对象,我们事先就创建好一个大小为256的Integer数组SMALL_VALUES,所以如果值在这个范围内,就可以直接返回我们事先创建好的对象就可以了。

    但是对于Double类型来说,我们就不能这样做,因为它在这个范围内个数是无限的。 
    总结一句就是:在某个范围内的整型数值的个数是有限的,而浮点数却不是。

    所以在Double里面的做法很直接,就是直接创建一个对象,所以每次创建的对象都不一样。

    public static Double valueOf(double d) {
        return new Double(d);
    }
    • 1
    • 2
    • 3

    下面我们进行一个归类: 
    Integer派别:Integer、Short、Byte、Character、Long这几个类的valueOf方法的实现是类似的。 
    Double派别:Double、Float的valueOf方法的实现是类似的。每次都返回不同的对象。

    下面对Integer派别进行一个总结,如下图: 
    这里写图片描述

    下面我们来看看另外一种情况:

    public class Main {
        public static void main(String[] args) {
    
            Boolean i1 = false;
            Boolean i2 = false;
            Boolean i3 = true;
            Boolean i4 = true;
    
            System.out.println(i1==i2);//true
            System.out.println(i3==i4);//true
        }
    }
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12

    可以看到返回的都是true,也就是它们执行valueOf返回的都是相同的对象。

    public static Boolean valueOf(boolean b) {
        return b ? Boolean.TRUE : Boolean.FALSE;
    }
    • 1
    • 2
    • 3

    可以看到它并没有创建对象,因为在内部已经提前创建好两个对象,因为它只有两种情况,这样也是为了避免重复创建太多的对象。

    public static final Boolean TRUE = new Boolean(true);
    
    public static final Boolean FALSE = new Boolean(false);
    • 1
    • 2
    • 3

    上面把几种情况都介绍到了,下面来进一步讨论其他情况。

    Integer num1 = 400;  
    int num2 = 400;  
    System.out.println(num1 == num2); //true
    • 1
    • 2
    • 3

    说明num1 == num2进行了拆箱操作

    Integer num1 = 100;  
    int num2 = 100;  
    System.out.println(num1.equals(num2));  //true
    • 1
    • 2
    • 3

    我们先来看看equals源码:

    @Override
    public boolean equals(Object o) {
        return (o instanceof Integer) && (((Integer) o).value == value);
    }
    • 1
    • 2
    • 3
    • 4

    我们指定equal比较的是内容本身,并且我们也可以看到equal的参数是一个Object对象,我们传入的是一个int类型,所以首先会进行装箱,然后比较,之所以返回true,是由于它比较的是对象里面的value值。

    Integer num1 = 100;  
    int num2 = 100;  
    Long num3 = 200l;  
    System.out.println(num1 + num2);  //200
    System.out.println(num3 == (num1 + num2));  //true
    System.out.println(num3.equals(num1 + num2));  //false
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6

    1、当一个基础数据类型与封装类进行==、+、-、*、/运算时,会将封装类进行拆箱,对基础数据类型进行运算。 
    2、对于num3.equals(num1 + num2)为false的原因很简单,我们还是根据代码实现来说明:

    @Override
    public boolean equals(Object o) {
        return (o instanceof Long) && (((Long) o).value == value);
    }
    • 1
    • 2
    • 3
    • 4

    它必须满足两个条件才为true: 
    1、类型相同 
    2、内容相同 
    上面返回false的原因就是类型不同。

    Integer num1 = 100;
    Ingeger num2 = 200;
    Long num3 = 300l;
    System.out.println(num3 == (num1 + num2)); //true
    • 1
    • 2
    • 3
    • 4

    我们来反编译一些这个class文件:javap -c StringTest 
    这里写图片描述

    可以看到运算的时候首先对num3进行拆箱(执行num3的longValue得到基础类型为long的值300),然后对num1和mum2进行拆箱(分别执行了num1和num2的intValue得到基础类型为int的值100和200),然后进行相关的基础运算。

    我们来对基础类型进行一个测试:

    int num1 = 100;
    int num2 = 200;
    long mum3 = 300;
    System.out.println(num3 == (num1 + num2)); //true
    • 1
    • 2
    • 3
    • 4

    上面就说明了为什么最上面会返回true.

    所以,当 “==”运算符的两个操作数都是 包装器类型的引用,则是比较指向的是否是同一个对象,而如果其中有一个操作数是表达式(即包含算术运算)则比较的是数值(即会触发自动拆箱的过程)。

    通过上面的分析我们需要知道两点: 
    1、什么时候会引发装箱和拆箱 
    2、装箱操作会创建对象,频繁的装箱操作会消耗许多内存,影响性能,所以可以避免装箱的时候应该尽量避免。

    参考文章: 
    Java自动装箱与拆箱及其陷阱

    深入剖析Java中的装箱和拆箱

     
     
  • 相关阅读:
    Linux系统教程:设置GRUB菜单密码
    vimdiff的常用命令
    Zero-Copy实现原理
    解决业务代码里的分布式事务一致性问题
    用好这6个APP,学英语SO EASY!
    线程池调优
    理解select,poll,epoll实现分析
    时序图
    性能监控-TP理解
    sshd_config OpenSSH SSH 进程配置文件配置说明
  • 原文地址:https://www.cnblogs.com/www123----/p/7834263.html
Copyright © 2011-2022 走看看