锁定老帖子 主题:【解惑】让人头疼的"相等"关系
精华帖 (0) :: 良好帖 (0) :: 新手帖 (0) :: 隐藏帖 (0)
|
|
---|---|
作者 | 正文 |
发表时间:2009-07-18
最后修改:2010-06-18
Java中判断相等关系一般有两种手段:(1) “==”关系操作符 (2) equals()方法。 显然,基本数据类型变量之间只能用"=="。而对象之间两种手段都是合法的。但是有很多初学者会在“判断Java的相等关系”上面犯错误,这里我们在JVM运行层面上彻底剖析其中的奥秘。如果你对JVM规范不太了解的话,在看本文前请先了解一下JVM运行程序时,在内存中管理的五个运行时数据区,特别是堆和Java栈方面的知识(参见《Java 虚拟机体系结构 》) 。
★ “==”运算符的比较本质
先来看看两段源代码: //代码1:整型包装器的"=="比较 Integer n1=new Integer(1); Integer n2=new Integer(1); if(n1==n2); //false //代码2:整型变量的"=="比较 int n3=1; int n4=1; if(n3==n4); //true 代码1的结果让我们感到意外。但在解释这个现象之前,我们首先阐明一个重要的知识点: JVM运行Java程序,会在内存中会开辟一块叫做“堆 ” 的运行时数据区。在运行过程中所有创建的类对象都存放在这块区域中(准确来说是类的非静态非常量实例数据都存放在堆中)。更重要的是,这些对象的堆空间都有自己的地址,这些地址就是我们常说的 对象引用 。不管是在方法区中还是在Java栈中,存储的都是对象引用,并非对象中的数据。
下面我们看看上面两段代码在JVM中所对应的执行指令: 0 new java.lang.Integer [16] //在堆中分配一个Integer对象n1的空间,并将对象引用(堆地址)压入操作数栈 3 dup //复制对象n1的引用压入操作数栈 4 iconst_1 //将一个整型长度的常量1压入操作数栈 5 invokespecial java.lang.Integer(int) [18] //弹出整型常量1和对象n1的引用,对堆中对象n1的实例数据进行初始化 8 astore_1 [n1] //弹出对象n1的引用,并将其保存在局部变量区的第1个位置上。 9 new java.lang.Integer [16] //对象n2同上 12 dup 13 iconst_1 14 invokespecial java.lang.Integer(int) [18] 17 astore_2 [n2] //将对象n2的引用保存在局部变量区的第2个位置上 18 aload_1 [n1] //将局部变量1中的n1对象引用压入操作数栈 19 aload_2 [n2] //将局部变量2中的n2对象引用压入操作数栈 20 if_acmpne 23 //弹出操作数栈的n1,n2的引用,并比较这两个引用值是否相等。 22 return 0 iconst_1 // 将整型常量1压入操作数栈。 1 istore_3 [n3] //弹出刚压入栈的整型常量1,将其存储在局部变量区的第3个位置上 2 iconst_1 // 将整型常量1压入操作数栈。 3 istore 4 [n4] //弹出刚压入栈的整型常量1,将其存储在局部变量区的第4个位置上 5 iload_3 [n3] //将局部变量3中的整型常量1压入操作数栈 6 iload 4 [n4] //将局部变量3中的整型常量2压入操作数栈 8 if_icmpne 34 //弹出刚压入栈的两个整型常量,并比较这两个整型常量是否相等。 11 return 从代码1的字节码指令可以看出,整型包装器对象n1和n2比较的是对象引用(指令:if_acmpne 23),两个对象在堆中是两块不同的空间,自然地址是不相同的。 而代码2的字节码指令可以看出,整型变量n3和n4比较的是整型常量值,都是1,自然是相同的。
★ equals方法的比较本质
还是来看一段源代码: Integer n1=new Integer(1); Integer n2=new Integer(1); if(n1.equals(n2)); //true 下面是<Integer> equals(Object obj)方法源代码,比较的是整型值。 public boolean equals(Object obj) { if (obj instanceof Integer) { return value == ((Integer)obj).intValue(); } return false; } 是不是equals方法比较的都是对象的数据值呢?这当然和对象所属的类的equals方法是如何实现的有很大关系。我们再看看一段代码: Value v1=new Value(1); //Value是自定义类,其中并没有定义equals()方法。 Value v2=new Value(1); if(v1.equals(v2)); //false Java中Object是所有类的祖先,既然Value没有定义equals()方法。那么上面代码调用的自然是Object的equals()方法。我们看看<Object> equals(Object obj)方法源码,用"=="比较对象的引用。 public boolean equals(Object obj) { return (this == obj); } 如果我们想通过equals方法来达到比较对象中数据值的目的,就必须在指定类中自己实现equals方法来覆盖掉Object的equals方法。千万切忌,如果不覆盖,equals方法的默认行为仍然是比较对象引用。
通过上面,我们已经对"=="和"equals"的本质有了清晰地认识,但匪夷所思的事情仍然会发生。
1、String类型的特殊性造成的“相等比较”疑惑 再看看两段源代码: String s1=new String("aaaa"); String s2=new String("aaaa"); if(s1==s2); //false if(s1.equals(s2)); //true String s3="aaaa"; String s4="aaaa"; if(s3==s4); // true if(s3.equals(s4)); //true 代码4很好理解,但代码5有点让人困惑。在解释之前我们要先明确几个问题: (2) String s3="aaaa"; 是一种比较特殊的对象创建方法。它涉及到JVM管理方法区中常量池 和拘留字符串对象 的相关问题。在《String in Java 》一文中有详细的总结。 下面查看代码5中"=="比较字符串对象在JVM运行时对应的指令: 0 ldc <String "aaaa"> [13] //将常量池中"aaa"字符串常量指向的堆中拘留String对象的地址压入操作数栈 2 astore_1 //弹出栈顶值,并将其存储在局部变量区的第1个位置上 3 ldc <String "aaaa"> [13] //将常量池中"aaa"字符串常量指向的堆中拘留String对象的地址压入操作数栈 5 astore_2 //弹出栈顶值,并将其存储在局部变量区的第2个位置上 6 aload_1 //将局部变量1压入操作数栈 7 aload_2 //将局部变量2压入操作数栈 8 if_acmpne 11 //弹出两个栈顶值进行比较 很显然,"=="仍然比较的是地址。但是由于压入操作数栈的是字符串常量"aaa"所指向的同一个拘留String对象的地址。因此s3和s4保存的是相同的地址,自然"=="的比较结果也是相同的。
2、Integer类型的自动打包 (autoboxing)机制造成的“相等比较”疑惑 继续看两段代码 Integer a=127; Integer b=127; if(a==b); //结果:true Integer c=128; Integer d=128; if(c==d); //结果:false 代码6和代码7几乎一样的语句竟然有不同的结果,实在是很困惑。在解释这个问题前仍然要阐明几点:
但还是没有解决代码7,8不同的疑惑呀?下面我们来看看Integer.valueOf(int)的源代码: /** * Returns a <tt>Integer</tt> instance representing the specified * <tt>int</tt> value. * If a new <tt>Integer</tt> instance is not required, this method * should generally be used in preference to the constructor * {@link #Integer(int)}, as this method is likely to yield * significantly better space and time performance by caching * frequently requested values. * * @param i an <code>int</code> value. * @return a <tt>Integer</tt> instance representing <tt>i</tt>. * @since 1.5 */ public static Integer valueOf(int i) { final int offset = 128; if (i >= -128 && i <= 127) { // must cache return IntegerCache.cache[i + offset]; } return new Integer(i); } private static class IntegerCache { private IntegerCache(){ } static final Integer cache[]= new Integer[-(-128) + 127 + 1]; static { for(int i = 0; i < cache.length; i++) cache[i] = new Integer(i - 128); } }
看看Integer的源代码就知道了,其实Interger把 -128~127之间的每一个值都建立了一个对应的Integer对象,并将这些对象组织成cache数组,类似一个缓存。
这个缓存数组中的Integer对象是可以安全复用的。也就是Integer a=127;和Integer b=127;中的引用a,b都是缓存数组中new Integer(127)对象的地址。所以代码6中的a==b自然是true。
声明:ITeye文章版权属于作者,受法律保护。没有作者书面许可不得转载。
推荐链接
|
|
返回顶楼 | |
浏览 2314 次