java 中的 equals、==与hashcode

时间:2022-06-16 16:09:56

一、== 详解

1、简单的背景 Java中一切都是对象,在程序运行时,每个对象的存储位置有以下几个选择: 1)寄存器:速度最快,容量最小,在Java中存储器是完全透明的——无法控制也无法建议编译器将某个对象存入存储器中; 2)堆栈:位于RAM中,通过堆栈指针可以获得这个区域在内存中的地址,可以通过控制堆栈指针的加减实现存储的分配。在创建程序时,Java系统必须知道所有存储在堆栈的项目的确切生命周期以便控制堆栈指针的移动。基本类型的对象和其他对象的对象引用存储在这一区域。 3)堆:堆是一种通用的内存池,也位于RAM中,用于存放大部分的Java对象。堆不同于堆栈的一点是,编译器不需要知道堆中对象的确切生命周期。用new生成的对象存储在堆中。 2、== 实质 了解了Java中对象的存储位置后,我们便可以说:== 的工作是比较两个对象对应堆栈中的内容是否相同。 具体来说,对于基本类型——包括boolean、char、byte、short、int、long、float、double和void 由于他们定义的对象就存储在堆栈中,因此采用==就是在比较值是否相等。
public class test {
public static void main(String[] args) {
int i1=10;
int i2=10;
int i3=15;

boolean bool1=false;
boolean bool2=false;
boolean bool3=true;

char c1='x';
char c2='x';
char c3='y';

short s1=255;
short s2=255;
short s3=128;

byte b1=64;
byte b2=64;
byte b3=32;

long l1=100;
long l2=100;
long l3=200;

float f1=3.14f;
float f2=3.14f;
float f3=6.28f;

double d1=2.78;
double d2=2.78;
double d3=5.56;

System.out.print("i1==i2?");
System.out.println(i1==i2);
System.out.print("i2==i3?");
System.out.println(i2==i3);

System.out.print("l1==l2?");
System.out.println(l1==l2);
System.out.print("l2==l3?");
System.out.println(l2==l3);

System.out.print("bool1==bool2?");
System.out.println(bool1==bool2);
System.out.print("bool2==bool3?");
System.out.println(bool2==bool3);

System.out.print("s1==s2?");
System.out.println(s1==s2);
System.out.print("s2==s3?");
System.out.println(s2==s3);

System.out.print("c1==c2?");
System.out.println(c1==c2);
System.out.print("c2==c3?");
System.out.println(c2==c3);

System.out.print("b1==b2?");
System.out.println(b1==b2);
System.out.print("b2==b3?");
System.out.println(b2==b3);

System.out.print("f1==f2?");
System.out.println(f1==f2);
System.out.print("f2==f3?");
System.out.println(f2==f3);

System.out.print("d1==d2?");
System.out.println(d1==d2);
System.out.print("d2==d3?");
System.out.println(d2==d3);
}
}
输出如下:
i1==i2?true
i2==i3?false
l1==l2?true
l2==l3?false
bool1==bool2?true
bool2==bool3?false
s1==s2?true
s2==s3?false
c1==c2?true
c2==c3?false
b1==b2?true
b2==b3?false
f1==f2?true
f2==f3?false
d1==d2?true
d2==d3?false

而对于其他大部分对象,堆栈中存储的只是对象引用,而不是对象的内容,因此采用==比较是在比较引用是否相等,即是否指向堆中的同一个对象。
public class test {
public static void main(String[] args) {
String str1 = new String("abc");
String str2 = new String("abc");
String str3 = str1;

Date date1 = new Date();
Date date2 = new Date();
Date date3 = date1;

Person p1=new Person();
Person p2=new Person();
Person p3=p1;

System.out.print("str1==str2?");
System.out.println(str1==str2);
System.out.print("str1==str3?");
System.out.println(str1==str3);

System.out.print("Date1==Date2?");
System.out.println(date1==date2);
System.out.print("date1==date3?");
System.out.println(date1==date3);

System.out.print("p1==p2?");
System.out.println(p1==p2);
System.out.print("p1==p3?");
System.out.println(p1==p3);
}
}
输出如下:
str1==str2?false
str1==str3?true
Date1==Date2?false
date1==date3?true
p1==p2?false
p1==p3?true
3、对引号字符串 String 的特殊情况  Java为String提供了一种特殊的实例对象的方式:通过——引号字符串。
String str1 = "abc";
注意,引号字符串保存在一个叫做字符串池的区域中。JVM中存在字符串池,字符串池中保存很多String对象,并且可以共享使用。因此字符串池提高了效率。当用引号字符串创建String对象时,JVM会首先在字符串池中寻找,是否存在对应的对象;如果存在,则返回一个指向这个已存在对象的引用;否则在池中创建一个新的对象,再将引用返回。
public class test {
public static void main(String[] args) {
String str1 = "abc";
String str2 = "abc";

String str3=new String("abc");

System.out.print("str1==str2?");
System.out.println(str1==str2);

System.out.print("str1==str3?");
System.out.println(str1==str3);
}
}
输出如下:
str1==str2?true
str1==str3?false
由于str1与str2都是通过引号字符串建立的,当str2建立时,字符串池中已经存在“abc”,因此JVM直接返回这个引用,所以str==str2成立;而str3由new建立,没有保存在字符串池中,也就是在堆栈中保存的str3内容与str1的内容不相同,所以str3==str1不成立。
通过String的intern()方法可以访问字符串池。intern()方法会在字符串池中搜寻,是否在字符串池中已经存在与当前对象相同的对象,如果存在,则返回字符串池中的对应的引用;如果不存在,则将当前对象添加到字符串池中,再返回引用。
public class test {
public static void main(String[] args) {
String str1 = "abc";
String str2 = "abc";

String str3=new String("abc");
str3=str3.intern();

System.out.print("str1==str2?");
System.out.println(str1==str2);

System.out.print("str1==str3?");
System.out.println(str1==str3);
}
}
这是的输出如下:
str1==str2?true
str1==str3?true
可以看到,通过intern(),str3变成了对字符串池中的与str1和stre2相同的引用。
4、对 基本类型包装器的情况 Java为每一个基本类型都提供了一个包装器类型,包装器类型更像是一个定义完备的类。Java中的基本类型与包装器类型的对应关系如下表。
基本类型 大小 包装器类型
boolean - Boolean
char 16bit Character
byte 8bit Byte
short 16bit Short
int 32bit Integer
long 64bit Long
float 32bit Float
double 64bit Double
void - Void


包装器类型也支持与基本类型相似的创建对象的方式,对于采用这种方式建立的对象,==的操作与类型有关。 对于Integer、Boolean、Character、Byte、Long 这些类型,== 比较值是否相等;
public class test {
public static void main(String[] args) {
Integer i1 = 10;
Integer i2 = 10;

Boolean bool1 = false;
Boolean bool2 = false;

Character c1 = 'x';
Character c2 = 'x';

Short s1 = 255;
Short s2 = 255;

Byte b1 = 64;
Byte b2 = 64;

Long l1 = 100l;
Long l2 = 100l;

Float f1 = 3.14f;
Float f2 = 3.14f;

Double d1 = 2.78;
Double d2 = 2.78;

System.out.print("i1==i2?");
System.out.println(i1 == i2);

System.out.print("l1==l2?");
System.out.println(l1 == l2);

System.out.print("bool1==bool2?");
System.out.println(bool1 == bool2);

System.out.print("c1==c2?");
System.out.println(c1 == c2);

System.out.print("b1==b2?");
System.out.println(b1 == b2);

System.out.print("s1==s2?");
System.out.println(s1 == s2);

System.out.print("f1==f2?");
System.out.println(f1 == f2);

System.out.print("d1==d2?");
System.out.println(d1 == d2);

}
}
运行结果如下:
i1==i2?true
l1==l2?true
bool1==bool2?true
c1==c2?true
b1==b2?true
s1==s2?false
f1==f2?false
d1==d2?false
而对于Short、Float和Double类型,值相等的情况下,==仍然得到false。
对于采用new 建立的对象,以上所有包装器类型在堆栈中保存的都是对象的引用,也就是说==比较是否指向同一对象。
public class test {
public static void main(String[] args) {
Integer i1 = new Integer(10);
Integer i2 = new Integer(10);

Boolean bool1 = new Boolean(false);
Boolean bool2 = new Boolean(false);

Character c1 = new Character('x');
Character c2 = new Character('x');

System.out.print("i1==i2?");
System.out.println(i1 == i2);

System.out.print("bool1==bool2?");
System.out.println(bool1 == bool2);

System.out.print("c1==c2?");
System.out.println(c1 == c2);

}
}
运行结果如下:
i1==i2?false
bool1==bool2?false
c1==c2?false

二、 equals() 详解

1、Object 的equals(Obejct) 在Object类中提供了一个equals(Object)方法,其源码如下:
    public boolean equals(Object obj) {
return (this == obj);
}
也就是当且仅当obj与this指向同一对象时返回true。
很多Java的类都 override了这个方法;大部分override以后的方法都是比较引用指向对象的内容是否相同。例如String的equals方法便会逐个字符比较两个String的内容:
public boolean equals(Object anObject) {
if (this == anObject) {
return true;
}
if (anObject instanceof String) {
String anotherString = (String)anObject;
int n = value.length;
if (n == anotherString.value.length) {
char v1[] = value;
char v2[] = anotherString.value;
int i = 0;
while (n-- != 0) {
if (v1[i] != v2[i])
return false;
i++;
}
return true;
}
}
return false;
}
Java的documention中建议自定义类override这个方法,并且要求equals方法遵循下面四个约定:
1)自反性 对于任何非null的引用x,x.equals(x)应始终返回true; 2)对称性 对于任何非null的引用x和y,x.equals(y)返回true当且仅当y.equals(x)返回true; 3)传递性 对于任意非null的引用x、y和z,如果x.equals(y)返回true,y.equals(z)返回true,那么x.equals(z)也要返回true; 4)不变性 对于任意非null的引用协和y,如果在多次调用equals()之间,x和y中涉及到equals()方法的信息都没有修改,那么多次调用都应该返回相同的值。 2、自定义类覆盖equals(Object) 如果在自定义类中没有override equals方法,那么便会继承Object中的equals方法,也就是比较两个引用是否指向同一个对象。这可能不是你想要的结果,所以可以再override的equals方法中自定义实现的逻辑。下面的代码给出了一个粗糙的实例。
public class Person {
private String id;
private String name;
private int age;
public Person(String id, String name, int age) {
super();
this.id = id;
this.name = name;
this.age = age;
}
public String getId() {
return id;
}
public void setId(String id) {
this.id = id;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public int getAge() {
return age;
}
public void setAge(int age) {
this.age = age;
}
@Override
public boolean equals(Object obj) {
// TODO Auto-generated method stub
if(obj==null)
return false;
if(!(obj instanceof Person))
return false;
Person person=(Person)obj;
if(this.id==null){
return person.getId()==null;
}
return this.id.equals(person.getId());
}

}

3、自定义equals() 注意问题 1)常见的短路优化措施 对于非static的equals方法来说,当obj==null时一定返回false; 一般情况下,对于!(obj instanceof XXX)的来说可以返回false,也可以通过this.getClass()!=obj.getClass()判断; !instanceof 和getClass()之间还有微妙的区别。 由于instanceof 之后要加类型参数,也就是判断是不是具体的某个类型的实例,当在基类的equals中使用了!instanceof进行短路优化,而子类中没有override的话,这个短路优化不会起作用(子类始终是父类的instance),这个时候getclass的方法更为合适(不是同一子类必然不相等) 2)非final类避免使用obj.getClass()!=XXXClass.class来判断 非final类可能具有子类,而如果子类没有override equals方法,那么会使用超类的equals方法,上面的写法会导致始终返回false,应该用this.getClass()!=obj.getClass()判断。

三、hashcode()

Java dodumention建议每个override 了equials的类都应该override hashCode。由于没有override hashCode方法导致的使用HashTable、HashMap和HashSet的错误是很常见的。如果一个类没有override hashCode方法,那么便会继承Object的hashCode方法,根据约定,equals为true的对象hashCode相同,而对于Object来说只有指向同一对象的两个引用equals才会得到true,对于不同的引用,应该认为Object的hashCode方法会得到不相同的两个数。 Java对hashCode也有约定: 1)在应用程序运行期间,如果一个对象的equals方法用到的信息没有被修改的话,对该对象调用hashCode方法多次,都应该始终返回同一个整数。在一个程序执行多次的过程中,这个整数可以不同; 2)如果两个对象根据equals 方法是相等的,那么这两个对象的hashCode方法的结果也是相等的;也就是说,hashCode 结果不同的两个对象,equals 方法一定得到false; 3)对于equals 方法得到false的两个对象,不要求它们的hashCode方法得到的结果也一定不同。
public class test {
public static void main(String[] args) {
HashMap<Person,String>map=new HashMap<Person, String>();
Person p1=new Person("101", "jack", 20);
Person p2=new Person("101", "jack", 20);
System.out.print("p1.equals(p2)?");
System.out.println(p1.equals(p2));
map.put(p1, "test");
System.out.println(map.get(p2));
}
}
上面的代码输出结果为:
p1.equals(p2)?true
null
由于equals为true对象hashCode也应相同,并且HashMap也需要得到hashCode来计算对象的存储索引,因此如果两个对象的hashCode不同,hashMao就认为是不同的对象。在get的时候,也是根据对象的hashCode来计算得到相应的索引,再去查看对应的位置有没有对象。
在上面的例子中,由于Person没有override hashCode方法,所以使用Object的hashCode方法,p1和p2的hashCode得到不同的值。下面的代买修改了Person的定义:
public class Person {
private String id;
private String name;
private int age;
public Person(String id, String name, int age) {
super();
this.id = id;
this.name = name;
this.age = age;
}
public String getId() {
return id;
}
public void setId(String id) {
this.id = id;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public int getAge() {
return age;
}
public void setAge(int age) {
this.age = age;
}
@Override
public boolean equals(Object obj) {
// TODO Auto-generated method stub
if(obj==null)
return false;
if(!(obj instanceof Person))
return false;
Person person=(Person)obj;
if(this.id==null){
return person.getId()==null;
}
return this.id.equals(person.getId());
}
@Override
public int hashCode() {
// TODO Auto-generated method stub
if(id==null)
return 0;
return this.id.hashCode();
}

}
Java已经对String的hashCode进行了重写,可以保证相同的String内容会得到相同的hashCode。下面是String中的hashCode代码:
    public int hashCode() {
int h = hash;
if (h == 0 && value.length > 0) {
char val[] = value;

for (int i = 0; i < value.length; i++) {
h = 31 * h + val[i];
}
hash = h;
}
return h;
}