当前位置: 首页 > 科技观察

看一篇文章了解什么是Java自动拆箱

时间:2023-03-21 19:15:25 科技观察

本文主要介绍Java自动拆箱和自动装箱的相关知识。基本数据类型基本类型,或称内置类型,是Java中不同于类(Class)的特殊类型。它们是我们编程中最常用的类型。Java是一种强类型语言。变量的第一次声明必须指明数据类型,变量的第一次赋值称为变量初始化。Java有八种基本类型,可以分为三类:字符类型charboolean类型整型byte、short、int、long浮点型float、double。Java中没有无符号数值类型,它们的取值范围是固定的,不会随着机器硬件环境或操作系统的变化而改变。其实Java中还有一个基本类型void,它也有对应的包装类java.lang.Void,但是我们不能直接对它们进行操作。基本数据类型有什么好处我们都知道,在Java语言中,一个新的对象存放在堆中,我们通过栈中的引用来使用这些对象;因此,对象本身会消耗更多的资源。对于经常使用的类型,比如int等,如果我们每次使用这个变量都需要一个新的Java对象,会很麻烦。因此,和C++一样,Java提供了基本的数据类型。这个数据的变量不需要用new来创建。它们不会在堆上创建,而是直接存储在栈内存中,这样效率会更高。整数取值范围Java中的整数主要包括byte、short、int、long四种类型,表示的数字范围也是从小到大。之所以表示范围不同,主要是因为它们在存储数据个数相关时所占用的字节数。先来个简答科普,1byte=8bits(比特)。java中的整数是有符号数。我们先看看8bit在计算中可以表示的数:最小值:10000000(-128)(-2^7)***值:01111111(127)(2^7-1)这几种整数中,byte:byte以1个字节存储,范围从-128(-2^7)到127(2^7-1)。初始化变量时,字节类型默认值为0。short:short以2个字节存储,范围从-32,768(-2^15)到32,767(2^15-1)。变量初始化时,short类型的默认值为0,一般因为Java本身转换的原因,可以直接写成0。int:int以4个字节存储,范围从-2,147,483,648(-2^31)到2,147,483,647(2^31-1)。初始化变量时,int类型默认值为0。long:long以8个字节存储,范围从-9,223,372,036,854,775,808(-2^63)到9,223,372,036、854,775,807(2^63-1)。变量初始化时,long类型的默认值为0L或0l。直接写0。超出范围怎么办前面说过,在整型中,每种类型都有一定的表示范围,但是程序中的一些计算会导致超出表示范围,也就是溢出。比如下面的代码:inti=Integer.MAX_VALUE;intj=Integer.MAX_VALUE;intk=i+j;System.out.println("i("+i+")+j("+j+")=k("+k+")");输出结果:i(2147483647)+j(2147483647)=k(-2)这说明发生了溢出,发生溢出时不会抛出异常,也没有提示。因此,在程序中,使用同一类型的数据进行计算时,一定要注意数据溢出的问题。封装类型Java语言是一种面向对象的语言,但是Java中的基本数据类型并不是面向对象的,这在实际使用中有很多不便。为了解决这个问题,每一种基本数据类型都设计了一个对应的类来表示,所以把基本数据类型对应的八个类统称为包装类(WrapperClass)。包装类都位于java.lang包中。包装类与基本数据类型的对应关系如下表所示。八个类名中,除Integer类和Character类外,其余六个类的类名与基本数据类型一致。只需将类名的第一个字母大写即可。为什么需要包装类?很多人都有疑问。既然Java为了提高效率提供了八种基本数据类型,为什么还要提供包装类呢?这个问题,其实之前已经有人回答过了,因为Java是一门面向对象的语言,很多地方需要用到对象而不是基本数据类型。比如在集合类中,我们不能把int、double等类型放进去。因为集合的容器要求元素是Object类型。为了使基本类型也具有对象的特性,出现了包装类型,相当于对基本类型进行“包装”,使其具有对象的性质,并为其添加属性和方法,丰富了基本类型的操作。拆箱和装箱嗯,对于原始数据类型和包装类,有时必须在它们之间进行转换。例如,将基本数据类型的int转换为包装类型的Integer对象。我们认为包装类就是对基本类型的包装,所以把基本数据类型转换成包装类的过程就是包装。英文对应拳击,中文翻译为拳击。反之,将包装类转换为基本数据类型的过程就是拆包。英文对应unboxing,中文翻译为unboxing。在JavaSE5之前,给box,可以传入如下代码:Integeri=newInteger(10);自动拆箱和自动装箱在JavaSE5中,为了减少开发人员的工作量,Java提供了自动拆箱和自动装箱的功能。自动装箱:就是将基本数据类型自动转换成相应的包装类。自动拆箱:就是将包装类自动转换成对应的基本数据类型。Integeri=10;//自动装箱intb=i;//自动拆箱Integeri=10可以代替Integeri=newInteger(10);,这是因为Java为我们提供了自动装箱的功能,不需要开发者手动创建一个整数对象。自动装箱和自动拆箱的实现原理既然Java提供了自动拆箱的能力,那我们就来看看它的原理是什么,以及Java是如何实现自动拆箱功能的。自动拆箱我们有如下代码:publicstaticvoidmain(String[]args){Integerinteger=1;//boxinginti=integer;//unboxing}对上述代码进行反编译后,可以得到如下代码:publicstaticvoidmain(String[]args){Integerinteger=Integer.valueOf(1);inti=integer.intValue();}从上面的反编译代码可以看出,int的自动装箱是通过Integer.valueOf()方法实现的,Integer的自动拆箱是通过integer.intValue实现的。如果读者有兴趣,可以尝试反编译这八种类型,会发现如下规律:自动装箱是通过包装类的valueOf()方法实现的。自动拆箱是通过包装类对象xxxValue()来实现的。哪些地方会自动拆包拆箱?明白了原理之后,我们再来看看Java在什么情况下会自动帮我们解包拆箱。上面提到的变量初始化赋值的场景就不多介绍了,最简单也最容易理解。下面主要看看可能被忽略的场景。场景一、将基本数据类型放入集合类我们知道Java中的集合类只能接收对象类型,那么下面的代码为什么不报错呢?Listli=newArrayList<>();for(inti=1;i<50;i++){li.add(i);}反编译以上代码得到如下代码:Listli=newArrayList<>();for(inti=1;i<50;i+=2){li.add(Integer.valueOf(i));}以上,我们可以得出结论,当我们把基本数据类型放入一个集合时类,它会自动装箱。场景二:封装类型和基本类型的大小比较有没有人想过,当我们比较一个Integer对象和一个基本类型的大小时,实际上是在比较什么?看下面的代码:Integera=1;System.out.println(a==1?"Equalto":"Notequalto");Booleanbool=false;System.out.println(bool?"True":"错误的”);反编译上述代码得到如下代码:Integera=1;System.out.println(a.intValue()==1?"Equalto":"Notequalto");Booleanbool=false;System.out.println(bool.booleanValue?"true":"false");可以看出,包装类与基本数据类型的比较操作是先将包装类拆箱为基本数据类型,然后进行比较。场景三、封装类型的运算有没有人想过如何对Integer对象进行四次算术运算?看下面的代码:Integeri=10;Integerj=20;System.out.println(i+j);反编译后的代码如下:Integeri=Integer.valueOf(10);Integerj=Integer.valueOf(20);System.out.println(i.intValue()+j.intValue());我们发现两种包装类型之间的操作会自动拆箱成基本类型。场景四、三元运算符的使用这是一个很多人都不知道的场景,笔者也是在一次网上的血腥bug后才知道的。看一个简单的三元运算符的代码:booleanflag=true;Integeri=0;intj=1;intk=flag?i:j;很多人不知道,intk=flag?我:j;这条线,自动拆箱发生。反编译代码如下:booleanflag=true;Integeri=Integer.valueOf(0);intj=1;intk=flag?i.intValue():j;这其实就是三目运算符的语法规范:当第二,第三个操作数是基本类型和对象时,会将该对象拆箱为基本类型进行运算。因为在这个例子中,标志?我:j;片段中,第二段的i是包装类型的对象,第三段的j是基本类型,所以包装类会自动拆箱。如果此时i的值为null,NPE会长期发生。(自动拆箱导致空指针异常)场景5.函数参数和返回值这个比较好理解,直接上代码://自动拆箱publicintgetNum1(Integernum){returnnum;}//自动装箱publicIntegergetNum2(intnum){returnnum;}自动拆箱和缓存JavaSE的自动拆箱也提供了缓存相关的功能。我们先看下面的代码,猜猜输出结果:publicstaticvoidmain(String...strings){Integerinteger1=3;Integerinteger2=3;if(integer1==integer2)System.out.println("integer1==integer2");elseSystem.out.println("integer1!=integer2");Integerinteger3=300;Integerinteger4=300;if(integer3==integer4)System.out.println("integer3==integer4");elseSystem.out.println("integer3!=integer4");}我们一般认为以上两个判断的结果都是假的。虽然比较的值是相等的,但是因为比较的是对象,对象的引用不同,所以会认为两者if判断都是假的。在Java中,==比较对象,而equals比较值。因此,在此示例中,不同的对象具有不同的引用,因此在进行比较时两者都会返回false。奇怪的是,这里有两个相似的if条件返回不同的布尔值。上面代码的真实输出:integer1==integer2integer3!=integer4与Integer中的缓存机制有关。在Java5中,对Integer的操作引入了一项新功能,以节省内存并提高性能。整数对象通过使用相同的对象引用启用缓存和重用。适用于-128到+127范围内的整数值。仅适用于自动装箱。使用构造函数创建对象不适用。具体的代码实现可以看Java中整数缓存机制的文章,这里不再赘述。我们只需要知道,当需要自动装箱时,如果数字在-128到127之间,则直接使用缓存中的对象,而不是重新创建一个对象。里面的javadoc详细解释了缓存支持-128和127之间的自动装箱过程。***值127可以通过-XX:AutoBoxCacheMax=size来修改。事实上,在Java5中引入此功能时,范围固定为-128到+127。后来在Java6中,可以通过java.lang.Integer.IntegerCache.high设置最大值。这使得我们可以根据应用的实际情况灵活调整以提高性能。选择这个-128到127范围的原因是什么?因为这个范围的数字是使用最广泛的。在程序中,第一次使用Integer时,需要额外花费一些时间来初始化缓存。装箱转换部分中的Java语言规范(JLS)规定如下:如果变量p的值是:介于-128和127之间的整数(§3.10.1)true和布尔值false(§3.10.3)'\当u0000'和'\u007f'之间的字符范围(§3.10.4),将p包装成两个对象a和b时,可以直接用a==b来判断a的值是否和b相等。自动拆箱带来的问题当然,自动拆箱是一个非常好的功能,大大节省了开发者的精力,再也不用关心什么时候需要拆箱了。但是,他也引入了一些问题。您不能简单地使用==来比较包对象的值。虽然-128到127之间的数是可以的,但在这个范围之外还是需要用equals来比较。前面说了,有些场景会进行自动拆箱。同时我也说了,由于自动拆箱,如果包装类对象为null,自动拆箱时可能会抛出NPE。如果一个for循环中有很多拆箱操作,会浪费很多资源。【本文为专栏作家霍利斯原创文章,作者微信公众号Hollis(ID:hollishuang)】点此阅读更多本作者好文