【Java面试】---装箱拆箱问题汇总

来源:互联网 发布:windows nt 4.0 sp6 编辑:程序博客网 时间:2024/06/08 04:48

刷题的时候经常能遇到一些关于装箱,拆箱,还有内存的一些问题,现在做一个总结吧,重点倾斜向String类和一些基本数据类型及它们对应的包装类型,主要涉及到编译器优化的问题。

Java中String不是基本类型,但是有些时候和基本类型差不多,如String b = “tao” ; 可以对变量直接赋值,而不用 new 一个对象(当然也可以用 new)。

String部分分析

栈内存和堆内存

Java中的变量和基本类型的值存放于栈内存,而new出来的对象本身存放于堆内存,指向对象的引用还是存放在栈内存。例如如下的代码:

int  i=1;String s =  new  String( "Hello World" );

变量i和s以及1存放在栈内存,而s指向的对象”Hello World”存放于堆内存。

这里写图片描述

栈内存的数据共享

栈内存的一个特点是数据共享,这样设计是为了减小内存消耗,前面定义了i=1,i和1都在栈内存内,如果再定义一个j=1,此时将j放入栈内存,然后查找栈内存中是否有1,如果有则j指向1。如果再给j赋值2,则在栈内存中查找是否有2,如果没有就在栈内存中放一个2,然后j指向2。也就是如果该常量以及在栈内存中,就将变量指向该常量,如果没有就在该栈内存增加一个该常量,并将变量指向该常量。

这里写图片描述

如果j++,这时指向的变量并不会改变,而是在栈内寻找新的常量(比原来的常量大1),如果栈内存有则指向它,如果没有就在栈内存中加入此常量并将j指向它。这种基本类型之间比较大小和我们逻辑上判断大小是一致的。如定义i和j是都赋值1,则i==j结果为true。==用于判断两个变量指向的地址是否一样。i==j就是判断i指向的1和j指向的1是同一个吗?当然是了。对于直接赋值的字符串常量(如String s=“Hello World”;中的Hello World)也是存放在栈内存中,而new出来的字符串对象(即String对象)是存放在堆内存中。如果定义String s=“Hello World”和String w=“Hello World”,s==w吗?肯定是true,因为他们指向的是同一个Hello World。
这里写图片描述

堆内存非数据共享

堆内存没有数据共享的特点,前面定义的String s = new String( “Hello World” );后,变量s在栈内存内,Hello World 这个String对象在堆内存内。如果定义String w = new String( “Hello World” );,则会在堆内存创建一个新的String对象,变量w存放在栈内存,w指向这个新的String对象。堆内存中不同对象(指同一类型的不同对象)的比较如果用==则结果肯定都是false,比如s==w?当然不等,s和w指向堆内存中不同的String对象。如果判断两个String对象相等呢?用equals方法。

这里写图片描述

题目分析(String)

public class StringDemo{  private static final String MESSAGE="taobao";  public static void main(String [] args) {    String a ="tao"+"bao";    String b="tao";    String c="bao";    System.out.println(a==MESSAGE);    System.out.println((b+c)==MESSAGE);  }}

MESSAGE 成员变量及其指向的字符串常量肯定都是在栈内存里的,变量 a 运算完也是指向一个字符串“ taobao ”啊?是不是同一个呢?这涉及到编译器优化问题。对于字符串常量的相加,在编译时直接将字符串合并,而不是等到运行时再合并。也就是说String a = “tao” + “bao” ;和String a = “taobao” ;编译出的字节码是一样的。所以等到运行时,根据上面说的栈内存是数据共享原则,a和MESSAGE指向的是同一个字符串。而对于后面的(b+c)又是什么情况呢?b+c只能等到运行时才能判定是什么字符串,编译器不会优化,想想这也是有道理的,编译器怕你对b的值改变,所以编译器不会优化。运行时b+c计算出来的”taobao”和栈内存里已经有的”taobao”是一个吗?不是。b+c计算出来的”taobao”应该是放在堆内存中的String对象。这可以通过System. out .println( (b+c)== MESSAGE );的结果为false来证明这一点。如果计算出来的b+c也是在栈内存,那结果应该是true。Java对String的相加是通过StringBuffer实现的,先构造一个StringBuffer里面存放”tao”,然后调用append()方法追加”bao”,然后将值为”taobao”的StringBuffer转化成String对象。StringBuffer对象在堆内存中,那转换成的String对象理所应当的也是在堆内存中。
1,下面改造一下这个语句

 System. out .println( (b+c).intern()== MESSAGE );

结果是true, intern() 方法会先检查 String 池 ( 或者说成栈内存 ) 中是否存在相同的字符串常量,如果有就返回。所以 intern()返回的就是MESSAGE指向的”taobao”。

2,再把变量b和c的定义改一下

final  String b =  "tao" ;final  String c =  "bao" ;System. out .println( (b+c)== MESSAGE );

现在b和c不可能再次赋值了,所以编译器将b+c编译成了”taobao”。因此,这时的结果是true。在字符串相加中,只要有一个是非final类型的变量,编译器就不会优化,因为这样的变量可能发生改变,所以编译器不可能将这样的变量替换成常量。例如将变量b的final去掉,结果又变成了false。这也就意味着会用到StringBuffer对象,计算的结果在堆内存中。

如果对指向堆内存中的对象的String变量调用intern()会怎么样呢?实际上这个问题已经说过了,(b+c).intern(),b+c的结果就是在堆内存中。对于指向栈内存中字符串常量的变量调用intern()返回的还是它自己,没有多大意义。它会根据堆内存中对象的值,去查找String池中是否有相同的字符串,如果有就将变量指向这个string池中的变量。

String a = "tao"+"bao";String b = new String("taobao");System.out.println(a==MESSAGE); //trueSystem.out.println(b==MESSAGE);  //falseb = b.intern();System.out.println(b==MESSAGE); //trueSystem. out .println(a==a.intern());  //true

题目分析2(String)

有以下代码片段:String str1="hello";String str2="he"+ new String("llo");System.out.println(str1==str2);请问输出的结果是:

这里的str1指的是方法区中的字符串常量池中的“hello”,编译时期就知道的;

String str2 = “he” + new String(“llo”);
这里的str2必须在运行时才知道str2是什么,所以它是指向的是堆里定义的字符串“hello”,所以这两个引用是不一样的。如果用str1.equal(str2),那么返回的是true;因为String类重写了equals()方法。编译器没那么智能,它不知道”he” + new String(“llo”)的内容是什么,所以才不敢贸然把”hello”这个对象的引用赋给str2.
如果语句改为:”he”+”llo”这样就是true了。new String(“zz”)实际上创建了2个String对象,就是使用“zz”通过双引号创建的(在字符串常量池),另一个是通过new创建的(在堆里)。只不过他们的创建的时期不同,一个是编译期,一个是运行期。
String s = “a”+”b”+”c”;语句中,“a”,”b”, “c”都是常量,编译时就直接存储他们的字面值,而不是他们的引用,在编译时就直接将它们连接的结果提取出来变成”abc”了。

Integer部分分析

基本函数

首先 通过一道例题分析看几个基本函数表示什么:

设有下面两个赋值语句:a = Integer.parseInt("1024");b = Integer.valueOf("1024").intValue();下述说法正确的是()A,  a是整数类型变量,b是整数类对象。B,  a是整数类对象,b是整数类型变量。C,  a和b都是整数类对象并且它们的值相等。D,  a和b都是整数类型变量并且它们的值相等。

涉及的函数表示意义如下:
- intValue()是把Integer对象类型变成int的基础数据类型
- parseInt()是把String 变成int的基础数据类型
- ValueOf()是把String 转化成Integer对象类型;(现在JDK版本支持自动装箱拆箱了。)
本题:parseInt得到的是基础数据类型int,valueof得到的是装箱数据类型Integer,然后再通过valueInt转换成int,所以选择D

包装类型

包装类型有以下几种:

这里写图片描述

类型转换

下面赋值语句中正确的是()A  double d=5.3e12;B  float f=11.1;C  int i=0.0;D  Double oD=3

不加任何后缀
整型默认为 int 浮点默认为 double

A,科学计数表示方法,需要注意的是表示范围是否越界
B.double–>float 精度丢失,需要强转
C.double–>int 精度丢失,需要强转
D.3是int类型,Double是包装器类型.无法两次转型,应该表示为

double d= 3; 自动转型,int-->doubleDouble d = (double) 3; 强转+自动装箱

类型比较

通过一道例题来分析以下“==”和“equals”的相关比较操作

Integer i = 42;Long l = 42l;Double d = 42.0;

下面为true的是

A   (i == l)B   (i == d)C   (l == d)D   i.equals(d)E   d.equals(l)F   i.equals(l)G  l.equals(42L)

三条原则:

1,对于值类型来说比较大小就可以了,对于引用类型来说来说==比较的是地址,equals比较的是内容
2,包装类的“==”运算在不遇到基本类型的情况下不会自动拆箱
3,包装类的equals()方法不处理数据转型
分析上题,ABC选项,都是包装类,引用类型,比较的是地址,类型不同,编译错误
DEF选项,因为equals方法不处理数据转型,所以是比较类型,所以是错的
G是先对42L装箱,然后比较类型是否相同,如果相同,再比较值大小是否相同,显然都相同

总结

1、基本型和基本型封装型进行“==”运算符的比较基本型封装型将会自动拆箱变为基本型后再进行比较,因此Integer(0)会自动拆箱为int类型再进行比较,显然返回true;
int a = 220;
Integer b = 220;
System.out.println(a==b);//true

2,两个基本型的==操作就是比值(不管是不是同类型的基本型),但两个封装型的会先判断类型,类型不同报错,即使是同一类型也为false,因为引用指向的地址不同。(两个Integer的比较是特例)

特殊情况

两个Integer类型(Integer.valueOf)进行“==”比较如果其值在-128至127 ,那么返回true,否则返回false, 这跟Integer.valueOf()的缓冲对象有关
Integer c=3;
Integer h=3;
Integer e=321;
Integer f=321;
System.out.println(c==h);//true
System.out.println(e==f);//false
这个方法就是返回一个 Integer 对象,只是在返回之前,看作了一个判断,判断当前 i 的值是否在 [-128,127] 区别,且 IntegerCache 中是否存在此对象,如果存在,则直接返回引用,否则,创建一个新的对象。 创建新对象后当然就是不同的引用了。源代码如下:

public static Integer valueOf(inti) {      assertIntegerCache.high>=127;      if(i >= IntegerCache.low&& i <= IntegerCache.high)             return  IntegerCache.cache[i+ (-IntegerCache.low)];      return  new  Integer(i); }

但无论如何,Integer与new Integer不会相等。不会经历拆箱过程

package test;/** * @author 田茂林 * @data 2017年9月6日 下午9:46:42 */public class TestFinally {        public static void main(String[] args) {          Integer i= 57;          Integer l = new Integer(57);          if(i==l){              System.out.println("true");          }else{              System.out.println("false");   //输出为false          }        }    }

特殊情况结束

4、两个基本型的封装型进行equals()比较,首先equals()会比较类型,如果类型相同,则继续比较值,如果值也相同,返回true。
Integer a=1;
Integer b=2;
Integer c=3;
System.out.println(c.equals(a+b));//true

5、基本型封装类型调用equals(),但是参数是基本类型,这时候,先会进行自动装箱,基本型转换为其封装类型,再进行3中的比较。
int i=1;
int j = 2;
Integer c=3;
System.out.println(c.equals(i+j));//true
I==L
6,基本型不能调用equals()方法,否则编译会报错

再来一个例题:

package test;/** * @author 田茂林 * @data 2017年9月6日 下午9:46:42 */public class TestFinally {        public static  void add(Byte b)        {            b = b++;        }        public static void main(String[] args) {            Byte a = 127;            Byte b = 127;            add(++a);            System.out.print(a + " ");            add(b);            System.out.print(b + "");        }    }

运行结果

-128 127

public void add(Byte b){ b=b++; } 这里涉及java的自动装包/自动拆包(AutoBoxing/UnBoxing) Byte的首字母为大写,是类,看似是引用传递,但是在add函数内实现++操作,会自动拆包成byte值传递类型,所以add函数还是不能实现自增功能。也就是说add函数只是个摆设,没有任何作用。 Byte类型值大小为-128~127之间。 add(++a);这里++a会越界,a的值变为-128 add(b); 前面说了,add不起任何作用,b还是127

原创粉丝点击