细读源码之Integer

我们知道Java是面向对象的语言,号称一切皆对象,但是有8种原始数据类型(boolean、byte 、short、char、int、float、double、long)需要除排除在外。

在面试过程中经常会遇到,考察原始数据类型和其包装类语言特性的问题。

本文就以原始数据类型int和其包装类Integer为例进行讲解,主要包含以下几个方面的内容:

1.Integer的不可变性

2.自动装箱和自动拆箱(boxing/unboxing)

3.Integer的缓存值

4.使用Integer注意的事项

一.Integer的不可变性

A.什么是不可变对象?

如果一个对象,在它创建完成之后,不能再改变它的状态,那么这个对象就是不可变的。

前面讲解的字符串String是不可变对象,另外8种包装类型也都是不可变对象,其中自然包括Integer。

B.Integer如何实现不可变

1.Integer类定义使用final修饰,不能被继承,这样Integer的所有方法就不能被重写;

2.存储数据的字段声明为private final int value,使用private final修饰,value的值构造函数初始化后不能被修改。

C.Integer不可变带来的好处

1.Integer只有设计成不可变的对象,才能为其建立缓存值(后面会相信讲解),以达到节约内存的目的;

2.Integer是不可变的,必然是线程安全的,这样同一个Integer对象就可以被多个线程安全地共享,而且不需要任何同步操作;

3.Integer是不可变的,可以保证信息的安全。比如我们使用Integer来保存服务器某个服务的端口,如果我们可以轻易地把Integer对象改变为其他数值,这会给产品的可靠性带来严重的问题。

D.举例说明:

 上面代码执行结果:

 number和numberToIncrease两个变量的内存示意图,如下图所示:

1. 执行number = 1的时候,number执行value = 1的Integer对象;

2. 调用increase方法,number作为实参传给numberToIncrease,此时 number和numberToIncrease都指向了value = 1的Integer对象;

3. 执行numberToIncrease = numberToIncrease + 1;numberToIncrease就指向了value = 2的Integer对象,但是number的指向不变,还是原来value = 1的Integer对象。

最终,有了上面的执行结果。

二.自动装箱和自动拆箱(boxing/unboxing)

Java中的自动装箱和拆箱操作是通过语法糖实现的。什么是语法糖?

语法糖,是指计算机语言中添加的某种语法,这种语法对语言的功能并没有影响,但是更方便程序员使用。

通常来说使用语法糖能够增加程序的可读性,从而减少程序代码出错的机会。

在讲解自动装箱和自动拆箱操作之前,先说明一下装箱和拆箱的概念:

装箱:把Java原始数据类型(如:int)转化为其对应的包装类型(如:Integer)的过程我们称为装箱操作;

拆箱:把Java包装类型(如:Integer)转化为其对应的原始数据类型(如:int)的过程我们称为拆箱操作。

下面举个例子,说明一下装箱和拆箱的操作,例1:

1. main方法的第一行,代码中的0是int类型,当赋值给Integer类型的sum变量时,调用了Integer.valueOf将int类型转换成Integer类型,这个过程就是装箱;

2. main方法的第二行,sum要进行加法操作时,Integer类型无法直接进行加法操作,先执行sum.intValue()变成int后,再进行加法操作,而转化为int的过程就是拆箱;

3. main方法的第二行,当需要把加1的结果,再赋值给sum的时,再次调用Integer.valueOf进行类型转换,又进行了一次装箱操作。

当然平时我们很少会写上面那种臃肿的代码,常用的写法如下,例2:

 但是我们查看这两个类main方法的字节码,结果完全一致,如下所示:

从上面的字节码可以看出:

虽然例2中的代码中没有显式地去调用Integer.valueOf和Integer.intValue方法。

但是编译后的class文件中,却有这两个方法的隐式调用,而这个隐式调用过程就是自动装箱和自动拆箱的操作。

自动装箱:当一个Integer类型的值,需要变成int的时候(比如要进行加法运算),Java编译器会加入Integer.intValue()的方法调用,将Integer类型自动转换成int;

自动拆箱:当一个int的值,需要变成Integer的时候(比如把int类型的值赋值给Integer),Java编译器会加入一段Integer.valueOf(int i)的方法调用,把int类型自动转换为Integer类型。

三.Integer的缓存值

关于Integer的值缓存,涉及到Java 5的一个改进。在Java 5之前,构建Integer对象的传统方式是,直接调用其构造函数创建出一个新的对象。

但是根据实践的结果,我们发现大部分int数值运算的结果都集中在有限的、较小的数值范围内。

因此,在Java 5在Integer类上新增了一个valueOf的静态工厂方法,在调用它的时候会利用一个缓存机制,最终带来了明显的性能改进。

我们先看看下面的代码,猜测一下代码执行的结果,代码如下:

代码执行结果如下:

当没有读过Integer的源码,看到上面的结果,是不是会很惊讶。

为什么a和b赋值为1,进行==判断返回true,而c和d赋值为128,进行==判断就返回false了呢?

前面了解了自动装箱操作后,我们知道,当把int型的值,赋值为Integer类型的时候,会调用Integer.valueOf进行自动装箱操作,奥秘应该就是在Integer.valueOf方法中,源码如下:

当传入的参数i大于等于IntegerCache.low并且小于等于IntegerCache.high时,则从IntegerCache.cache中取值;

而其他情况下则新创建一个Integer对象。

IntegerCache的low和high是多少,还有cache又是什么呢,接着读一下IntegerCache的源码:

通过上面的代码,我们得出下面结论:

1.low的值是固定为-128;

2.high的默认值是127,但是可以通过java.lang.Integer.IntegerCache.high这个property进行设置。

最终取127和设置值中的较大值,并且取Integer.MAX_VALUE - (-low) -1和设置值中的较小值,作为最终的high的值;

3.最后根据low和high的值,对cache进行初始化,其cache[0] = -128,cache[cache.length - 1] = high。

还是上面的代码,如果加上JVM参数-Djava.lang.Integer.IntegerCache.high=128后再次执行,结果如下:

通过JVM参数,IntegerCache缓存的最大值设置为128,128也进行了缓存,c == d就由原来的false变成了true。

四.使用Integer注意的地方

1.使用int类型替换Integer进行数值计算

Integer类型无法直接进行数值计算,在计算之前需要进行拆箱变成int后进行计算,在计算之后赋值给Integer类型的时候又要进行装箱操作。

大量的装箱和拆箱操作非常浪费CPU和内存,下面代码对比一下二者的效率,代码如下:

上面代码执行结果如下所示:

computeByInt方法中,直接对int变量进行操作;

而computeByInteger方法中,sum = sum + 1,有一次自动装箱和一次自动拆箱操作,极大地影响了性能。

从最终的结果来看,两者之前有成千上万倍的性能差异。

2.使用int[]数组替换Integer[]和ArrayList

我们知道Java的对象都是引用类型,如果是一个原始数据类型数组,它在内存里是一段连续的内存;

而对象数组则不一样,数据存储的是引用,对象往往是分散地存储在堆的不同位置。

这种设计虽然带来了极大灵活性,但是也导致了数据操作的低效,尤其是无法充分利用现代CPU缓存机制。

下面举个例子说明二者性能的差异:

上面代码运行结果如下:

从运行结果上看,sumInt方法和sumInteger两个方法的性能差异巨大,主要有两个原因:

1. sumInteger进行加法操作的时候,多了一次拆箱操作;

2. int[]数组内数据存储是连续的,可以充分利用现代CPU缓存机制,而Integer[]中每个元素的intValue值,就分散地存储在堆的不同位置,无法充分利用CPU缓存机制,导致性能损失。

所以,使用原始数据类型替换包装类,使用数组替换动态数组(如ArrayList),在性能极度敏感的场景往往具有比较大的优势,一些追求极致性能的产品或者类库,会极力避免创建过多对象。

当然,在大多数纯业务功能代码里,并没有必要这么做,还是以开发效率优先。

©著作权归作者所有,转载或内容合作请联系作者
【社区内容提示】社区部分内容疑似由AI辅助生成,浏览时请结合常识与多方信息审慎甄别。
平台声明:文章内容(如有图片或视频亦包括在内)由作者上传并发布,文章内容仅代表作者本人观点,简书系信息发布平台,仅提供信息存储服务。

相关阅读更多精彩内容

  • 1 前言 我们都知道,java提供了8种基本数据类型供我们使用,此外,java还提供了这些基本数据类型所对应的包装...
    l1fe1阅读 3,532评论 0 1
  • 1. 描述 API:int的包装类型,内部包含了一个int字段。 关键字段: 2. 构造函数 第一种 直接赋值:传...
    Oliver_Li阅读 1,765评论 0 0
  • Integer类将基础类型int包装到了对象中,形成一个引用类型。它提供的主要功能是与int相关的类型封装与转换。...
    LuckyBuzz阅读 4,388评论 1 2
  • 1.典型回答 int 是基本数据类型(Primitive Types),是 java 的8个基本数据类型之一,ja...
    憩在河岸上的鱼丶阅读 3,440评论 0 2
  • Integer 本文源码基于JDK8 Integer也是我们经常使用的工具类、包装类,此文主要用于记录学习笔记,主...
    luoyoub阅读 1,751评论 1 0

友情链接更多精彩内容