【解惑】让人头疼的"相等"关系

Java中判断相等关系一般有两种手段:(1) “==”关系操作符  (2) equals()方法。 显然,基本数据类型变量之间只能用"=="。而对象之间两种手段都是合法的。但是有很多初学者会在“判断Java的相等关系”上面犯错误,这里我们在JVM运行层面上彻底剖析其中的奥秘。如果你对JVM规范不太了解的话,在看本文前请先了解一下JVM运行程序时,在内存中管理的五个运行时数据区,特别是堆和Java栈方面的知识(参见《Java 虚拟机体系结构 》) 。

★ “==”运算符的比较本质

先来看看两段源代码:

Java代码

//代码1:整型包装器的"=="比较
Integer n1=new Integer(1);
Integer n2=new Integer(1);
if(n1==n2); //false

Java代码

//代码2:整型变量的"=="比较
int n3=1;
int n4=1;
if(n3==n4); //true 

代码1的结果让我们感到意外。但在解释这个现象之前,我们首先阐明一个重要的知识点:

JVM运行Java程序,会在内存中会开辟一块叫做“堆 ” 的运行时数据区。在运行过程中所有创建的类对象都存放在这块区域中(准确来说是类的非静态非常量实例数据都存放在堆中)。更重要的是,这些对象的堆空间都有自己的地址,这些地址就是我们常说的 对象引用 。不管是在方法区中还是在Java栈中,存储的都是对象引用,并非对象中的数据。

下面我们看看上面两段代码在JVM中所对应的执行指令:

代码1的字节码指令代码

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

代码2的字节码指令代码

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方法的比较本质

还是来看一段源代码:

代码3:整型包装器的equals方法代码

Integer n1=new Integer(1);
Integer n2=new Integer(1);
if(n1.equals(n2)); //true

下面是<Integer> equals(Object obj)方法源代码,比较的是整型值。

Integer.equals源代码代码

public boolean equals(Object obj) {
     if (obj instanceof Integer) {
           return value == ((Integer)obj).intValue();
     }
     return false;
}

是不是equals方法比较的都是对象的数据值呢?这当然和对象所属的类的equals方法是如何实现的有很大关系。我们再看看一段代码:

自定义类value的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)方法源码,用"=="比较对象的引用。

Java代码

public boolean equals(Object obj) {
     return (this == obj);
}

如果我们想通过equals方法来达到比较对象中数据值的目的,就必须在指定类中自己实现equals方法来覆盖掉Object的equals方法。千万切忌,如果不覆盖,equals方法的默认行为仍然是比较对象引用。

通过上面,我们已经对"=="和"equals"的本质有了清晰地认识,但匪夷所思的事情仍然会发生。

1、String类型的特殊性造成的“相等比较”疑惑

再看看两段源代码:

代码4:string类型的相等比较代码

String s1=new String("aaaa");
String s2=new String("aaaa");
if(s1==s2); //false
if(s1.equals(s2)); //true

代码5:string类型的相等比较代码

String s3="aaaa";
String s4="aaaa";
if(s3==s4); // true
if(s3.equals(s4)); //true

代码4很好理解,但代码5有点让人困惑。在解释之前我们要先明确几个问题:
     (1) String是类,而并非基本数据类型。s1,s2都是对象实例,而并非基本数据变量。

(2) String s3="aaaa"; 是一种比较特殊的对象创建方法。它涉及到JVM管理方法区中常量池 拘留字符串对象的相关问题。在《String in Java 》一文中有详细的总结

下面查看代码5中"=="比较字符串对象在JVM运行时对应的指令:

代码5的指令代码

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)机制造成的“相等比较”疑惑

继续看两段代码

代码6:整型包装器对象的代码

Integer a=127;
Integer b=127;
if(a==b); //结果:true

代码7:整型包装器对象的代码

Integer c=128;
Integer d=128;
if(c==d); //结果:false

代码6和代码7几乎一样的语句竟然有不同的结果,实在是很困惑。在解释这个问题前仍然要阐明几点:
      (1) 源代码中的a、b、c、d并非整型变量,而是整型包装器对象。这一点是肯定。
      (2) Integer a=127;这种定义形式比较特殊。原因是编译过程中,编译器做了点小动作。它会自动调用Integer.valueOf(int)方法将整型常量127 打包 (autoboxing)成包装器类。我们叫做自动包装机制 。也就是说JVM运行的是Integer a=Integer.valueOf(127);这条语句。

但还是没有解决代码7,8不同的疑惑呀?下面我们来看看Integer.valueOf(int)的源代码:

Java代码

/**
 * 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。
     但要注意了,缓存数组只存储-128~127之间的Integer对象。对于其他的值的整形包装器,比如代码7中的Integer c,d=128分别在堆中创建了两个完全不同的Integer对象用来存储128。两个对象的地址都不一样。 
     这里提一点:如果是Integer a=new Integer(127);这种常规形式创建的Integer是没有cache数组的。只有Integer a=127或Integer a=Integer.valueOf(127)这样的方式才能使用上cache数组。而且包装器的整形值在-128~127之间。

实际上,这个小技巧对于初学者来说确实造成了麻烦。但是它却是Java性能优化上的一个重要的应用。我们都知道在堆中不停的开辟新对象需要很大的代价。当我们需要大量值在-128~127范围内的整型对象的时候,这样一个cache缓存减少了大量对象的创建,效率提升时可想而知的。

时间: 2024-08-06 03:39:37

【解惑】让人头疼的"相等"关系的相关文章

C语言基础知识----易让人头疼的关键字----const &amp;&amp; typedef &amp;&amp; define

const关键字 const=read only,修饰的为只读变量而不是常量.const修饰的变量不能用作数组的维数也不能放在switch语句的case:之后. 主要作用有: 1.通过把不希望被修改的变量或参数用const来修饰,编译器会保护这些变量不被修改增强系统的可靠性: 2.增强代码的可读性 [html] view plaincopyprint? const int a;      //a为常量不能被修改 int const a;      //a为常量不能被修改 const int *a

应用层的攻击已经越来越让人头疼了

最近将自己的香港的VPS由原来的CentOS 6.2 新装成windows server 2003 sp2后,开放的端口如下: TCP 1723    #用于 PPTP拨号访问google TCP  21      #用于FTP上传资料 TCP 80      #用于自己博客 TCP 3389   #远程桌面 可是最近的两天登录上去发现了很多远程桌面被暴力破解的拦截记录.

法法规规范规定顶顶顶顶让人头疼

http://www.tcrcsc.com/zpxx_282301.html http://www.tcrcsc.com/zpxx_282307.html http://www.tcrcsc.com/zpxx_282317.html http://www.tcrcsc.com/zpxx_282326.html http://www.tcrcsc.com/zpxx_282333.html http://www.tcrcsc.com/zpxx_282342.html http://www.tcrcs

前端是个让人头疼的东西

<ul>  <li class="ls">1</li>  <li class="ls">2</li>  <li class="ls">3</li></ul>//获取值,点击事件获取当前点击ul值 <script>               $(".ls").click(function(){              

用CSS解决一个让人头疼的需求

需求:下面的文字内容分别都写在一个a标签里,现在需要获取到每一行最后一个a标签的竖线,并删除  我首先想到的是用CSS3新增选择器-- :nth-child()来解决,比如 :nth-child(3n)  这里的3n表示获取到所有3的倍数的元素 更多用法参见:https://developer.mozilla.org/zh-CN/docs/Web/CSS/:nth-child     但这里又有一个问题,因为上面的文字内容都不是固定不变的,比如上面截图里的自考,现在是在第一排第3个, 但如果专升

教你怎样写让人头疼并且高质量的工作日志

来源:百度文库 工作日志一词大家都并不陌生,但并不代表大家都很熟悉. 基本上在大部分的企业,领导都会要求员工写工作日志,领导的领导要求领导写工作日志,层层递进. 写工作日志是自我工作管理的一种方法. 养成坚持每天写工作日志的习惯,可以确保每天工作不会出现遗漏,帮助日后回忆和检视, 在日积月累潜移默化中提高了自身的事务管理和时间管理能力,也为日后出现争议时提供佐证. 一.工作日志内容 好的工作日志应包括四方面内容:每日工作计划.完成情况.工作备忘.小结(心得体会).具体如下: (一)工作计划.即是

Android项目架构之业务组件化

前言: 从个人经历来说的话,从事APP开发这么多年来,所接触的APP的体积变得越来越大,业务的也变得越来越复杂,总来来说只有一句话:这是一个APP臃肿的时代!所以为了告别APP臃肿的时代,让我们进入一个U盘时代,每个业务模块都是一个具备独立运行的盘,插在哪里都可以完美运行,这就是推进业务组件的初衷也是一个美好的愿景. 需求背景: 随着公司的快速发展,版本不断的迭代,业务变得也越来越复杂,业务模块的数量有可能还会继续增加,而且每个模块的代码也会越来越多,这样下去势必影响开发效率,增加项目的维护成本

Android业务组件化之现状分析与探讨

前言: 从个人经历来说的话,从事APP开发这么多年来,所接触的APP的体积变得越来越大,业务的也变得越来越复杂,总来来说只有一句话:这是一个APP臃肿的时代!所以为了告别APP臃肿的时代,让我们进入一个U盘时代,每个业务模块都是一个具备独立运行的盘,插在哪里都可以完美运行,这就是推进业务组件的初衷也是一个美好的愿景. 需求背景: 随着公司的快速发展,版本不断的迭代,业务变得也越来越复杂,业务模块的数量有可能还会继续增加,而且每个模块的代码也会越来越多,这样下去势必影响开发效率,增加项目的维护成本

Android组件化方案

1为什么要项目组件化 2如何组件化 3组件化实施流程 1组件模式和集成模式的转换 2组件之间AndroidManifest合并问题 3全局Context的获取及组件数据初始化 4library依赖问题 5组件之间调用和通信 6组件之间资源名冲突 4组件化项目的工程类型 1app壳工程 2功能组件和Common组件 2业务组件和Main组件 5组件化项目的混淆方案 6工程的buildgradle和gradleproperties文件 1组件化工程的buildgradle文件 2组件化工程的grad