
本文深入探讨了在自定义java集合类(如arraydeque)中正确实现`equals`方法的挑战与解决方案,特别是在不依赖`java.util.*`工具类进行深度比较的场景。文章详细阐述了如何通过委托元素自身的`equals`方法实现值相等判断,并强调了使用迭代器进行高效元素遍历的重要性,以避免潜在的性能瓶颈,最终提供了一个结构清晰、性能优化的`equals`实现范例。
在Java编程中,当我们创建自定义的集合类(如ArrayDeque或LinkedListArrayDeque)时,正确地实现equals方法是至关重要的。它确保了对象之间的逻辑相等性判断符合预期,而不仅仅是引用相等。本教程将引导您如何在不使用java.util.*中的辅助方法(如Objects.equals)的情况下,为自定义Deque实现一个健壮且高效的equals方法,并涵盖深度比较的原理和性能优化。
Object类默认的equals方法执行的是引用相等性比较(即==)。对于大多数自定义类,我们需要重写此方法以实现值相等性比较。当集合类包含其他对象时,其equals方法的实现需要进行“深度比较”,这意味着它必须检查集合中每个对应元素的相等性。这种深度比较通常通过委托给集合元素的equals方法来完成。
在最初的尝试中,开发者可能会考虑引入一个独立的deepEquals方法。然而,这通常是不必要的,因为Java的equals方法本身就应该处理这种逻辑。一个设计良好的equals方法会递归地调用其成员对象的equals方法,从而实现“深度”比较。
考虑以下一个自定义ArrayDeque的初始equals和deepEquals实现:
立即学习“Java免费学习笔记(深入)”;
public boolean equals(Object o) { // 原始equals方法
    if (o == this) {
        return true;
    }
    if (o == null || this == null) { // this == null 不可能发生
        return false;
    }
    if (!(o instanceof Deque)) {
        return false;
    }
    Deque oll = (Deque) o;
    if (oll.size() != this.size()) {
        return false;
    }
    for (int i = 0; i < this.size(); i++) {
        Object a2 = oll.get(i);
        Object a1 = this.get(i);
        if (a1 == a2) { // 引用相等或都为null
            continue;
        }
        if (a2 == null) { // a1不为null但a2为null
            return false;
        }
        if (a1.getClass() != a2.getClass()) { // 类型检查过于严格
            return false;
        }
        return deepEquals(a1, a2); // 问题所在:提前返回且deepEquals实现不当
    }
    return true;
}
private boolean deepEquals(Object a1, Object a2) { // 原始deepEquals方法
    boolean deq;
    if (a1 instanceof Deque) { 
        deq = a1.equals(a2); // 委托给Deque的equals
    } else {
        if (a1 == a2) { // 再次检查引用相等
            return true;
        }
        return false; // 如果不是Deque,且引用不相等,则直接返回false
    }
    return deq;
}上述代码存在几个问题:
一个健壮的equals方法应遵循Object类定义的五项约定:
基于这些原则,我们可以构建一个正确的equals方法。
结合上述分析,以下是经过优化和修正的equals方法,它避免了deepEquals的冗余,并正确处理了元素比较逻辑:
public boolean equals(Object o) {
    // 1. 自反性:检查是否是同一个对象的引用
    if (o == this) {
        return true;
    }
    // 2. 非空性:检查传入对象是否为null
    if (o == null) {
        return false;
    }
    // 3. 类型检查:检查传入对象是否是Deque的实例
    // 使用 instanceof 优于 getClass() == other.getClass(),因为它允许子类与父类对象进行比较
    if (!(o instanceof Deque)) {
        return false;
    }
    // 将Object转换为Deque类型,以便访问其特定方法
    Deque<?> otherDeque = (Deque<?>) o; // 使用<?>泛型以兼容不同类型的Deque
    // 4. 大小检查:如果大小不同,则不可能相等
    if (otherDeque.size() != this.size()) {
        return false;
    }
    // 5. 元素级比较:遍历所有元素进行深度比较
    // 注意:这里的实现是基于get(i)的,后续会讨论迭代器优化
    for (int i = 0; i < this.size(); i++) {
        Object elementOfThis = this.get(i);
        Object elementOfOther = otherDeque.get(i);
        // 处理元素为null的情况和引用相等的情况
        // 如果两个元素引用相同(包括都为null),则认为它们相等,继续下一个元素
        if (elementOfThis == elementOfOther) {
            continue;
        }
        // 如果elementOfThis不为null但elementOfOther为null,则不相等
        if (elementOfOther == null) {
            return false;
        }
        // 至此,elementOfThis和elementOfOther都保证不为null
        // 委托给元素的equals方法进行值比较
        if (!elementOfThis.equals(elementOfOther)) {
            return false; // 只要有一个元素不相等,整个Deque就不相等
        }
    }
    // 如果所有元素都相等,则两个Deque相等
    return true;
}代码解析:
上述equals方法中的元素遍历使用了get(i)方法。对于ArrayDeque这样的基于数组的实现,get(i)通常是O(1)操作,因此整个equals方法的时间复杂度是O(N),其中N是集合的大小。然而,对于某些Deque实现,例如基于链表的LinkedListArrayDeque,get(i)操作可能需要从头开始遍历,导致其时间复杂度为O(i),从而使得整个equals方法的时间复杂度变为O(N^2),这在处理大型集合时会带来显著的性能问题。
为了避免这种性能瓶颈,我们应该利用Java集合框架中的Iterator接口。迭代器提供了一种高效、顺序访问集合元素的方式,通常是O(1)的next()操作,从而将整个比较过程的时间复杂度保持在O(N)。
假设ArrayDeque已经实现了Iterable<T>接口,并且其iterator()方法能够提供有效的迭代器(如问题中所示的ArrayDequeIterator),我们可以这样优化equals方法:
import java.util.Iterator; // 虽然原问题要求不使用java.util.*,但Iterator接口是集合框架的基础,通常被允许使用
public class ArrayDeque<T> implements Deque<T>, Iterable<T> {
    // ... 其他成员和方法 ...
    @Override
    public Iterator<T> iterator() {
        return new ArrayDequeIterator();
    }
    private class ArrayDequeIterator implements Iterator<T> {
        private int currentPosition = firposition; // 迭代器当前位置
        private int elementsTraversed = 0; // 已遍历元素计数
        @Override
        public boolean hasNext() {
            return elementsTraversed < size;
        }
        @Override
        public T next() {
            if (!hasNext()) {
                throw new java.util.NoSuchElementException();
            }
            T item = ts[currentPosition];
            currentPosition = (currentPosition + 1) % ts.length;
            elementsTraversed++;
            return item;
        }
    }
    @Override
    public boolean equals(Object o) {
        if (o == this) {
            return true;
        }
        if (o == null) {
            return false;
        }
        if (!(o instanceof Deque)) {
            return false;
        }
        Deque<?> otherDeque = (Deque<?>) o;
        if (otherDeque.size() != this.size()) {
            return false;
        }
        // 使用迭代器进行元素遍历和比较,提高效率
        Iterator<T> thisIterator = this.iterator(); // 获取当前Deque的迭代器
        // 假设 otherDeque 也是 Iterable 的,或者其 get(i) 性能可接受
        // 如果 otherDeque 也是自定义的,且我们知道它有高效的迭代器,可以这样:
        // Iterator<?> otherIterator = otherDeque.iterator();
        // 但根据原问题,oll.get(i) 是一个通用接口方法,我们先沿用它。
        // 如果 Deque 接口本身没有 iterator() 方法,但 ArrayDeque 实现了 Iterable,
        // 那么对于 otherDeque,我们可能只能依赖 get(i) 或假设它也是 Iterable。
        // 为了通用性,我们这里继续使用get(i)作为otherDeque的访问方式,
        // 但强调如果otherDeque也提供迭代器,应优先使用。
        int i = 0;
        while (thisIterator.hasNext()) { // 遍历当前Deque的元素
            Object elementOfThis = thisIterator.next();
            Object elementOfOther = otherDeque.get(i); // 访问另一个Deque的对应元素
            i++;
            if (elementOfThis == elementOfOther) {
                continue;
            }
            if (elementOfOther == null) {
                return false;
            }
            if (!elementOfThis.equals(elementOfOther)) {
                return false;
            }
        }
        return true;
    }
}关于ArrayDequeIterator的改进: 原始问题中的ArrayDequeIterator逻辑略显复杂,为了更清晰和健壮,可以简化hasNext()和next()方法,直接利用size和currentPosition(或elementsTraversed)来判断。上面示例中提供了一个更简洁的ArrayDequeIterator实现。
更进一步的迭代器优化(如果Deque接口也支持迭代器): 如果Deque接口本身就定义了iterator()方法(这是标准的做法),那么两个Deque都可以使用迭代器进行遍历,从而实现最优的O(N)时间复杂度:
// 假设 Deque 接口定义了 iterator() 方法
// public interface Deque<T> extends Iterable<T> { ... }
@Override
public boolean equals(Object o) {
    if (o == this) {
        return true;
    }
    if (o == null) {
        return false;
    }
    if (!(o instanceof Deque)) {
        return false;
    }
    Deque<?> otherDeque = (Deque<?>) o;
    if (otherDeque.size() != this.size()) {
        return false;
    }
    // 同时使用两个迭代器进行元素遍历
    Iterator<T> thisIterator = this.iterator();
    Iterator<?> otherIterator = otherDeque.iterator(); // 获取另一个Deque的迭代器
    while (thisIterator.hasNext()) {
        Object elementOfThis = thisIterator.next();
        Object elementOfOther = otherIterator.next(); // 两个迭代器同步前进
        if (elementOfThis == elementOfOther) {
            continue;
        }
        if (elementOfOther == null) { // 此时 elementOfThis 必不为 null
            return false;
        }
        if (!elementOfThis.equals(elementOfOther)) {
            return false;
        }
    }
    // 因为之前已经检查了size相等,且迭代器同步前进,
    // 如果一个迭代器hasNext()为false,另一个也应如此。
    // 如果循环结束,则所有元素都相等。
    return true;
}这种双迭代器同步遍历的方式是比较两个集合内容最标准和高效的做法。
以上就是Java自定义Deque的equals方法深度比较与性能优化实践的详细内容,更多请关注php中文网其它相关文章!
 
                Copyright 2014-2025 https://www.php.cn/ All Rights Reserved | php.cn | 湘ICP备2023035733号