本文探讨一下java对象之间比较的三种方式
概念
- obj1 == obj2:比较两个对象的内存地址是否相同,即是否同一次new出来的对象。
- obj1.equals(obj2):默认情况下(Object中的实现),跟==效果相同,但是子类可以覆写该方法。
- obj.hashCode():用于标识某个对象的hash值。主要用于hashMap,hashSet等容器的key值查找,其值并不一定唯一。
使用规则
-
==
永远用来比较两个对象的内存地址,无法修改它的作用。 -
equals
可以被覆写,以用来表达业务概念的相等(往往指内容相同)。 - 根据设计契约,
equals
被重写后,需要同步重写hashCode
。 - 满足
==
的两个对象,一定满足equals
,且拥有相同的hashCode
(暂时没有想到反例)。 - 满足
equals
的两个对象一定满足hashCode
相等,但是反之不要求一定成立,但是尽量保证成立的概率高一些。
==
非常容易理解,下面重点介绍下equals
和hashCode
。
equals
equals可以被重写,说明equals不是严格数学意义上的相等,而是业务概念上的相等。
一个合适的 equals()方法必须满足以下五点条件(on java8):
- 反身性:对于任何 x, x.equals(x) 应该返回 true。
- 对称性:对于任何 x 和 y, x.equals(y) 应该返回 true当且仅当 y.equals(x) 返回 true 。
- 传递性:对于任何x,y,还有z,如果 x.equals(y) 返回 true 并且 y.equals(z) 返回 true,那么 x.equals(z) 应该返回 true。
- 一致性:对于任何 x和y,在对象没有被改变的情况下,多次调用 x.equals(y) 应该总是返回 true 或者false。
- 对于任何非null的x,x.equals(null)应该返回false。
可以通过下面的方式对上述条件的满足性进行测试:
下面是满足这些条件的测试,并且判断对象是否和自己相等(我们这里称呼其为右值):
- 如果右值是null,那么不相等。
- 如果右值是this,那么两个对象相等。
- 如果右值不是同一个类型或者子类,那么两个对象不相等。
- 如果所有上面的检查通过了,那么你必须决定 右值 中的哪些字段是重要的,然后比较这些字段。
Java 7 引入了 Objects
类型来帮助这个流程,这样我们能够写出更好的 equals()
函数。
hashCode
Object默认的hashCode方法是使用对象的地址计算散列码。
如果不为你的容器的键值类型覆写hashCode() 和equals() ,那么使用散列的数据结构(HashSet,HashMap,LinkedHashst或LinkedHashMap)就无法正确处理你的键值类型。
关于如何生成比较好的hash值。on java8中给出了建议,在 Objects 类中有一个非常熟悉的方法可以帮助创建 hashCode() 方法:Objects.hash()
。当你定义含有超过一个属性的对象的 hashCode() 时,你可以使用这个方法。如果你的对象只有一个属性,可以直接使用 Objects.hashCode()
。
如果要自己设计Map和更快的查找,就需要自行设计高效的散列函数算法(即hashCode()的算法实现):查询一个值的过程首先就是计算散列码,然后使用散列码查询数组。如果能够保证没有冲突(如果值的数量是固定的,那么就有可能),那可就有了一个完美的散列函数,但是这种情况只是特例。通常,冲突由外部链接处理:数组并不直接保存值,而是保存值的 list。然后对 list中的值使用equals()方法进行线性的查询。
这部分的查询自然会比较慢,但是,如果散列函数好的话,数组的每个位置就只有较少的值。因此,不是查询整个list,而是快速地跳到数组的某个位置,只对很少的元素进行比较。这便是HashMap会如此快的原因。
设计hashCode()时最重要的因素就是:无论何时,对同一个对象调用hashCode()都应该生成同样的值。如果在将一个对象用put()添加进HashMap时产生一个hashCode()值,而用get()取出时却产生了另一个hashCode()值,那么就无法重新取得该对象了。所以,如果你的hashCode()方法依赖于对象中易变的数据,用户就要当心了,因为此数据发生变化时,hashCode()就会生成一个不同的散列码,相当于产生了一个不同的键。
hashCode只是辅助hash容器的快速查找,并不具备唯一性,如果出现冲突,可以借助equals来解决,因为equals具有唯一性,这里的唯一性不是数学意义上的,而是业务概念上的。(从业务概念上将,使用==未必总是合适的,==可能太严格了。)
在Effective Java Programming Language Guide(Addison-Wesley 2001)这本书中,Joshua Bloch为怎样写出一份像样的hashCode()给出了基本的指导:
- 给int变量result赋予某个非零值常量,例如17。
- 为对象内每个有意义的字段(即每个可以做equals)操作的字段计算出一个int散列码c:
字段类型 | 计算公式 |
---|---|
boolean | c = (f ? 0 : 1) |
byte , char , short , or int | c = (int)f |
long | c = (int)(f ^ (f>>>32)) |
float | c = Float.floatToIntBits(f); |
double | long l =Double.doubleToLongBits(f); c = (int)(l ^ (l >>> 32)) |
Object , where equals() calls equals() for this field | c = f.hashCode() |
Array | 应用以上规则到每一个元素中 |
- 合并计算得到的散列码: result = 37 * result + c;
- 返回 result。
- 检查hashCode()最后生成的结果,确保相同的对象有相同的散列码。
实验
我们通过两个实验来看下,各种比较的结果
- 基础示例
import java.util.Objects;
class DummyClass {
public int dummyPara = 0;
};
class DummySwapper {
DummyClass obj = new DummyClass();
@Override
public boolean equals(Object rhs) {
if (!(rhs instanceof DummySwapper)) { // 首先判断是不是同类型
return false;
}
return obj.dummyPara == ((DummySwapper)rhs).obj.dummyPara; // 再判断成员的值是否相等
}
@Override
public int hashCode() {
// return Objects.hashCode(obj); // --> 这种写法跟equals是不配套的,无法配合使用。
return Objects.hashCode(obj.dummyPara);
}
public static void main(String... args) {
String str1 = new String("hello");
String str2 = new String("hello");
System.out.println(str1.equals(str2)); // -> true
System.out.println(str1 == str2); // -> false
String str3 = new String(str1);
System.out.println(str1.equals(str3)); // -> true
System.out.println(str1 == str3); // -> false
char value1[] = {'a', 'b', 'c'};
char value2[] = {'a', 'b', 'd'};
System.out.println(value1 == value2); // -> false
System.out.println(value1.equals(value2)); // -> false
value1 = value2;
System.out.println(value1 == value2); // -> true
System.out.println(value1.equals(value2)); // -> true
DummyClass obj1 = new DummyClass();
DummyClass obj2 = obj1;
System.out.println(obj1.hashCode() == obj2.hashCode()); // -> true
System.out.println(obj1.equals(obj2)); // -> true
DummyClass obj3 = new DummyClass();
System.out.println(obj1.hashCode() == obj3.hashCode()); // -> false
System.out.println(obj1.equals(obj3)); // -> flase
DummySwapper swapper1 = new DummySwapper();
DummySwapper swapper2 = swapper1;
System.out.println(swapper1.hashCode() == swapper2.hashCode()); // -> true
System.out.println(swapper1.equals(swapper2)); // -> true
DummySwapper swapper3 = new DummySwapper();
System.out.println(swapper1.hashCode() == swapper3.hashCode()); // -> true
System.out.println(swapper1.equals(swapper3)); // -> true
}
}
输出
true
false
true
false
false
false
true
true
true
true
false
false
true
true
true
true
- hashSet的例子:
import java.util.HashSet;
import java.util.Objects;
enum LivingBeingType {
ANIMAL,
FRUIT,
}
class LivingBeing {
public LivingBeingType type;
LivingBeing(LivingBeingType type) {
this.type = type;
}
@Override
public int hashCode() {
return Objects.hashCode(type);
}
@Override
public boolean equals(Object obj) {
if (this == obj) {
return true;
}
return obj instanceof LivingBeing && Objects.equals(type, ((LivingBeing)obj).type);
}
}
class MyDog extends LivingBeing {
MyDog() {
super(LivingBeingType.ANIMAL);
}
}
class MyCat extends LivingBeing {
MyCat() {
super(LivingBeingType.ANIMAL);
}
}
class Apple extends LivingBeing {
Apple() {
super(LivingBeingType.FRUIT);
}
}
class Orange extends LivingBeing {
Orange() {
super(LivingBeingType.FRUIT);
}
}
class LivingBeingTest {
public static void main(String... args) {
HashSet<LivingBeing> objs = new HashSet<>();
objs.add(new MyDog());
objs.add(new MyCat());
objs.add(new Apple());
objs.add(new Orange());
objs.forEach(e -> System.out.println(e));
}
}
输出
Apple@34a245ab
MyDog@4e50df2e
可以看出,如果我们按照动物和水果来分类,则猫和狗被认为是同一个对象,苹果和橘子认为是同一个对象。所以最后hashSet中只有两个元素。
String类的例子
我们非常熟悉的String类,其实是重写了Object的equals方法和hashCode
方法。Object的equals方法就是使用==
判断两个对象是否相等,hashCode方法返回该对象的内存地址值计算的一个hash值。
覆写后的equals方法,除了认为内存相同是相同对象外,还认为字符串完全相同,也是相同的对象。hashCode方法按照同样的概念做了覆写。
// from jdk14.0.2
public boolean equals(Object anObject) {
if (this == anObject) {
return true;
}
if (anObject instanceof String) {
String aString = (String)anObject;
if (!COMPACT_STRINGS || this.coder == aString.coder) {
return StringLatin1.equals(value, aString.value);
}
}
return false;
}
public int hashCode() {
// The hash or hashIsZero fields are subject to a benign data race,
// making it crucial to ensure that any observable result of the
// calculation in this method stays correct under any possible read of
// these fields. Necessary restrictions to allow this to be correct
// without explicit memory fences or similar concurrency primitives is
// that we can ever only write to one of these two fields for a given
// String instance, and that the computation is idempotent and derived
// from immutable state
int h = hash;
if (h == 0 && !hashIsZero) {
h = isLatin1() ? StringLatin1.hashCode(value)
: StringUTF16.hashCode(value);
if (h == 0) {
hashIsZero = true;
} else {
hash = h;
}
}
return h;
}
代码链接:https://gitee.com/haoliangfei/java_beginner/tree/master/objects_compare
参考:
网友评论