一、==和equals
对于基本数据类型,使用==来判断相等即可 ,而如果对俩个对象引用类型使用==判定相等,那么只会比较俩个引用的地址,此时只有俩个引用指向同一个地址时,才会得到true的结果。这样显然不能满足我们面向对象语言中判定俩个对象相同的要求。当俩个对象,它看起来一样,动起来也一样,那他俩就是一样的。这是我们来判断俩个对象相同的思路。于是我们开发了equals方法。
JAVA中所有类的超类Object中定义并且实现了equals方法。
Object.equals只是简单比较俩个对象的引用地址,故对有需要判定equal的类中,要对equals方法进行重写。而我们需要判定俩个对象相等时,一定要用equals!
二、怎么重写equals
首先,equals方法必须满足:自反性、对称性、传递性、一致性、不能等于空指针。然后若两个对象equals判定为true时,其hashcode必须相等(反过来未必)。
其次, 第八章PPT刚开始讲了判断俩个对象相等的三个原则。第二个原则是从AF角度来看,俩个对象Rep映射到的抽象空间相同;第三个原则是从观察角度,如果俩个对象调用任何方法得到结果一样,那他俩就是一样的。我们要根据具体情况和要求来重写equals。对象可以大致分为immutable和mutable俩类,对于不同类型要有不同的策略。
2.1 immutable类型的equals
“当我看到一只猫,它走路像老虎、体型像老虎、叫声像老虎,我就称其为老虎。”对于不可变类型来讲,由于它不能被修改,所以只要它看起来是相同的,就可以判定equals返回true。
但是在重写equals时,我们不能简单地通过比较俩个类中所有field是否相等来判定equals。这里我们要考虑前面所说的原则二:看起来相同,是从外部观察者角度来看的,也就是说俩个对象在A空间的映射值相同,而内部的Rep不必完全相同。
所以,在重写equals方法时,要结合我们的AF函数来仔细构造。当俩个对象Rep不同,而AF(Rep)相同时,也需要给出true的结果。
此外,重写equals后,也必须重写hashcode方法,以确保得到一致的结果。
2.2 mutable类型的equals
对于mutable类型,有两种等价性的策略:1.观察等价性;2.行为等价性。
观察等价性,在我的理解就是,在俩个对象生命周期的这一个moment,如果在这个时刻中,其Rep在AF映射下相同,那么就是相等的。也就是说在这一时刻里,我们将这俩个对象看作不可变的,从观察的角度来判定他俩是否相等。举个例子,如果两个类是容器类型的,比如LinkedList、ArrayList,当他俩装的东西一样时,就可以说他俩是相等的。
下面是ArrayList的equals源码:
public boolean equals(Object o) {
if (o == this) {
return true;
}
if (!(o instanceof List)) {
return false;
}
final int expectedModCount = modCount;
// ArrayList can be subclassed and given arbitrary behavior, but we can
// still deal with the common case where o is ArrayList precisely
boolean equal = (o.getClass() == ArrayList.class)
? equalsArrayList((ArrayList<?>) o)
: equalsRange((List<?>) o, 0, size);
checkForComodification(expectedModCount);
return equal;
}
boolean equalsRange(List<?> other, int from, int to) {
final Object[] es = elementData;
if (to > es.length) {
throw new ConcurrentModificationException();
}
var oit = other.iterator();
for (; from < to; from++) {
if (!oit.hasNext() || !Objects.equals(es[from], oit.next())) {
return false;
}
}
return !oit.hasNext();
}
private boolean equalsArrayList(ArrayList<?> other) {
final int otherModCount = other.modCount;
final int s = size;
boolean equal;
if (equal = (s == other.size)) {
final Object[] otherEs = other.elementData;
final Object[] es = elementData;
if (s > es.length || s > otherEs.length) {
throw new ConcurrentModificationException();
}
for (int i = 0; i < s; i++) {
if (!Objects.equals(es[i], otherEs[i])) {
equal = false;
break;
}
}
}
other.checkForComodification(otherModCount);
return equal;
}
可以看到,ArrayList的equals方法判定标准是:俩个对象都是list的子类,并且储存的元素数量相等并且对应位置的每一个元素相等。另外在方法里还涉及了一些线程安全的判定,这些超出了我目前知识范围。
然后我自己动手试了一下:
List<Integer> al=new ArrayList<>();
List<Integer> bl=new LinkedList<>();
al.add(64);
bl.add(64);
System.out.println(al.equals(bl));
al.add(28);
bl.add(28);
System.out.println(al.equals(bl));
得到的结果确实是true。
行为等价性,在我的理解是,无论什么时刻,俩个对象调用相同的方法,所得到的结果一样。但是对于俩个可变对象而言,并不能保证其在每一时刻都是相等的,因此,若要实现行为等价性,唯一的可能就是A和B其实是同一个对象。因此,如果判断标准是行为等价性的话,不需要重写equals方法,直接判定引用相等即可。
三、一点小探索
这里我对String的创建机制有些好奇,于是动手探索了一下。
public class Main {
public static void main(String[] args) {
String as=new String("abc");
String bs=new String("abc");
System.out.println(as.equals(bs));
System.out.println(as==bs);
String cs="abc";
String ds="abc";
System.out.println(cs==as);
System.out.println(cs==bs);
System.out.println(cs==ds);
}
public void test(){
}
}
我们知道,如果我们隐式地实例化一个String对象时,若堆中已有相同的字符串,则不会开辟空间,而是返回已有字符串的地址引用。但如果我显式地使用new初始化呢?如2、3句,然后我调用了equals和==来查看结果。发现equals方法得到的结果是true,而==得到结果为false。这说明如果使用new的话,即使是String也会强制在堆中分配新的空间。并且equals方法并不是简单地比较引用地址。
然后我隐式地初始化一个相同的String对象呢?最后三条输出语句结果分别为false,false,true。根据结果可以知道,cs的引用地址并不等于as或者bs之中任意一个,隐式初始化字符串时编译器并不会检查当前由用户强制new的String对象是否已有相同的String。而当我们多次隐式创建同一个字符串时,这几个String会指向相同的地址,即不会分配新的空间。