对于所有的对象都通用的方法
覆盖 equals 请遵守通用约定
什么时候应该覆盖 equals 方法?
如果类具有自己特有的“逻辑相等”的概念,比如 Integer 或者 String。程序员需要利用 equals 方法来比较逻辑相等,而不是它们是否指向同一个对象。
有一种“值类”不需要覆盖 equals 方法
用实例受控确保“每个值最多只存在一个对象的类”,比如 Boolean ,Enum 等类。
在覆盖 equals 方法的时候,必须遵守它的通用规定
- 自反性:对于任何非 null 的 x,x.equals(x)必须返回 true。
- 对称性:y.equals(x) = x.equals(y)
- 传递性:x.equals(y) = true = y.equals(z),则 y.equals(z) = true
- 一致性:当对象没有被修改,多次调用 x.equals(y) ,返回的结果相同。
- x.equals(null) 必须返回 false。
- 非空性:所有非空对象都不能等于 null。
如何实现高质量的 equals 方法 - 使用 == 操作符检查“参数是否为这个对象的引用”。
- 使用 Instanceof 操作符检查“参数是否为正确的类型”。此处的类型可以为接口,允许该接口的实现类间进行比较。
- 把参数转换为正确的类型。
- 检查“关键”域是否匹配。
对于非 float 和 double 类型的基本类型域,使用 == 比较。否则使用 Float.compare 方法和 Double.compare 方法。
举例
@Override
public boolean equals(Object o) {
if (o == this)
return true;
if (!(o instanceof PhoneNumber))
return false;
PhoneNumber pn = (PhoneNumber)o;
return pn.lineNum == lineNum && pn.prefix == prefix
&& pn.areaCode == areaCode;
}
警告
- 覆盖 equals 时总要覆盖 hashCode。
- 不要将 equals 声明中的 Object 对象替换为其他的类型。
覆盖 equals 时总要覆盖hashCode
如果不这样做的话,该类就无法结合所有基于散列的集合一起正常运作,如 HashMap 和 HashSet。
规定:相等的对象必须具有相等的散列码。
HashMap 类的 get 方法是根据参数的散列码获取的。
举例:一个合法但不好用的 hashCode 方法永远都不应该被使用。
@Override
public int hashCode() {
return 42;
}
举例:用关键域的参数来计算散列码,并用素数相乘,31可以用位移和减法代替乘法。
@Override
public int hashCode() {
int result = Short.hashCode(areaCode);
result = 31 * result + Short.hashCode(prefix);
result = 31 * result + Short.hashCode(lineNum);
return result;
}
始终要覆盖 toString
提供好的 toString 实现可以使类用起来更加舒适,使用了这个类的系统也更易于调试。
在静态工具类和枚举类中编写 toString 方法是没有意义的。
谨慎地覆盖 clone
约定:
- x.clone() != x
- x.clone().getClass() == x.getClass()
- x.clone().equals(x)
不可变的类永远都不应该提供 clone 方法。
举例:
@Override
public PhoneNumber clone() {
try{
return (PhoneNumber) super.clone;
} catch (CloneNotSupportedException e) {
throw new AssertionError();
}
}
实际上,clone 方法就是另一个构造器;必须确保它不会伤害到原始对象,并确保正确地创建被克隆对象中的约束条件。
举例:为了将 elements 数组中的内容全部克隆,最容易的做法是,递归调用。
@Override
public Stack clone() {
try{
Stack result = (Stack) super.clone();
result.elements = elements.clone();
return result;
} catch (CloneNotSupportedException e) {
throw new AssertionError();
}
}
如果你编写线程安全的类准备实现Cloneable 接口,要记住它的 clone 方法必须得到严格的同步。
除此之外,还有一些好的办法。
- 提供一个拷贝构造器。
public Yum(Yum yum) {...}
- 拷贝工厂方法
public static Yum newInstance(Yum yum) {...}
这种方法的好处是你甚至可以转换类型。
新的接口不应该扩展 Cloneable 接口,新的可扩展的类也不应该实现这个接口。
考虑实现 Comparable 接口
为实现 Comparable 接口的对象数组进行排序非常简单。
Arrays.sort(a);
对存储在集合中的 Comparable 对象进行搜索、计算极限值以及自动维护也同样简单。
public class wordList {
public static void main(String[] args) {
Set<String> s = new TreeSet<>();
Collections.addAll(s,args);
System.out.println(s);
}
}
如果你正在编写一个值类,它具有非常明显的内在排序关系,比如按字母顺序、按数值顺序或者按年代顺序,那你就应该坚决考虑实现 Comparable 接口。
public interface Comparable<T> {
/**
* Compares this object with the specified object for order. Returns a
* negative integer, zero, or a positive integer as this object is less
* than, equal to, or greater than the specified object.
*
* <p>The implementor must ensure <tt>sgn(x.compareTo(y)) ==
* -sgn(y.compareTo(x))</tt> for all <tt>x</tt> and <tt>y</tt>. (This
* implies that <tt>x.compareTo(y)</tt> must throw an exception iff
* <tt>y.compareTo(x)</tt> throws an exception.)
*
* <p>The implementor must also ensure that the relation is transitive:
* <tt>(x.compareTo(y)>0 && y.compareTo(z)>0)</tt> implies
* <tt>x.compareTo(z)>0</tt>.
*
* <p>Finally, the implementor must ensure that <tt>x.compareTo(y)==0</tt>
* implies that <tt>sgn(x.compareTo(z)) == sgn(y.compareTo(z))</tt>, for
* all <tt>z</tt>.
*
* <p>It is strongly recommended, but <i>not</i> strictly required that
* <tt>(x.compareTo(y)==0) == (x.equals(y))</tt>. Generally speaking, any
* class that implements the <tt>Comparable</tt> interface and violates
* this condition should clearly indicate this fact. The recommended
* language is "Note: this class has a natural ordering that is
* inconsistent with equals."
*
* <p>In the foregoing description, the notation
* <tt>sgn(</tt><i>expression</i><tt>)</tt> designates the mathematical
* <i>signum</i> function, which is defined to return one of <tt>-1</tt>,
* <tt>0</tt>, or <tt>1</tt> according to whether the value of
* <i>expression</i> is negative, zero or positive.
*
* @param o the object to be compared.
* @return a negative integer, zero, or a positive integer as this object
* is less than, equal to, or greater than the specified object.
*
* @throws NullPointerException if the specified object is null
* @throws ClassCastException if the specified object's type prevents it
* from being compared to this object.
*/
public int compareTo(T o);
}
约定:当这个对象与指定对象比较的时候,当该对象小于、等于或大于指定对象时,分别返回一个负整数,零或者正整数。
- sgn(x.compareTo(y))==-sgn(y.compareTo(x)) (暗示着抛异常也如此)
- 传递性
- 强烈建议(x.compareTo(y) == 0) == (x.equals(y))
视图模式:
下面是 HashMap 这个类的源码
public class HashMap<K,V> extends AbstractMap<K,V>
implements Map<K,V>, Cloneable, Serializable {
//父类 AbstractMap 中创建了 keySet 实例,如下
// transient Set<K> keySet;
// 外部的类通过调用该方法获得 keySet 实例,但没办法对底层的 keySet 进行操作
public Set<K> keySet() {
Set<K> ks = keySet;
if (ks == null) {
ks = new KeySet();
keySet = ks;
}
return ks;
}
// 内部类 KeySet ,进行权限控制
final class KeySet extends AbstractSet<K> {
public final int size() { return size; }
public final void clear() { HashMap.this.clear(); }
public final Iterator<K> iterator() { return new KeyIterator(); }
public final boolean contains(Object o) { return containsKey(o); }
public final boolean remove(Object key) {
return removeNode(hash(key), key, null, false, true) != null;
}
public final Spliterator<K> spliterator() {
return new KeySpliterator<>(HashMap.this, 0, -1, 0, 0);
}
public final void forEach(Consumer<? super K> action) {
Node<K,V>[] tab;
if (action == null)
throw new NullPointerException();
if (size > 0 && (tab = table) != null) {
int mc = modCount;
for (int i = 0; i < tab.length; ++i) {
for (Node<K,V> e = tab[i]; e != null; e = e.next)
action.accept(e.key);
}
if (modCount != mc)
throw new ConcurrentModificationException();
}
}
}
}
言归正传,如果你想为一个实现了 Comparable 的类增加值组件,请不要扩展这个类;而是要编写一个不相关的类,其中包含第一个类的一个实例,然后提供一个“视图”方法返回这个实例。这样既可以让你自由地在第二个类上实现 compareTo 方法,同时也允许它的客户端在必要的时候,把第二个类的实例视同第一个类的实例。
对于(x.compareTo(y) == 0) == (x.equals(y)) 这个约定,BigDecimal 中的这两个方法是不同的。
Java 7 中已经在所有的装箱基本类型中增加了静态的 compare 方法。
举例:先比较最关键的域
public int CompareTo(PhoneNumber pn) {
// Short 类是 Comparable 的实现类
int result = Short.compare(areaCode,pn.areaCode);
if (result == 0) {
result = Short.compare(prefix,pn.prefix);
if (result == 0) {
result = Short.compare(lineNum,pn.lineNum);
}
}
return result;
}
在 Java 8 中,Comparator 接口配置了一组比较器的构造方法,使得比较器的构造工作变得非常流畅。
private static final Compartor<PhoneNumber> COMPARATOR =
comringInt((PhoneNumber pn) -> pn.areaCode)
.thenComparingInt(pn -> pn.prefix)
.thenComparingInt(pn -> pn.lineNum);
public int compareTo(PhoneNumber pn) {
return COMPARATOR.compare(this,pn);
}
使用 compare 方法时,不要两个数相减,很容易造成整数溢出。
可以使用如下静态方法
static Comparator<Object> hashCodeOrder = new Comparator<>() {
public int compare(Object o1,Object o2) {
return Integer.compare(o1.hashCode(),o2.hashCode());
}
}
或是比较器构造方法
static Comparator<Object> hashCodeOrder =
Comparator.comaringInt(o -> o.hashCode());
总结:避免使用 < 和 > 操作符,而应该使用装箱基本类型中的 compare 方法,或者在 Comparator 接口中使用比较器构造方法。