https://github.com/clxering/Effective-Java-3rd-edition-Chinese-English-bilingual/blob/dev/Chapter-3
准则一 覆盖 equals 方法时应遵守的约定
重写equals 方法需要满足的特性
-
Reflexive: For any non-null reference value x, x.equals(x) must return true.
反身性:对于任何非空的参考值 x,x.equals(x) 必须返回 true。 -
Symmetric: For any non-null reference values x and y, x.equals(y) must return true if and only if y.equals(x) returns true.
对称性:对于任何非空参考值 x 和 y,x.equals(y) 必须在且仅当 y.equals(x) 返回 true 时返回 true。 -
Transitive: For any non-null reference values x, y, z, if x.equals(y) returns true and y.equals(z) returns true, then x.equals(z) must return true.
传递性:对于任何非空的引用值 x, y, z,如果 x.equals(y) 返回 true,y.equals(z) 返回 true,那么 x.equals(z) 必须返回 true。 -
Consistent: For any non-null reference values x and y, multiple invocations of x.equals(y) must consistently return true or consistently return false, provided no information used in equals comparisons is modified.
一致性:对于任何非空的引用值 x 和 y, x.equals(y) 的多次调用必须一致地返回 true 或一致地返回 false,前提是不修改 equals 中使用的信息。 -
For any non-null reference value x, x.equals(null) must return false.
对于任何非空引用值 x,x.equals(null) 必须返回 false。
高质量构建 equals 方法的秘诀
- 使用 == 运算符检查参数是否是对该对象的引用。
- 使用 instanceof 运算符检查参数是否具有正确的类型。
- 将参数转换为正确的类型
- 对于类中的每个「重要」字段,检查参数的字段是否与该对象的相应字段匹配。
对于类型不是 float 或 double 的基本类型字段,使用 == 运算符进行比较;对于对象引用字段,递归调用 equals 方法;对于 float 字段,使用 static Float.compare(float,float) 方法;对于 double 字段,使用 Double.compare(double, double)。float 和 double 字段的特殊处理是由于 Float.NaN、-0.0f 和类似的双重值的存在而必须的;请参阅 JLS 15.21.1 或 Float.equals 文档。虽然你可以将 float 和 double 字段与静态方法 Float.equals 和 Double.equals 进行比较,这将需要在每个比较上进行自动装箱,这将有较差的性能。对于数组字段,将这些指导原则应用于每个元素。如果数组字段中的每个元素都很重要,那么使用 Arrays.equals 方法之一。
注意:写完 equals 方法后,问自己三个问题:它具备对称性吗?具备传递性吗?具备一致性吗?一般来说有IDE来为我们生成会更好。
准则二 当覆盖 equals 方法时,总要覆盖 hashCode 方法
为什么呢?
因为如果不覆写hashCode ,该类将违反 hashCode 方法的一般约定,这将阻止该类在 HashMap 和 HashSet 等集合中正常运行。
Map<Point, String> map = ImmutableBiMap.of(new Point(0, 0), "Origin", new Point(10, 10), "Point");
String val = map.get(new Point(0, 0));
System.out.println(val);
上面这段代码毫无疑问结果是null,但是这个逻辑上不对呀,在equals中我们坐标点相等说明这两个点就是同一个点,这个原因就是我们没有复写hashCode 导致的,相等的对象必须具有相等的散列码。
如果我们复写hashCode 我们就会发现上面的代码就能够输出Origin
@Override
public int hashCode() {
return Objects.hash(x, y);
}
对于生成hashCode 有一套流程,也是书中提到的:
初始化哈希码:
- 初始化一个 int 类型的哈希码变量,通常使用一个非零的质数作为初始值,比如 17 或者 31。
- 计算字段的哈希值:
对于类中的每个字段,计算一个哈希值。
对于基本类型,直接使用其值或转换后的值。
对于引用类型,调用其 hashCode 方法。
对于数组,可以使用 Arrays.hashCode 方法。 - 结合字段哈希值:
使用某种算法(例如乘法和异或运算)将这些哈希值结合起来。
通常的做法是使用一个质数(如 31)乘以前一个哈希值,再加上当前字段的哈希值。 - 返回最终哈希码:
返回计算出的 int 类型哈希码。
如果一个类是不可变的,并且计算散列码的成本非常高,那么你可以考虑在对象中缓存散列码,而不是在每次请求时重新计算它。如果你认为这种类型的大多数对象都将用作散列键,那么你应该在创建实例时计算散列码。同时需要注意线程安全问题。
// 示例这里没有去解决线程安全问题
@Override
public int hashCode() {
int result = hashCode;
if (result == 0) {
result = Short.hashCode(areaCode);
result = 31 * result + Short.hashCode(prefix);
result = 31 * result + Short.hashCode(lineNum);
hashCode = result;
}
return result;
}
注意事项:
- 不要试图从散列码计算中排除重要字段,以提高性能。
虽然得到的散列算法可能运行得更快,但其糟糕的质量可能会将散列表的性能降低到无法使用的程度。特别是,散列算法可能会遇到大量实例,这些实例在你选择忽略的不同区域。如果发生这种情况,散列算法将把所有这些实例映射很少一部分散列码,使得原本应该在线性阶 O(n) 运行的程序将在平方阶 O(n^2) 运行。 - 不要为 hashCode 返回的值提供详细的规范,这样客户端就不能理所应当的依赖它。这(也)给了你更改它的余地。
准则三:始终覆盖 toString 方法
一般来说POJO才会覆盖这些方法,便于我们进行调试,如果对象过大就打印一些摘要信息。同样的对于工具类这种类,也不需要复写toString() 方法,因为这样做没有意义。
准则四:明智地覆盖 clone 方法
- 首先克隆需要实现Cloneable 不然会抛出异常 CloneNotSupportedException
- 由于Object 的clone方法是protected的,所以我们只能通过super关键字去调用,但是这样子类型会不对,因此有了以下的实现, 需要注意的是java默认的拷贝方法是浅拷贝:
@Override
protected Point clone() throws CloneNotSupportedException {
return (Point)super.clone();
}
准则五 考虑实现 Comparable 接口
如果一个类有多个重要字段,那么比较它们的顺序非常关键。从最重要的字段开始,一步步往下。如果比较的结果不是 0(用 0 表示相等),那么就完成了;直接返回结果。
关于比较器书中不推荐下面这种写法,它们依赖于以下事实:如果第一个值小于第二个值,则两个值之间的差为负;如果两个值相等,则为零;如果第一个值大于零,则为正。
static Comparator<Object> hashCodeOrder = new Comparator<>() {
public int compare(Object o1, Object o2) {
return o1.hashCode() - o2.hashCode();
}
};
书中更推荐:
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());