软件构造之判断对象相等的一些思考

一、==和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会指向相同的地址,即不会分配新的空间。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

呆先森HIT

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值