《Effective Java》这本书可以说是程序员必读的书籍之一。这本书讲述了一些优雅的,高效的编程技巧。对一些方法或API的调用有独到的见解,还是值得一看的。刚好最近重拾这本书,看的是第三版,顺手整理了一下笔记,用于自己归纳总结使用。建议多读一下原文。今天整理第二章节:对所有对象都通用的方法。
要避免犯错,最简单的方式就是不重写。这样的话,该类的实例只会与自身相等。
如果满足下述条件中的一个,不重写equals方法就是合理的:
在重写equals方法时,必须遵守通用约定。equals方法用来判断等价关系,有如下属性:
在尝试重写equals方法时,千万不要忽视这个约定。如果违反了,可能程序就会表现的不正常,甚至崩溃。
public boolean equals(MyClass o) {
}
该方法只是重载了equals方法,而不是重写。应该强制使用@Override注解,在编译期间会告知我们哪里出错了。
总而言之,除非迫不得已,否则不要重写equals方法。多数情况下,从Object继承的equals实现就能满足。
重写equals方法的每个类都必须重写hashCode。
如果没有这样做,类就会违反hashCode的通用约定,这将使其实例无法正常应用与诸如HashMap和HashSet等集合中。
hashCode有其通用约定,如下:
result = 31 * result + c // c为该字段的hashCode
return result
虽然Object类提供了toString方法的一个实现,但他返回的字符串通常不是类的用户所希望看到的。toString的约定指出:“建议所有的子类都重写这个方法”。
借助Google的AutoValue或IDE自带的生成toString方法也比Object自带的toString好得多。
Cloneable 接口的设计初衷是作为一个混合接口,用来表明类支持克隆功能。然而,它没有达到这个目的,主要的缺陷是缺乏 clone 方法,而 Object 类中的 clone 方法是受保护的。你无法直接调用 clone 方法,即便对象实现了 Cloneable,除非借助反射。更糟糕的是,反射调用也不一定总能成功,因为对象可能没有可访问的 clone 方法。
由于clone()方法并不保证正确行为,除非类路径上的每个类都正确覆盖了clone()方法,并且正确调用了super.clone(),因此在Effective Java中,推荐使用“复制构造器”或者“私有构造器和静态工厂方法”来替代clone()方法。
public class Example implements Cloneable {
private int[] values;
public Example(int[] values) {
this.values = values.clone();
}
// 复制构造器
public Example(Example original) {
this.values = original.values.clone();
}
// 如果确实需要实现Cloneable接口的clone方法,可以按照如下方式实现
@Override
public Example clone() {
try {
return (Example) super.clone();
} catch (CloneNotSupportedException e) {
throw new AssertionError(); // Can never happen
}
}
}
以上代码中,我们使用clone()方法来复制对象时,如果对象中包含数组等引用类型,应当在复制构造器中对这些引用类型也进行深复制。同时,由于clone()方法可能会抛出CloneNotSupportedException,所以通常会像示例中那样通过AssertionError来保证这个异常永远不会发生,因为如果Example类不支持Cloneable接口,那么super.clone()调用时就会抛出异常。
与 Cloneable 和 clone 方法相关的复杂性通常是不必要的。更好的方法是提供复制构造函数或复制工厂方法。复制构造函数是一种以自身类型作为参数的构造函数:
// 复制构造器
public Yum(Yum yum) { ... }
// 复制工厂
public static Yum newInstance(Yum yum) { ... }
与Cloneable/clone相比,复制构造器方式及其静态工厂变体有许多优点:他们不依赖于Java核心语言之外的、存在风险的对象创建机制;他们不需要遵守基本没有文档说明的约定,更何况这样的约定还没有办法强制实施;他们与final字段的正常使用没有冲突;他们不会抛出不必要的检查型异常;他们不需要类型转换。
当你重写 clone 时,确保使用 super.clone() 并对任何可变对象进行深拷贝。如果类的所有字段都是不可变的,则无需额外处理。对于大多数情况下,使用复制构造函数或工厂方法代替 clone 是更好的选择。
compareTo 并非 Object 类中声明的,而是 Comparable 接口的唯一方法。compareTo 方法与 equals 类似,但它不仅支持相等性比较,还允许顺序比较,同时它是泛型的。通过实现 Comparable 接口,一个类表明其实例具有自然顺序。这使得对实现 Comparable 的对象数组进行排序变得非常简单:
Arrays.sort(a);
几乎Java平台类库中所有的值类,以及所有的枚举类型都实现了Comparable接口。实现Comparable接口如下:
public interface Comparable<T> {
int compareTo(T t);
}
编写 compareTo 方法类似于编写 equals 方法,但有一些关键区别。由于 Comparable 是参数化接口,因此 compareTo 方法是静态类型化的,避免了类型检查和强制转换。如果参数类型错误,代码甚至无法编译。
在 compareTo 方法中,字段是按顺序比较的。对于对象引用字段,可以递归调用 compareTo 方法。如果字段没有实现 Comparable,或者需要非标准排序,可以使用 Comparator。例如,下面是一个比较 CaseInsensitiveString 类的 compareTo 方法:
// 使用对象引用字段的单字段 Comparable
public final class CaseInsensitiveString implements Comparable<CaseInsensitiveString> {
public int compareTo(CaseInsensitiveString cis) {
return String.CASE_INSENSITIVE_ORDER.compare(s, cis.s);
}
// 其他代码省略
}
effective java此前的几个版本推荐在compareTo方法中使用关系运算符<和>来比较整形的基本类型字段,使用Double.compare和Float.compare来比较浮点型基本类型字段。而在Java7中,所有基本类型封装类都添加静态的compare方法。在compareTo方法中使用<和>非常繁琐,而且容易出错,所以不再推荐。
Java8中,Comparator接口提供了一组比较构造器方法,这些比较器可以用来实现Comparable接口所要求的compareTo方法,不过性能上会稍微慢一些。使用比较器构造方法实现的Comparable如下:
// 使用比较器构造方法的 Comparable
private static final Comparator<PhoneNumber> COMPARATOR =
comparingInt((PhoneNumber pn) -> pn.areaCode)
.thenComparingInt(pn -> pn.prefix)
.thenComparingInt(pn -> pn.lineNum);
public int compareTo(PhoneNumber pn) {
return COMPARATOR.compare(this, pn);
}
基于差的比较器,会破坏传递性,存在问题。应该避免使用:
// 错误的差值比较器 - 违反传递性
static Comparator<Object> hashCodeOrder = new Comparator<>() {
public int compare(Object o1, Object o2) {
return o1.hashCode() - o2.hashCode();
}
};
使用静态的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.comparingInt(o -> o.hashCode());
总而言之,每当要实现一个可以合理地进行排序地值类时,都应该让这个类实现Comparable接口,这样他的实例就可以轻松地被排序、查找和用在基于比较地集合中。在CompareTo方法地实现中,当比较字段的值时,应该避免使用<和>运算符。相反,请使用基本类型的封装类中的静态compare方法,或使用Comparator接口中的比较器构造方法。
原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。
如有侵权,请联系 cloudcommunity@tencent.com 删除。
原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。
如有侵权,请联系 cloudcommunity@tencent.com 删除。