这一章主要介绍了如何覆盖Object的非final方法和Comparable.compareTo方法。

如何重写equals方法

当一个类具有特定的逻辑相等的概念而不是对象等同(即obj1 == obj2)时,类的编写者就需要考虑重写equals方法,但是不恰当的重写equals方法容易导致一些问题,所以在重写equals方法时,应该遵守一些通用的约定。

  1. 自反性,即 x.equals(x)true
  2. 对称性,即 x.equals(y)true 的充要条件是y.equals(x)true
  3. 传递性,即 x.equals(y)truey.equals(z)true,那么x.equals(z) 也为 true
  4. 一致性,即在没有对 x 和 y,做任何修改的情况下,任何时候调用x.equals(y)的值应该是一致的。
  5. 对于一个非null的引用,x.equals(null)必须是false

自反性要求一个对象比如等价于自身。对称性涉及两个对象之间的比较,假如实现一个CaseInsensitiveString类,并重写equals方法。

public class CaseInsensitiveString {
    private final String s;

    public CaseInsensitiveString(String s) {
        this.s = s;
    }

    @Override
    public boolean equals(Object o) {
        if ((o instanceof CaseInsensitiveString)) {
            return ((CaseInsensitiveString)o).s.equalsIgnoreCase(s);
        }
        if(o instanceof String) {
            return s.equalsIgnoreCase((String)o);
        }
        return false;
    }
}

在重写的equals方法中,判断了比较对象是否是String类型,显然这会违反对称性,因为String.equals(CaseInsensitiveString)一定会返回false,而CaseInsensitiveString.equals(String)可能会返回true。 传递性涉及 3 个对象之间的比较,考虑这样的情形,Point类有两个整型变量,表示二维平面的坐标,而colorPoint类继承自Point类,它增加了一个color属性。



为了不违反对称性,如果Point.equals(ColorPoint)trueColorPoint.equals(Point)必须为true,所以ColorPointequals方法可以是这样的。


上代码创建的 3 个对象可以看出,这样就违反了传递性,所以没法继承一个对象并且添加一个值后,仍然能遵守equals方法的约定。 针对这种情况,可以使用_组合优于继承_的原则,将ColorPoint改写成这样。

// Composition

一致性要求在两个对象没有变更过的情况下,任何一次调用equals方法的结果必须是一样的。无论是可变对象还是不可变对象,equals方法不能依赖不可靠的资源。

下面这些诀窍有助于实现高质量的equals方法。

  1. 首先使用==操作符检查参数的引用是否就是当前的对象。
  2. 使用instanceof操作符检查参数是否是正确的类型,因为equals方法的参数类型是Object
  3. 在比较前将参数强制转换成正确的类型。
  4. 在两个对象检查每个待比较的成员变量是否匹配。 在比较时,对于除floatdouble的基本类型,使用==操作符,对于引用,调用它们的equals进行比较,对于数组类型则需要比较每个元素(可以调用Arrays.equals()的重载方法),对于floatdouble类型,使用其包装类型的compare进行比较。
  5. 在完成equals方法之后检查它是否遵守了equals方法的约定。

同时重写equals方法和hashCode方法

计算对象的哈希值的变量必须与equals一致。在重写equals方法后,必须以相同的方式重写hashCode方法,否则会导致依赖对象hashCode方法的行为与预期不符,例如HashMapHashSet等。

重写hashCode方法的原则如下:

  1. 在不修改equals相关的成员变量的情况下,每次调用hashCode方法的返回值应该一致。
  2. 如果x.equals(y)true,那么x.hashCode()y.hashCode()必须相等。
  3. 如果x.equals(y)false,那么x.hashCode()y.hashCode()可以不同,也可以相同,如果要产生更好的性能,尽量不通。

理想情况下,hashCode方法应该为不同对象产生不同的哈希值,下面这种方法可以给出相近的效果。

  1. 选择一个非 0 值作为初始值,比如 17,赋值给result; 2.a 对于每一个待计算哈希值的变量,按照如下规则计算:
    • 1)如果类型是boolean,计算f ? 1 : 0
    • 2)如果类型是bytechar

第一步选择一个非 0 的初始值,是为了让 2.a 为 0 的初始域影响到哈希值,进而增加减少哈希值冲突(考虑一个集合中有多种类型,这多种类型是都集继承自一个父类,计算哈希值的初始域为 0,每个子类的非零初始值选择不一样)。在 2.b 中选择使用 31 来计算哈希值,有两个原因,其中一个原因是必须选择一个非 0 值来计算,减少冲突(考虑相同字母异序字符串,如果不使用非 0 值,将会计算出相同的哈希值),另一个原因是31 * i == (i << 5) - i,计算性能更高。

始终重写toString方法

按照具体实现类的需求重写toString方法,有助于该类更易使用,所以toString方法应该返回所有该类重要的字段信息。比如对于PhoneNumbertoString方法的返回值

谨慎重写clone方法

Java 的Object类提供了clone方法,但是一个类如果不实现Cloneable接口,则无法成功调用clone方法,可见Cloneable接口只是标识了clone方法能否被正确调用。Object.clone的描述部分如下:

创建一个对象的拷贝,这个拷贝的精确含义取决于具体clone方法的实现。对于一个对象来说 x.clone() != xtruex.clone().getClass() == x.getClass()true,但是者不是必须的。 x.clone().equals(x)true,但是这也不是必须的。拷贝一个对象将会创建一个新的对象,并且复制内部数据结构,但是这些操作不会调用构造方法。

在重写clone方法时,必须调用super.clone(),来克隆一个当前类的对象,对于PhoneNumer类,其clone方法如下。

@Override
public PhoneNumber clone()  {
    try {
        return (PhoneNumber)super.clone();
    } catch (CloneNotSupportedException e) {
        throw new AssertionError();
    }
}

该重写方法返回的是PhoneNumber类型,而Objectclone方法返回的类型是Object,对于 JDK 1.5 发布的_协变返回类型_,这是可行的,所以对于成员变量为基本类型以及不可变对象的类,这样克隆一个对象,是合适的,但是如果成员变量有可变对象,那么继续使用类似PhoneNumber中的方式就会产生问题。

public class Stack {
	
	private Object[] elements;
	
	private int size;
	
	private static final int DEFAULT_INITIAL_CAPACITY = 16;
	
	public Stack() {
		this.elements = new Object[DEFAULT_INITIAL_CAPACITY];
	}
	
	public void push(Object o) {
		ensureCapacity();
		elements[size++] = o;
	}
	
	public Object pop() {
		if(size == 0) {
			throw new RuntimeException("empty stack");
		}
		Object result = elements[--size];
		elements[size] = null;
		return result;
	}
	
	private void ensureCapacity() {
		if(elements.length == size) {
			elements = Arrays.copyOf(elements, 2 * size + 1);
		}
	}
}

Stack类中,有一个数组类型的成员变量,如果在clone方法中直接调用super.clone(),则新对象的elements属性与原对象都指向同一个引用,修改了新对象的elements,原对象也会跟着改变。所以,如果要正确的定义clone方法,必须递归的调用可变对象的clone方法,那么这就要求这个可变对象的类也集成了Cloneable接口,并且其clone方法能够正确返回克隆对象。Stack.clone方法定义如下。

@Override
public Stack clone() {
    try {
        Stack stack = (Stack)super.clone();
        stack.elements = this.elements.clone();
        return stack;
    } catch (CloneNotSupportedException e) {
        throw new AssertionError(e);
    }
    
}

需要注意的是,现在这个版本的Stack.clone方法针对基本类型,或者String类型,是可以正常运行的,但是如果elements数组存的是其他类型,又需要单独针对每个对象调用clone方法进行拷贝。所以在clone方法中,可以采取一些其他手段对可变对象进行拷贝(克隆),比如自定义一种拷贝方法,但是需要注意的是,clone方法相当于另一种构造方法,在待克隆的对象创建完之前,调用对象的方法会产生一些问题。 另外,Object.clone()是非线程安全的,在多线程的场景下,重写clone方法需要保证线程安全。

实现Cloneable接口从而重写clone方法来做对象拷贝(克隆)存在一系列问题,所以应该尽量避免使用该方式来拷贝对象。

实现Comparable接口

Comparable接口的compareTo方法与Objectequals方法类似,也必须遵守类似equals方法的约束:自反性,对成性,传递性,所以无法通过继承来扩展类的同时保持这三种约束。

此外,实现compareTo方法时,建议x.compareTo(y) == 0的同时,x.equals(y)true,但是这只是建议,不是必须的。比如BigDecimal类,对象BigDecimal b1 = new BigDecimal("1.0")BigDecimal b2 = new BigDecimal("1.00")b1.equals(b2)false,但是b1.compareTo(b2)为 0。