前言
equals() 和 hashCode() 都是 Object 对象中的非 final 方法,它们设计的目的就是被用来覆盖(
override
)的,所以在程序设计中还是经常需要处理这两个方法的。而掌握这两个方法的覆盖准则以及它们的区别还是很必要的,相关问题也不少。
首先看看==
和 equals() 的不同。
对于基本数据类型,==
比较的是它们的值。
Example
int num1 = 1, num2 = 1;
char ch1 = 'a', ch2 = 'a';
if (num1 == num2 && ch1 == ch2) {
System.out.println(num1 == num2);
System.out.println(ch1 == ch2);
} else {
System.out.println(num1 == num2);
System.out.println(ch1 == ch2);
}
输出结果:
true
true
对于引用类型,==
比较的是它们的内存地址。以 String 为例:
String str1 = "Hello World";
String str2 = "Hello World";
if (str1 == str2) {
System.out.println("str1和str2的地址相同");
} else {
System.out.println("str1和str2的地址不同");
}
输出结果:
str1和str2的地址相同
根据String的源码解释:
The String class represents character strings. All string literals in Java programs, such as
"abc"
, are implemented as instances of this class.Strings are constant; their values cannot be changed after they are created.
可见,直接这样初始化的字符串属于 String 类的实现,并且 String 类型的字符串自被创建后就是不可变的了。并且 Java 也推荐这样初始化 String ,这是为什么呢?
在这一构造函数的注释中这样写道:
public String(String original) {
this.value = original.value;
this.hash = original.hash;
}
Initializes a newly created String object so that it represents the same sequence of characters as the argument; in other words, the newly created string is a copy of the argument string. Unless an explicit copy of original is needed, use of this constructor is unnecessary since Strings are immutable.
大意就是说这样初始化 String 对象,其字符序列是与参数相同的;也就是说新创建的字符串是参数字符串的副本。 因此除非需要显式的原始副本,否则不必使用此构造函数,因为字符串是不可变的。
那为什么字符串是不可变的呢?
在 String 源码中可以看到,其实字符串是被存放到了一个 char 类型的数组中,且该数组被 final
关键字修饰,因此创建好的字符串是不可变的也就可以想通了。
/** The value is used for character storage. */
private final char value[];
因此下面的语句是等价的:
String str = "abc";
//str等价于:
char [] data = {'a', 'b', 'c'};
String str = new String(data);
这样理解了一番之后,就可以得出一些结论:
- 如果要初始化字符串,直接给 String 类型的变量赋值即可;
- 但如果要转换数组为字符串, String 类提供了不少相应的构造函数,可以查阅文档选择合适的使用。
- 用 String 类创建的字符串是不可变的(
constant
)。
回到最开始的问题,又有一个新的问题:String是怎么判断两个字符串引用地址的呢?
在String的源码中提到了字符串缓冲池(A pool of strings
):
A pool of strings, initially empty, is maintained privately by the class String.
...
if the pool already contains a string equal to this String object as determined by the equals(Object) method, then the string from the pool is returned. Otherwise, this String object is added to the pool and a reference to this String object is returned.
这个缓冲池是由 String 类维护的,当缓冲池中已经存在一个和传入字符串相同的字符串(通过调用 equals() 方法来确定是否相同),那么就返回缓冲池中的字符串。否则,就把传入的新字符串添加到缓冲池中并返回这个字符串的引用。
结合之前提到的使用构造函数来创建新字符串的方式,这种方式新创建的字符串是传入字符串的副本,现在对这句话进行解释。看下面的例子:
String str1 = "Hello";
String str2 = new String("Hello");
if (str1 == str2) {
System.out.println("str1和str2的地址相同");
} else {
System.out.println("str1和str2的地址不同");
}
输出结果为:
str1 和 str2 的地址不同
- str1 创建的字符串被加入到字符串缓冲池中去, str1 指向缓冲池中的 "Hello" ;
- str2 调用构造函数传入 "Hello" ,实际上进行了两步:
- 先去缓冲池中找有没有相同的字符串(通过调用 equals() 方法来确定是否相同);
- 发现缓冲池中有相同的字符串,那就不需要新创建一遍了;而如果没有,就新创建一个字符串。这里的情况显然是第一种。但是由于 str2 中调用了构造函数,因此要在内存的堆空间上新建一个对象,而这个 str2 正是指向内存堆上的一个地址空间。
因此 str1 和 str2 通过 ==
判断到的地址空间是不同的。
所有类中的 equals() 方法都是继承自或重写 Object 类中 equals() 方法的。Object类提供的 equals() 方法如下:
public boolean equals(Object obj) {
return (this == obj);
}
可见最原始的 equals() 方法其实就是调用的==
。对于任何非空(non-null
)的引用变量 x 和 y ,当且仅当 x 和 y 指向同一个对象的时候,equals() 方法就返回 true 。
重写 equals() 的准则,这个在 Object 类中有提到过:
- 自反性:
It is reflexive: x.equals(x) should return true;- 对称性:
It is symmetric: x.equals(y) should return true if and only if y.equals(x) returns true;- 传递性:
It is transitive: if x.equals(y) returns true and y.equals(z) returns true, then x.equals(z) should return true.- 一致性:
It is consistent: for any non-null reference values x and y, multiple invocations of x.equals(y) consistently return true or consistently return false, provided no information used in equals comparisons on the objects is modified.- 非空性:
x.equals(null) should return false.
那么问题来了,哪些情况下会违反对称性和传递性?
- 违反对称性
对称性就是x.equals(y)时,y也得equals x,很多时候,我们自己覆写equals时,让自己的类可以兼容等于一个已知类,比如下面的例子:
public final class CaseInsensitiveString {
private final String s;
public CaseInsensitiveString(String s) {
if (s == null)
throw new NullPointerException();
this.s = s;
}
@Override
public boolean equals(Object o) {
if (o instanceof CaseInsensiticeString)
return s.equalsIgnoreCase(((CaseInsensitiveString)o).s);
if (o instanceof String)
return s.equalsIgnoreCase((String) o);
return false;
}
}
这个想法很好,想创建一个无视大小写的String,并且还能够兼容String作为参数,假设我们创建一个CaseInsensitiveString:
CaseInsensitiveString cis = new CaseInsensitiveString("Case");
那么肯定有 cis.equals("case")
,问题来了,"case".equals(cis)
吗? String 并没有兼容 CaseInsensiticeString ,所以 String 的 equals() 也不接受 CaseInsensiticeString 作为参数。
所以有个准则,一般在覆写 equals() 只兼容同类型的变量。
- 违反传递性
传递性就是A等于B,B等于C,那么A也应该等于C。
假设我们定义一个类Cat。
public class Cat(){
private int height;
private int weight;
public Cat(int h, int w)
{
this.height = h;
this.weight = w;
}
@Override
public boolean equals(Object o) {
if (!(o instanceof Cat))
return false;
Cat c = (Cat) o;
return c.height == height && c.weight == weight;
}
}
名人有言,不管黑猫白猫抓住老鼠就是好猫,我们又定义一个类ColorCat:
public class ColorCat extends Cat{
private String color;
public ColorCat(int h, int w, String color)
{
super(h, w);
this.color = color;
}
我们在实现 equals 方法时,可以加上颜色比较,但是加上颜色就不兼容和普通猫作对比了,这里我们忘记上面要求只兼容同类型变量的建议,定义一个兼容普通猫的 equals 方法,在“混合比较”时忽略颜色。
@Override
public boolean equals(Object o) {
if (! (o instanceof Cat))
return false; //不是Cat或者ColorCat,直接false
if (! (o instanceof ColorCat))
return o.equals(this);//不是彩猫,那一定是普通猫,忽略颜色对比
return super.equals(o)&&((ColorCat)o).color.equals(color); //这时候才比较颜色
}
假设我们定义了猫:
ColorCat whiteCat = new ColorCat(1,2,"white");
Cat cat = new Cat(1,2);
ColorCat blackCat = new ColorCat(1,2,"black");
此时有whiteCat
等于cat
,cat
等于blackCat
,但是whiteCat
不等于blackCat
,所以不满足传递性要求。
源码注释中还提到,无论何时当 equals() 方法被重写的时候,都有必要去重写一下 hashCode() 方法以便维持 hashCode() 方法的通用契约(general contract
),这个契约就是相同的对象必须具有相同的哈希值。
类库提供的 equals() 方法,如果已经重写的话,那么比较的也许就不止是地址空间了,这就看具体类库是怎么实现的了。以 String 类为例,它提供的 equals() 方法如下:
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;
}
- 首先会判断当前对象和传入对象是否指向相同的地址空间,如果是就直接返回 true ;
- 如果指向的地址空间不同,检测传入对象是否为 String 类的实例,如果是就比较两个对象中值是否相同,如果相同,返回 true ;否则返回 false ;
- 如果传入对象不是 String 类的实例,返回 false 。
Example
String str1 = "Hello";
String str2 = new String("Hello");
if (str2.equals(str1)) {
System.out.println("str1 equals to str2");
} else {
System.out.println("str1 doesn't equals to str2");
}
输出结果为:
str1 equals to str2
原因显而易见。