Java 的 == 和 equals 比较
概述:
1) ==可用于基本类型(int, short, long, byte, string, char等)和引用类型:当用于基本类型时候,是比较值是否相同;当用于引用类型的时候,是比较对象是否相同。
2) 对于String a = “a”; Integer b = 1; 这种类型的特有对象创建方式,==的时候值是相同的。
3) 基本类型没有equals方法,equals只比较值(对象中的内容)是否相同(相同返回true)。
4) 一个类如果没有定义equals方法,它将默认继承Object中的equals方法,返回值与==方法相同,即同时比较值和内存地址相同。
详述:
1) == 和 equals 的实质
在Java中利用"=="比较变量时,系统使用变量在"栈"中所存的值作为比较的依据。
基本数据类型在"栈"中存的是其内容值,而对象类型在"栈"中存的是地址,这些地址指向"堆"中的对象。
java.lang包中的Object类有public boolean equals(Object obj)方法,它比较两个对象是否相等。
其它对象的equals方法仅当被比较的两个引用指向的对象内容相同时,对象的equals()方法返回true。
总之
"==" 和 "!="比较的是地址. 也可认为"=="和"!="比较的是对象句柄; 而equals()比较的是对象内容.
或者说,"=="和"!="比较的是"栈"中的内容,而equals()比较的是"堆"中的内容。
2) == 操作符
专门用来比较两个变量的值是否相等,也就是用于比较变量所对应的内存中所存储的数值是否相同,要比较两个基本类型的数据或两个引用变量是否相当,只能用==操作符。
Java的基本数据类型为 (byte,char,short,int,float,long,double,boolean)
如果一个变量指向的数据是对象类型的,那么,这时候涉及了两块内存,对象本身占用一块内存(对内存),变量本身也占用一块内存,例如 Object obj = new Object() 变量obj是一个内存,new Object()是一个内存,此时,变量所对应的内存中存储的数据就是对象占用的那块内存的首地址。对于指向对象内存的变量,如果要比较两个变量是否指向同一个对象,即要看这两个变量所对应的内存中的数值是否相等,这时候就需要用 == 操作符进行比较。
3)构造器形成的差别
对于String和Integer来说,由于他们特有的创建对象的方式。
使用构造器和不使用构造器得到一个对象,==方法比较所产生的结果是不同的。
String a = “abc”;
String b = "abc";
此时a==b得到结果为true (在常量池的内存地址相同)
String a = new String("abc");
String b = new String("abc");
此时a==b得到的结果为 false (在堆中的内存地址不同)
Integer a = 1;
Integer b = 1;
此时a==b的结果是true (Integer a = 1 会自动装箱成 Integer a = Integer.valueOf(1);)
Integer a = new Integer(1);
Integer b = new Integer(1);
此时a==b得到的结果为 false (在堆中的内存地址不同)
通过这一点其实我们也能够更加容易理解==对内存的实际操作,实际执行的是近似于基本类型比较。
String 对象和字符串连接池:
引号内包含文本 "abc" 是String类特有创建对象的方式,但是"=="返回的结果是true,为什么呢?
因为在JVM内,存在字符串池,其中保存着很多 String对象,并且可以被共享使用
因此它提高了效率,字符串池由String类维护,我们可以调用intern()方法来访问字符串池。
当运用引号内包含文本创建对象时,所创建的对象是加入到字符串池里面的。
如果要创建下一个字符串对象,JVM首先会到字符串池中寻找,是否存在对应的字符串对象:
1)如果存在,则返回一个己存在对象的对象的引用给要创建的对象引用
2)如果不存在,才会创建一个新的对象,并返回一个新对象的对象引用给要创建的对象引用.
以上这段话理解起来可能比较拗口,用代码理解就是str2和str1是两个对象引用,并指向了同一个对象,所以'=='返回的是true.
只有引号内包含文本 string str = "abc"; 创建对象才会将创建的对象放入到字符串池。
String str = new String("abc") 这种方法创建的字符串对象是不放入到字符串池的。
所以,引号内包含文本创建对象的性能,要比后来new对象的这种方法创建字符串对象的性能要好。
String str1 = "abc"; String str2 = "abc"; String str3 = str1+str2; // 这种创建方式是不放入字符串池的. String str4 = str1+"cd"; // 这种创建方式是不放入字符串池的. String str5 = "ab"+str2; // 这种创建方式是不放入字符串池的. String str6 = "ab"+"cd"; // 这种创建方式是放入字符串池的.这种情况实际上是创建了1个对象,abcd"1个对象 String str7 = "abcd"; System.out.println(str1==str2); //返回ture System.out.println(str6==str7); //返回ture
另一个问题:
我们首先来看一段 Java代码:
String str = new String("abc");
紧接着这段代码之后的往往是这个问题,那就是这行代码究竟创建了几个String对象呢?
相信大家对这道题并不陌生,答案也是众所周知的,2个
接下来我们就从这道题展开,一起回顾一下与创建String对象相关的一些JAVA知识。
我们可以把上面这行代码分成 String str、=、"abc"和new String()四部分来看待。
1)String str只是定义了一个名为str的String类型的变量,因此它并没有创建对象;
2)=是对变量str进行初始化,将某个对象的引用(或者叫句柄)赋值给它,显然也没有创建对象;
3)现在只剩下new String("abc")了。那么,new String("abc")为什么又能被看成"abc"和new String()呢?我们来看一下被我们调用了的String的构造器:
public String(String original) { // other code ... }
大家都知道,我们常用的创建一个类的实例(对象)的方法有以下两种:
1)我们正是使用new调用了String类的上面那个构造器方法 String(String original) 创建了一个对象,并将它的引用赋值给了str变量。 String str = new String("abc");
2)同时我们注意到,被调用的构造器方法接受的参数也是一个String对象,这个对象正是"abc"。
使用new创建对象是调用Class类的newInstance方法,利用反射机制创建对象。
4)equals方法
用于比较两个独立对象的内容是否相同,就好比去比较两个人的长相是否相同,它比较的两个对象是独立的。
例如,对于下面的代码:
String a=new String("foo");
String b=new String("foo");
两条new语句创建了两个对象,然后用a,b这两个变量分别指向了其中一个对象,这是两个不同的对象,他们的首地址是不同的,即a和b中存储的数值(对象的首地址)是不相同的,所以,表达式a==b即返回false,而这两个对象中内容是相同的,所以,表达式a.equals(b)将返回true。
在实际开发中,我们经常要比较传递进行来的字符串内容是否相等,许多人稍不注意就使用==进行比较了,这是错误的,有大量这样的错误。记住,字符串的比较基本都是使用equals方法。
5)如果一个类没有定义equals方法
它将继承Object类的equals方法,Object类的equals方法的实现代码如下:
boolean equals(Object o) {
return this==o;
}
这说明,如果一个类没有自己定义equals方法,它默认的equals方法(从Object类继承的)就是使用==操作符,也是比较两个变量指向的对象是否是同一个对象,这时候使用equals和使用==会得到同样的结果,如果比较的是两个独立的对象则总返回false。
如果你编写的类希望能够比较该类创建的两个实例对象的内容是否相同,那么你必须覆盖equals方法,由你自己写代码来决定在什么情况即可以认为两个对象的内容是相同的。
示例代码:
public class Test { public static void main(String[] args) { Integer p = 1; Integer q = 1; Integer i = new Integer(1); Integer j = new Integer(1); if(p == q){ System.out.println("integer:p == q"); // 实际结果 }else{ System.out.println("integer:p != q"); } if(p.equals(q)){ System.out.println("integer:p.equals(q)"); // 实际结果 }else{ System.out.println("integer:p.equals(q)"); } if(i == j){ System.out.println("int:i == j"); }else{ System.out.println("int:i != j"); // 实际结果 } if(i.equals(j)){ System.out.println("integer:i.equals(j)"); // 实际结果 }else{ System.out.println("integer:!i.equals(j)"); } String a = "abc"; String b = "abc"; String c = new String("abc"); String d = new String("abc"); if(a == b){ System.out.println("abc对象相等"); // 实际结果 }else{ System.out.println("abc对象不相等"); } if(a.equals(b)){ System.out.println("ab相等"); // 实际结果 }else{ System.out.println("ab不相等"); } if(c.equals(d)){ System.out.println("cd相等"); // 实际结果 }else{ System.out.println("cd不相等"); } if(c == d){ System.out.println("cd对象相等"); }else{ System.out.println("cd对象不相等"); // 实际结果 } } }
equals方法的重要性毋须多言,只要你想比较两个对象是不是同一对象,你就应该实现equals方法,让对象用你认为相等的条件来进行比较.
下面的内容只是API的规范,没有什么太高深的意义,
但我之所以最先把它列在这儿,是因为这些规范在事实中并不是真正能保证得到实现。
1)对于任何引用类型, o.equals(o) == true成立. 自己跟自己比较,一定成立
2)如果 o.equals(o1) == true 成立,那么o1.equals(o)==true也一定要成立.
3)如果 o.equals(o1) == true 成立且 o.equals(o2) == true 成立,那么o1.equals(o2) == true 也成立.
4)如果第一次调用o.equals(o1) == true成立,在o和o1没有改变的情况下以后的任何次调用都成立.
5)o.equals(null) == true 任何时间都不成立,因为 o 一定是对象,分配了内存首地址,不可能为null
以上几条规则并不是最完整的表述,详细的请参见API文档.
对于Object类,它提供了一个最最严密的实现,那就是只有是同一对象时,equals方法才返回true,也就是人们常说的引用比较,而不是值比较。这个实现严密得已经没有什么实际的意义, 所以在具体子类(相对于Object来说)中,如果我们要进行对象的值比较,就必须实现自己的equals方法.
先来看一下以下这段程序:
public boolean equals(Object obj) { if (obj == null) return false; if (!(obj instanceof FieldPosition)) return false; FieldPosition other = (FieldPosition) obj; if (attribute == null) { if (other.attribute != null) { return false; } } else if (!attribute.equals(other.attribute)) { return false; } return (beginIndex == other.beginIndex & endIndex == other.endIndex && field == other.field); }
这是JDK中java.text.FieldPosition的标准实现,似乎没有什么可说的.
我信相大多数或绝大多数程序员认为,这是正确的合法的equals实现.
毕竟它是JDK的API实现啊. 还是让我们以事实来说话吧:
package debug; import java.text.*; public class Test { public static void main(String[] args) { FieldPosition fp = new FieldPosition(10); FieldPosition fp1 = new MyTest(10); System.out.println(fp.equals(fp1)); System.out.println(fp1.equals(fp)); } } class MyTest extends FieldPosition { int x = 10; public MyTest(int x){ super(x); this.x = x; } public boolean equals(Object o) { if(o==null) return false; if(!(o instanceof MyTest )) return false; return ((MyTest)o).x == this.x; } }
运行一下看看会打印出什么:
System.out.println(fp.equals(fp1)); // 打印 true
System.out.println(fp1.equals(fp)); // 打印 flase
两个对象,出现了不对称的equals算法.
问题出在哪里(脑筋急转弯:当然出在JDK实现的BUG)?
我相信有太多的程序员(除了那些根本不知道实现 equals方法的程序员外)在实现equals方法时都用过instanceof运行符来进行短路优化的,实事求是地说很长一段时间我也这么用过。太多的教程,文档都给了我们这样的误导。
而有些稍有了解的程序员可能知道这样的优化可能有些不对,但找不出问题的关键。另外一种极端是知道这个技术缺陷的骨灰级专家就提议不要这样应用。我们知道,"通常"要对两个对象进行比较,那么它们"应该"是同一类型。所以首先利用instanceof运算符进行短路优化,如果被比较的对象不和当前对象是同一类型则不用比较返回false。
但事实上,"子类是父类的一个实例",所以如果子类 o instanceof 父类,始终返回true,这时肯定不会发生短路优化,下面的比较有可能出现多种情况,一种是不能造型成父类而抛出异常,另一种是父类的private 成员没有被子类继承而不能进行比较,还有就是形成上面这种不对称比较。可能会出现太多的情况。
那么,是不是就不能用instanceof运算符来进行优化?
答案是否定的,JDK中仍然有很多实现是正确的,如果一个class是final的,明知它不可能有子类,为什么不用 instanceof来优化呢?为了维护SUN的开发小组的声誉,我不说明哪个类中,但有一个小组成员在用这个方法优化时,在后加上了加上了这样的注释:可能是有些疑问,但不知道如何做(不知道为什么没有打电话给我......)
那么对于非final类,如何进行类型的quick check呢?
if(obj.getClass() != XXXClass.class) return false;
用被比较对象的class对象和当前对象的class比较,看起来是没有问题,但是,如果这个类的子类没有重新实现equals方法,那么子类在比较的时候,obj.getClass() 肯定不等于XXXCalss.class, 也就是子类的equals将无效,所以
if(obj.getClass() != this.getClass()) return false;
才是正确的比较。
另外一个quick check是if(this==obj) return true;
// quick check if (this == obj) { return true; } // (1) same object? if (!(obj instanceof XXXXClass)) { return false; }
是否equals方法比较的两个对象一定是要同一类型?上面我用了"通常",这也是绝大多数程序员的愿望,但是有些特殊的情况,我们可以进行不同类型的比较,这并不违反规范。但这种特殊情况是非常罕见的,一个不恰当的例子是,Integer类的equals可以和Sort做比较,比较它们的value是不是同一数学值。(事实上JDK的API中并没有这样做,所以我才说是不恰当的例子)在完成quick check以后,我们就要真正实现你认为的“相等”。
对于如果实现对象相等,没有太高的要求,比如你自己实现的“人”类,你可以认为只要name相同即认为它们是相等的,其它的sex, ago都可以不考虑。这是不完全实现,但是如果是完全实现,即要求所有的属性都是相同的,那么如何实现equals方法?
class Human{ private String name; private int ago; private String sex; // ...... public boolean equals(Object obj) { quick check....... Human other = (Human)ojb; return this.name.equals(other.name) && this.ago == ohter.ago && this.sex.equals(other.sex); } }
这是一个完全实现,但是,有时equals实现是在父类中实现,而要求被子类继承后equals能正确的工作,这时你并不事实知道子类到底扩展了哪些属性,所以用上面的方法无法使equals得到完全实现。
一个好的方法是利用反射来对equals进行完全实现:
public boolean equals(Object obj){ // quick check....... Class c = this.getClass(); Filed[] fds = c.getDeclaredFields(); for(Filed f:fds){ if(!f.get(this).equals(f.get(obj))) return false; } return true; }
为了说明的方便,上明的实现省略了异常,这样的实现放在父类中,可以保证你的子类的equals可以按你的愿望正确地工作。
关于equals方法的最后一点是:如果你要是自己重写(正确说应该是履盖)了equals方法,那同时就一定要重写hashCode(). 这是规范,否则.............
我们还是看一下这个例子:
public final class PhoneNumber { private final int areaCode; private final int exchange; private final int extension; public PhoneNumber(int areaCode, int exchange, int extension) { rangeCheck(areaCode, 999, "area code"); rangeCheck(exchange, 99999999, "exchange"); rangeCheck(extension, 9999, "extension"); this.areaCode = areaCode; this.exchange = exchange; this.extension = extension; } private static void rangeCheck(int arg, int max, String name) { if(arg < 0 || arg > max) { throw new IllegalArgumentException(name + ": " + arg); } } public boolean equals(Object o) { if(o == this) return true; if(!(o instanceof PhoneNumber)) return false; PhoneNumber pn = (PhoneNumber)o; return pn.extension == extension && pn.exchange == exchange && pn.areaCode == areaCode; } }
注意这个类是final的,所以这个equals实现没有什么问题。
我们来测试一下:
public static void main(String[] args) { Map hm = new HashMap(); PhoneNumber pn = new PhoneNumber(123, 38942, 230); hm.put(pn, "I love you"); PhoneNumber pn1 = new PhoneNumber(123, 38942, 230); System.out.println(pn); System.out.println("pn.equals(pn1) is " + pn.equals(pn1)); System.out.println(hm.get(pn1)); System.out.println(hm.get(pn)); }
既然pn.equals(pn1),那么我put(pn,"I love you")后,get(pn1)为什么是null呢?
答案是因为它们的hashCode不一样,而hashMap就是以hashCode为主键的。
即 pn 和 pn1 的内容值相同,但是 pn 和 pn1 两个对象的首地址不同,hm中并没有 pn1对象的首地址。
所以规范要求,如果两个对象进行equals比较时如果返回true,那么它们的hashcode要求返回相等的值。
参考推荐:
版权所有: 本文系米扑博客原创、转载、摘录,或修订后发表,最后更新于 2021-01-20 16:25:43
侵权处理: 本个人博客,不盈利,若侵犯了您的作品权,请联系博主删除,莫恶意,索钱财,感谢!