目录
1、前言
《Effective Java》这本书可以说是程序员必读的书籍之一。这本书讲述了一些优雅的,高效的编程技巧。对一些方法或API的调用有独到的见解,还是值得一看的。刚好最近重拾这本书,看的是第三版,顺手整理了一下笔记,用于自己归纳总结使用。建议多读一下原文。今天整理第二章节:对所有对象都通用的方法。
2、在重写equals方法时要遵守通用约定
要避免犯错,最简单的方式就是不重写。这样的话,该类的实例只会与自身相等。
如果满足下述条件中的一个,不重写equals方法就是合理的:
- 该类的每个实例在本质上都是唯一的。对于诸如Thread这样代表活动实体而不是值的类来说,这是成立的。
- 该类没有必要提供一个“逻辑相等”的测试。如java.util.regex.Pattern。
- 超类已经重写了equals方法,而且其行为适合这个类。
- 类是私有的或包私有的,我们可以确信其equals方法绝对不会被调用。
2.1、什么时候重写equals方法呢?
- 当一个类在对象相同之外还存在逻辑相等的概念,而且其上层超类都没有重写equals方法。这通常就是值类的情况。如Integer或String。
在重写equals方法时,必须遵守通用约定。equals方法用来判断等价关系,有如下属性:
在尝试重写equals方法时,千万不要忽视这个约定。如果违反了,可能程序就会表现的不正常,甚至崩溃。
- 自反性:对于任何非null的引用值x,x.equals(x)必须返回true
- 对称性:对于任何非null的引用值x和y,当且仅当y.eguals(x)返回true 时,x.equals(y)必须返回 true
- 传递性:对于任何非null的引用值x、y和z,如果x.equals(y)返回 true,并且y.equals(z)返回true,那么x.equals(z)必须返回 true
- 一致性:对于任何非null的引用值x和y,只要equals 比较中用到的信息没有修改,多次调用x.equals(y)必须一致地返回 true 或一致地返回false
- 对于任何非 null的引用值 x,x.equals(nul1)必须返回 false
2.2、重写equals的注意事项
- 重写equals方法时,应该总是重写hashCode方法
- 不要自作聪明。不要过度考虑各种相等关系。如File类不应该将多个指向同一个文件的符号连接视为相等。
- 不要将equals方法声明中的Object替换为其他类型。如:
public boolean equals(MyClass o) {
}
该方法只是重载了equals方法,而不是重写。应该强制使用@Override注解,在编译期间会告知我们哪里出错了。
总而言之,除非迫不得已,否则不要重写equals方法。多数情况下,从Object继承的equals实现就能满足。
3、重写equals方法时应该总是重写hashCode方法
重写equals方法的每个类都必须重写hashCode。
3.1、为什么要重写hashCode
如果没有这样做,类就会违反hashCode的通用约定,这将使其实例无法正常应用与诸如HashMap和HashSet等集合中。
hashCode有其通用约定,如下:
- 当在一个对象上重复调用hashCode方法时,只要在equals的比较中用到的信息没有修改,他就必须一致的返回同样的值
- 如果根据equals方法,两个对象是相等的,那么在这两个对象上调用hashCode方法,必须产生同样的整数结果
- 如果根据equals方法,两个对象不相等,那么在这两个对象上调用hashCode方法,并不要求产生不同的结果,只是不相等的对象产生不同的结果,可能会提高哈希表的性能
3.2、推荐的hashCode方法
- 声明一个名为result的int变量,将其初始化为对象中第一个重要字段的hash码
- 对其余的每个重要字段,如果是基本类型,则使用Type.hashCode(x)来计算。如果是对象引用,则在该字段上递归调用hashCode。如果是数组类型,则调用Arrays.hashCode。然后按照如下方式合并到result中:
result = 31 * result + c // c为该字段的hashCode
- 返回result
4、总是重写toString方法
虽然Object类提供了toString方法的一个实现,但他返回的字符串通常不是类的用户所希望看到的。toString的约定指出:“建议所有的子类都重写这个方法”。
- 提供一个好的toString实现可以让类用起来更舒适,使用该类的系统也更容易调试
- 如果条件允许,toString方法应该返回当前对象中包含的所有有意义的信息
- 无论是否决定指定格式,都应该在文档中清晰地表明自己的意图
借助Google的AutoValue或IDE自带的生成toString方法也比Object自带的toString好得多。
5、谨慎重写clone方法
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()
调用时就会抛出异常。
5.1、替代Cloneable的复制方法
与 Cloneable
和 clone
方法相关的复杂性通常是不必要的。更好的方法是提供复制构造函数或复制工厂方法。复制构造函数是一种以自身类型作为参数的构造函数:
// 复制构造器
public Yum(Yum yum) { ... }
// 复制工厂
public static Yum newInstance(Yum yum) { ... }
与Cloneable/clone相比,复制构造器方式及其静态工厂变体有许多优点:他们不依赖于Java核心语言之外的、存在风险的对象创建机制;他们不需要遵守基本没有文档说明的约定,更何况这样的约定还没有办法强制实施;他们与final字段的正常使用没有冲突;他们不会抛出不必要的检查型异常;他们不需要类型转换。
5.2、小结
当你重写 clone
时,确保使用 super.clone()
并对任何可变对象进行深拷贝。如果类的所有字段都是不可变的,则无需额外处理。对于大多数情况下,使用复制构造函数或工厂方法代替 clone
是更好的选择。
6、考虑实现Comparable接口
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());
6.1、小结
总而言之,每当要实现一个可以合理地进行排序地值类时,都应该让这个类实现Comparable接口,这样他的实例就可以轻松地被排序、查找和用在基于比较地集合中。在CompareTo方法地实现中,当比较字段的值时,应该避免使用<和>运算符。相反,请使用基本类型的封装类中的静态compare方法,或使用Comparator接口中的比较器构造方法。
7、粉丝福利
感谢您耐心阅读完这篇技术博客!希望这些内容能为您的技术探索之旅带来启发和帮助。
为了回馈一直支持我的粉丝朋友们,我特别准备了一份专属福利!🎉
《推荐系统:技术原理与实践》是一本深入浅出地讲解推荐系统核心原理与实战技巧的优质书籍。无论是初学者还是有一定基础的技术人员,都能从中获取宝贵的知识和灵感。
福利获取方式:
1. 关注我的公众号:柴犬说编程
2. 在公众号对话框中回复“抽奖”
3. 即可获得抽奖链接,参与抽奖,赢取这本书籍!
抽奖活动将持续到2025-02-28 22:00,中奖名单将在公众号公布,敬请关注!
再次感谢您的支持与陪伴,希望这本书能成为您技术成长路上的得力助手。祝您好运!😊