java语法糖--自动装箱和拆箱详解

JAVA中最常用的语法糖--自动装箱和拆箱

(本文中的所有源码对应的版本均是JDK1.8)

(本文中使用的反编译工具是JD_JUI和XJad)

目录

JAVA中最常用的语法糖--自动装箱和拆箱

一,究竟什么是自动装箱和拆箱

1.1  我们直接来看一段非常简单的实例代码:

1.2  带着问题,反编译

1.3  探索的结果

二,自动装拆箱的方法探索

 2.1  Integer下的intValue()

 2.2  Integer下的valueOf(int)方法--重中之重

 2.3  装箱优化下的Cache

三,涉及等值判断和算数运算的情况

 3.1  第一个实例

  3.2  第二个实例

四,Trove4j解决jdk自带的集合自动装拆箱的问题



一,究竟什么是自动装箱和拆箱

1.1  我们直接来看一段非常简单的实例代码:

    public static void main(String[] args)
    {
        int intValue = 100;
        Integer boxValue = intValue;
        int value = boxValue;
    }

相信上述代码是每个java程序员都能看懂的,但是,问题来了:

  • 为什么基本类型int和封装的类型之间可以直接等号赋值?本质是什么?

1.2  带着问题,反编译

 public static void main(String[] args) {
    int intValue = 100;
    Integer boxValue = Integer.valueOf(intValue);
    int value = boxValue.intValue();
  }
  • 当把基本类型int直接等号赋值给Integer包装类的时候,实际上在编译过程中,编译器多做了一步额外的操作,就是反编译示例中的Integer.valueOf(intValue)。这一步就是自动装箱的语法糖,稍后我们进到该方法中继续探索。
  • 而对应的,当把Integer直接等号赋值给int基本类型的时候,也是如此,编译器替我们执行了一步boxValue.intValue()操作。这一步就是自动拆箱的语法糖。
  • 实际上,所谓的语法糖,自动装箱,拆箱都只是一个名词定义而已,不用纠结于名词本身,我们只关心原理。

1.3  探索的结果

在编译阶段,编译器替我们进行了一些额外的操作(语法糖的本质),使得JAVA中的基本数据类型和其对应的包装类之间可以隐式的进行装换。这就是自动装箱和拆箱的本质,所谓的自动,就是编译器的隐式操作。

那么,借此机会,回顾一下,JAVA中的基本数据类型及其包装类。

 

二,自动装拆箱的方法探索

 2.1  Integer下的intValue()

首先,必须知道Integer的一个核心数据结构,其内部有一个int类型的成员变量。这也是包装类的设计需求。

上述是基本类型对应包装类的基本设计,很简单,就是包装类中维护了一个对应的基本类型,然后提供构造方法,将外部基本类型的值赋值给其内部维护的这个对应的成员变量。

下面来看intValue()方法,即int value = boxValue.intValue()这一步的语法糖。

毫无疑问,就是这么简单,但是仔细想想,这一步的操作有一个启发,就是拆箱操作基本没有任何的性能损耗。

 2.2  Integer下的valueOf(int)方法--重中之重

装箱的过程不再多说,直接进入到这个核心方法中来:

  • 这一步是装箱过程的关键,也是很多程序为了追求效率的极致,所摒弃的自动装箱的原因。 
  • 该方法有两个出口,第二个出口很简单,直接new出来一个包装类,然后将外部基本类型传入。那么这种方式的弊端显而易见了,我必须创建一个对应的对象,这个过程无可厚非,但是如果一个大型程序中涉及大量的装箱操作,必然会带来大量的包装类的创建(原因很简单,这种情况的使用频率相当之高),从而影响整个程序的GC,进而影响性能。
  • 那么另一个出口的作用是什么呢?很明显,JVM的开发人员也看到了这种装箱操作所带来的性能问题,所以,对其进行了一步优化,优化的思路非常简单,对一部分的int数值下的包装类做缓存CaChe,当传递进来的基本类型的int值在这个Cache的范围中,则直接返回cache中对应的Integer,避免重复大量的new操作所带来的性能损耗。

 2.3  装箱优化下的Cache

  •  首先,这个缓存区是在Integer的包装类中的内部类,私有静态。

  • 我们来看其内部的成员变量,很容易理解,low和high代表的是缓存的两个边界,cache就是具体的缓存区,是一个Integer包装类的静态数组,后续会看到这个cache的大小,就是恰好包含两个边界之间的所有数值。

  • 这里有一个细节,low这个下边界有初始值,但是high没有初始值,是因为high可以被我们手动设置,而low目前是固定的,不能被改变。

 缓存类中,有且仅有这一个无参私有的构造方法,并且在自身内部没有任何创建实例的地方,换句话说,这个类在任何时间任何地点都没有实例,这就说明了,其内部的所有成员变量全部是静态的,且自身也是一个静态内部类。

这个静态代码块是这个缓存的核心执行代码块了,并且jdk1.8中目前的这个缓存类有且仅有以上介绍的这三部分组成。

  • 第一件事,就是给high赋值。high默认是127,也就是说,在没有人为赋值的情况下,这个缓存的上边界是127。但是这个数值是可以手动设置改变的。实现手动赋值的方式是,设置截图中所示的这一个系统变量,也就是系统参数。从代码中很明显的看到手动设置这个上边界是有限制的,首先不能是127以下的值,否则设置不设置毫无意义,最后都还是127。其次,设置的这个参数不能大于Inteeger.Max-129。至于为什么不能大于这个值,后续会看到,开发者是以一个int类型的值作为缓存数组的长度的。
  • 第二件事,就是实现缓存了,也就是初始化其内部的这个缓存数组cache,实现的方式,很简单,for循环,从low到high,一次new一个Integer,然后将low到high中的每一个值一次赋值给新创建的Integer。所以说,从这里可以看出来,这是有偏移量的,,,缓存的数值范围是low到high,但是数组下标是从0开始的,所有数组中的每一个元素的数组下标和元素内部的数值都恰好有一个low大小的偏移量。例如,cache[0]中存储的数值是-128,依次类推。
  • 而静态代码块的最后来了一个assert的语法,用来检验后面的这个布尔表达式是否正确,也就是一步强制检测,要求这个缓存的范围必须最小为[-128,127],其上边界必须大于等于127。否则,程序抛出异常并且强制停止。其实,这一步有些莫名其妙,感觉完全没有必要。
  • 回过头看,valueOf()方法,当在缓存范围内的时候,想要获取int数值对应的Integer的时候,在计算数组下标的时候,是有一步偏移量的计算的,现在也可以理解是为什么了。

 

三,涉及等值判断和算数运算的情况

 3.1  第一个实例

    public static void main(String[] args)
    {
        int intValue = 100;
        
        Integer boxValue = intValue;
        int value = boxValue;
        Integer boxValue2 = 100;
        
        Integer boxValue3 = 150;
        Integer boxValue4 = 150;

        System.out.println(boxValue == value);
        System.out.println(boxValue.equals(value));
        System.out.println(boxValue == boxValue2);
        System.out.println(boxValue3 == boxValue4);
    }

  • 这里来说一下最后一个判断,boxValue3和boxValue4,进行双等号判断的时候,返回结果为false,现在我们也能理解原因了,150这个数值已经超过了默认的high值127,所以缓存池中没有这个数值,以致于每次使用都要new出来一个新的,所以进行双等号判断的时候,自然就不是用一个实例,从而返回false了。
  • 而前面的判断。由于int值是100,所以可以存cache中获取,从而导致获取的都是同一个示例,所以自然而然双等号判断的结果也就是true了。

我们再来看一下这个示例反编译的结果,主要看双等号和equals判断是否涉及装箱和拆箱

可以看到,当一个Integer的包装类和一个int基本类型进行值比较的时候,无论使用的是双等号(第一个红框的源码)判断,还是使用的是equals(第二个红框的源码)。其结果都是将其中一种类型进行装箱或者拆箱,,,这也就是我们开头所说的,自动装箱和拆箱可以算是java语法中出现的最多的语法糖了,虽然很简单,但是了解其原理还是很有必要的。

  3.2  第二个实例

    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;

        System.out.println(c == d);
        System.out.println(e == f);
        System.out.println(c == (a + b));
        System.out.println(c.equals(a + b));
        System.out.println(g == (a + b));
        System.out.println(g.equals(a + b));

    }

 很有意思,我们反编译来看为什么最终的输出结果好像和我们想的不一样。

 

  • 对比①②③,需要知道,双等号对于两个包装类型的判断,在正常情况的时候,是直接比较地址的,这个好理解。但是,一旦双等号的两端出现算数运算,则全部拆箱,全部装换成了基本类型的比较,例如图中的③
  • 对比③④⑤⑥,需要知道,任何地方,只要有算数运算,参与算数运算的包装类都要拆包,且最后的算数运算表达式的结果是一个int基本类型,如果后续需要Integer的操作,还需要再装箱,例如图中的④
  • 对比⑤⑥,我们发现其结果一个是true,一个是false。这就给到我们启示,双等号对于基本类型的判断是会处理隐式强制的类型装换的,但是,equals方法的参数中如果出现了自动装箱,是不会隐式的执行类型装换的,哪怕是从int向上转成long都是不处理的。其实并不是equals的问题,而是包装类无法进行类型的装换,导致了上述结果。
  • 由此来看,自动装箱拆箱除了带来不直接让我们看到的性能问题,还会有一些隐式的强规则的陷阱,所以,了解自动装箱和拆箱的语法糖还是很有必要的。

四,Trove4j解决jdk自带的集合自动装拆箱的问题

 4.1  TIntArrayList和ArrayList的对比

其实,这个思路很简单,trove4j并不是对基本类型和包装类型的装箱和拆箱操作的过程进行了优化,而是直接在集合类中直接抛弃了包装类的泛型化设计,为每一种基本数据类型的集合单独做一个专属的集合封装类,从而避免了基本类型和包装类型之间的装换问题。

  • 看上述第一个截图中的ArrayList,其内部的核心数据结构是一个Object的数组,之所以这样设计,就是为了可以支持泛型,从而避免重复逻辑的代码多次重写。而为了支持泛型,也就要求在设计的时候能够兼容所有的类型,而这就出现了Object的数组,只有Object引用可以兼容所有的类,到此,我们也就理解了,JAVA的jdk中自带的集合类,为什么只支持包装类,而不支持基本类型,因为基本数据类型无法支持泛型化设计。
  • 而Trove4j恰好又采用了另外一种设计思路,抛弃泛型化支持,从而避免Object的包装类的限制,对每一种基本数据类型都写一个只属于自己的集合类,比如int类型对应的是TIntArrayList,而long类型对应的是TLongArrayList。抛弃了泛型化设计,就避免了只能是包装类的尴尬情况,从而直接在底层支持基本类型,就像第二个截图中的int基本类型数组,这也就避免了集合使用过程中的自动装箱拆箱所带来的一系列问题。
  • 显然,在追求性能和效率的情况下,对于基本类型的集合类使用,使用Trove4j要比直接使用jdk中自带的集合类好的多,但是,当元素本身就是包装类而非基本类型的时候,jdk自带的集合类也就没有了自动装箱拆箱的问题了。

此处只是以很简单的例子说明了对集合类的自动装箱拆箱优化的一种思路,并且简单的举出了一个例子进行对比,从而较为直观的看到两者的不同,从而最终探究原理,提出泛型化设计给自动装拆箱带来的问题,至于Trove4j本身的使用和各种集合类的具体代码逻辑不在这个话题中讨论了,有兴趣的同学自行学习。


到此,相信我们也对JAVA中基本类型的装拆箱有了一定的了解,其实仔细深入源码进行探究,这个问题相当简单,但是,勿以事小而不为, 对技术的追求和成就感才是真正让我们坚持在互联网行业的动力。

  • 2
    点赞
  • 7
    收藏
    觉得还不错? 一键收藏
  • 2
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值