重写equals方法时遵守通用约定

This commit is contained in:
尉勇强2 2019-09-27 18:46:58 +08:00
parent ca0267cf95
commit 24d84af4a2
10 changed files with 456 additions and 0 deletions

View File

@ -0,0 +1,18 @@
# 说明
### 目录
#### ch02.[创建和销毁对象](ch02创建和销毁对象)
1. [考虑使用静态工厂方法替代构造方法](ch02创建和销毁对象/01.考虑使用静态工厂方法替代构造方法.md)
2. [当构造方法参数过多时使用builder模式](ch02创建和销毁对象/02.当构造方法参数过多时使用builder模式.md)
3. [使用私有构造方法或枚类实现Singleton属性](ch02创建和销毁对象/03.使用私有构造方法或枚类实现Singleton属性.md)
4. [使用私有构造方法执行非实例化](ch02创建和销毁对象/04.使用私有构造方法执行非实例化.md)
5. [使用依赖注入取代硬连接资源](ch02创建和销毁对象/05.使用依赖注入取代硬连接资源.md)
6. [避免创建不必要的对象](ch02创建和销毁对象/06.避免创建不必要的对象.md)
7. [消除过期的对象引用](ch02创建和销毁对象/07.消除过期的对象引用.md)
8. [避免使用Finalizer和Cleaner机制](ch02创建和销毁对象/08.避免使用Finalizer和Cleaner机制.md)
9. [使用try-with-resources语句替代try-finally语句](ch02创建和销毁对象/09.使用try-with-resources语句替代try-finally语句.md)
#### ch03.[所有对象的通用方法](ch03所有对象的通用方法)

View File

@ -0,0 +1,88 @@
## 重写equals方法时遵守通用约定
虽然 `Object` 是一个具体的类,但它主要是为继承而设计的。它的所有非 final 方法equals、hashCode、toString、clone 和 finalize都有清晰的通用约定 general contracts因为它们被设计为被子类重写。任何类要重写这些方法时都有义务去遵从它们的通用约定如果不这样做将会阻止其他依赖于约定的类 (例如 HashMap 和 HashSet) 与此类一起正常工作。
重写 equals 方法看起来很简单,但是有很多方式会导致重写出错,其结果可能是可怕的。避免此问题的最简单方法是不覆盖 equals 方法,在这种情况下,类的每个实例只与自身相等。如果满足以下任一下条件,则说明是正确的做法:
- 每个类的实例都是固有唯一的。 对于像 Thread 这样代表活动实体而不是值的类来说,这是正确的。 Object 提供的 equals 实现对这些类完全是正确的行为。
- 类不需要提供一个「逻辑相等logical equality」的测试功能。例如 `java.util.regex.Pattern` 可以重写 equals 方法检查两个是否代表完全相同的正则表达式 Pattern 实例,但是设计者并不认为客户需要或希望使用此功能。在这种情况下,从 Object 继承的 equals 实现是最合适的。
- 父类已经重写了 equals 方法,则父类行为完全适合于该子类。例如,大多数 Set 从 AbstractSet 继承了 equals 实现、List 从 AbstractList 继承了 equals 实现Map 从 AbstractMap 的 Map 继承了 equals 实现。
- 类是私有的或包级私有的,可以确定它的 equals 方法永远不会被调用。如果你非常厌恶风险,可以重写 equals 方法,以确保不会被意外调用:
```java
@Override
public boolean equals(Object o) {
throw new AssertionError(); // Method is never called
}
```
  什么时候需要重写 equals 方法呢如果一个类包含一个逻辑相等logical equality的概念此概念有别于对象标识object identity而且父类还没有重写过 equals 方法。这通常用在值类value classes的情况。值类只是一个表示值的类例如 Integer 或 String 类。程序员使用 equals 方法比较值对象的引用,期望发现它们在逻辑上是否相等,而不是引用相同的对象。重写 equals 方法不仅可以满足程序员的期望,它还支持重写过 equals 的实例作为 Map 的键key或者 Set 里的元素,以满足预期和期望的行为。
当你重写 equals 方法时必须遵守它的通用约定。Object 的规范如下: equals 方法实现了一个等价关系equivalence relation。它有以下这些属性:
- **自反性:** 对于任何非空引用 x`x.equals(x)` 必须返回 true。
- **对称性:** 对于任何非空引用 x 和 y如果且仅当 `y.equals(x)` 返回 true 时 `x.equals(y)` 必须返回 true。
- **传递性:** 对于任何非空引用 x、y、z如果 `x.equals(y)` 返回 true`y.equals(z)` 返回 true`x.equals(z)` 必须返回 true。
- **一致性:** 对于任何非空引用 x 和 y如果在 equals 比较中使用的信息没有修改,则 `x.equals(y)` 的多次调用必须始终返回 true 或始终返回 false。
- 对于任何非空引用 x`x.equals(null)` 必须返回 false。
### **自反性Reflexivity**
第一个要求只是说一个对象必须与自身相等。 很难想象无意中违反了这个规定。 如果你违反了它,然后把类的实例添加到一个集合中,那么 `contains` 方法可能会说集合中没有包含刚添加的实例。
### **对称性Symmetry**
第二个要求是,任何两个对象必须在是否相等的问题上达成一致。与第一个要求不同的是,我们不难想象在无意中违反了这一要求。
例如,考虑下面的类,它实现了不区分大小写的字符串。字符串被 toString 保存,但在 equals 比较中被忽略:
- [Item10Example01.java](MethodsCommonToAllObjects/src/main/java/com/jueee/item10/Item10Example01.java):违反对称性
- [Item10Example02.java](MethodsCommonToAllObjects/src/main/java/com/jueee/item10/Item10Example02.java):符合对称性
### **传递性Transitivity**
传递性的要求是,如果第一个对象等于第二个对象,第二个对象等于第三个对象,那么第一个对象必须等于第三个对象。
考虑子类的情况, 将新值组件value component添加到其父类中。换句话说子类添加了一个信息它影响了 equals 方法比较。
- [Item10Example03.java](MethodsCommonToAllObjects/src/main/java/com/jueee/item10/Item10Example03.java):不符合对称性
- [Item10Example04.java](MethodsCommonToAllObjects/src/main/java/com/jueee/item10/Item10Example04.java):符合对称性,但是丧失了传递性
- [Item10Example05.java](MethodsCommonToAllObjects/src/main/java/com/jueee/item10/Item10Example05.java):一个很好的变通方法:“优先使用组合而不是继承”。
### **一致性Consistent**
如果两个对象是相等的,除非一个(或两个)对象被修改了, 那么它们必须始终保持相等。
换句话说,可变对象可以在不同时期可以与不同的对象相等,而不可变对象则不会。
### **非空性Non-nullity**
所有的对象都必须不等于 null。虽然很难想象在调用 `o.equals(null)` 的响应中意外地返回 true但不难想象不小心抛出 `NullPointerException` 异常的情况。通用的约定禁止抛出这样的异常。
### 总结
编写高质量 equals 方法的配方recipe
编写高质量 equals 方法的配方recipe
1. 使用 == 运算符检查参数是否为该对象的引用。如果是,返回 true。这只是一种性能优化但是如果这种比较可能很昂贵的话那就值得去做。
2. 使用 `instanceof` 运算符来检查参数是否具有正确的类型。 如果不是,则返回 false。 通常,正确的类型是 equals 方法所在的那个类。 有时候,改类实现了一些接口。 如果类实现了一个接口,该接口可以改进 equals 约定以允许实现接口的类进行比较,那么使用接口。 集合接口(如 SetListMap 和 Map.Entry具有此特性。
3. 参数转换为正确的类型。因为转换操作在 instanceof 中已经处理过,所以它肯定会成功。
4. 对于类中的每个「重要」的属性,请检查该参数属性是否与该对象对应的属性相匹配。如果所有这些测试成功,返回 true否则返回 false。如果步骤 2 中的类型是一个接口,那么必须通过接口方法访问参数的属性;如果类型是类,则可以直接访问属性,这取决于属性的访问权限。
对于类型为非 float 或 double 的基本类型,使用 == 运算符进行比较;对于对象引用属性,递归地调用 equals 方法;对于 float 基本类型的属性,使用静态 `Float.compare(float, float)` 方法;对于 double 基本类型的属性,使用 `Double.compare(double, double)` 方法。
某些对象引用的属性可能合法地包含 null。 为避免出现 `NullPointerException` 异常,请使用静态方法 Objects.equals(Object, Object) 检查这些属性是否相等。
equals 方法的性能可能受到属性比较顺序的影响。 为了获得最佳性能你应该首先比较最可能不同的属性开销比较小的属性或者最好是两者都满足derived fields。 你不要比较不属于对象逻辑状态的属性,例如用于同步操作的 lock 属性。 不需要比较可以从“重要属性”计算出来的派生属性,但是这样做可以提高 equals 方法的性能。 如果派生属性相当于对整个对象的摘要描述,比较这个属性将节省在比较失败时再去比较实际数据的开销。
### 提醒
1. **当重写 equals 方法时,同时也要重写 hashCode 方法**
2. **不要让 equals 方法试图太聪明。** 如果只是简单地测试用于相等的属性,那么要遵守 equals 约定并不困难。如果你在寻找相等方面过于激进那么很容易陷入麻烦。一般来说考虑到任何形式的别名通常是一个坏主意。例如File 类不应该试图将引用的符号链接等同于同一文件对象。幸好 File 类并没这么做。
3. **在 equal 时方法声明中,不要将参数 Object 替换成其他类型。** 对于程序员来说,编写一个看起来像这样的 equals 方法并不少见,然后花上几个小时苦苦思索为什么它不能正常工作:在 equal 时方法声明中,不要将参数 Object 替换成其他类型。对于程序员来说,编写一个看起来像这样的 equals 方法并不少见,然后花上几个小时苦苦思索为什么它不能正常工作。
### 完整示例
[Item10Example06.java](MethodsCommonToAllObjects/src/main/java/com/jueee/item10/Item10Example06.java)

View File

@ -0,0 +1,32 @@
HELP.md
target/
!.mvn/wrapper/maven-wrapper.jar
!**/src/main/**
!**/src/test/**
### STS ###
.apt_generated
.classpath
.factorypath
.project
.settings
.springBeans
.sts4-cache
### IntelliJ IDEA ###
.idea
*.iws
*.iml
*.ipr
### NetBeans ###
/nbproject/private/
/nbbuild/
/dist/
/nbdist/
/.nb-gradle/
build/
### VS Code ###
.vscode/
/chromedriver.exe

View File

@ -0,0 +1,25 @@
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>com.github.jueee</groupId>
<artifactId>MethodsCommonToAllObjects</artifactId>
<version>0.0.1-SNAPSHOT</version>
<packaging>jar</packaging>
<name>MethodsCommonToAllObjects</name>
<url>http://maven.apache.org</url>
<properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
</properties>
<dependencies>
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>3.8.1</version>
<scope>test</scope>
</dependency>
</dependencies>
</project>

View File

@ -0,0 +1,38 @@
package com.jueee.item10;
import java.util.Objects;
// 违反对称性
public class Item10Example01 {
public static void main(String[] args) {
CaseInsensitiveString01 cis = new CaseInsensitiveString01("Polish");
String s = "polish";
System.out.println(cis.equals(s)); // true
System.out.println(s.equals(cis)); // false
CaseInsensitiveString01 cis2 = new CaseInsensitiveString01("polish");
System.out.println(cis.equals(cis2)); // true
}
}
// 实现了不区分大小写的字符串字符串被 toString 保存但在 equals 比较中被忽略
class CaseInsensitiveString01 {
private final String s;
public CaseInsensitiveString01(String s) {
this.s = Objects.requireNonNull(s);
}
// Broken - violates symmetry!
@Override
public boolean equals(Object o) {
if (o instanceof CaseInsensitiveString01)
return s.equalsIgnoreCase(((CaseInsensitiveString01)o).s);
if (o instanceof String) // One-way interoperability!
return s.equalsIgnoreCase((String)o);
return false;
}
}

View File

@ -0,0 +1,34 @@
package com.jueee.item10;
import java.util.Objects;
// 符合对称性
public class Item10Example02 {
public static void main(String[] args) {
CaseInsensitiveString02 cis = new CaseInsensitiveString02("Polish");
String s = "polish";
System.out.println(cis.equals(s)); // false
System.out.println(s.equals(cis)); // false
CaseInsensitiveString02 cis2 = new CaseInsensitiveString02("polish");
System.out.println(cis.equals(cis2)); // true
}
}
// 实现了不区分大小写的字符串字符串被 toString 保存但在 equals 比较中被忽略
class CaseInsensitiveString02 {
private final String s;
public CaseInsensitiveString02(String s) {
this.s = Objects.requireNonNull(s);
}
// Broken - violates symmetry!
@Override
public boolean equals(Object o) {
return o instanceof CaseInsensitiveString02 && ((CaseInsensitiveString02)o).s.equalsIgnoreCase(s);
}
}

View File

@ -0,0 +1,50 @@
package com.jueee.item10;
import java.awt.Color;
// 不符合对称性
public class Item10Example03 {
public static void main(String[] args) {
Point01 p = new Point01(1, 2);
ColorPoint01 cp = new ColorPoint01(1, 2, Color.RED);
System.out.println(p.equals(cp)); // true
System.out.println(cp.equals(p)); // false
}
}
class ColorPoint01 extends Point01 {
private final Color color;
public ColorPoint01(int x, int y, Color color) {
super(x, y);
this.color = color;
}
@Override
public boolean equals(Object o) {
if (!(o instanceof ColorPoint01))
return false;
return super.equals(o) && ((ColorPoint01) o).color == color;
}
}
// 简单不可变的二维整数类型 Point01
class Point01 {
private final int x;
private final int y;
public Point01(int x, int y) {
this.x = x;
this.y = y;
}
@Override
public boolean equals(Object o) {
if (!(o instanceof Point01))
return false;
Point01 p = (Point01) o;
return p.x == x && p.y == y;
}
}

View File

@ -0,0 +1,61 @@
package com.jueee.item10;
import java.awt.Color;
// 符合对称性但是丧失了传递性
public class Item10Example04 {
public static void main(String[] args) {
Point02 p = new Point02(1, 2);
ColorPoint02 cp1 = new ColorPoint02(1, 2, Color.RED);
ColorPoint02 cp2 = new ColorPoint02(1, 2, Color.BLUE);
System.out.println(p.equals(cp1)); // true
System.out.println(cp1.equals(p)); // true
System.out.println(p.equals(cp2)); // true
System.out.println(cp2.equals(p)); // true
System.out.println(cp1.equals(cp2)); // false
System.out.println(cp2.equals(cp1)); // false
}
}
class ColorPoint02 extends Point02 {
private final Color color;
public ColorPoint02(int x, int y, Color color) {
super(x, y);
this.color = color;
}
@Override
public boolean equals(Object o) {
if (!(o instanceof Point02))
return false;
// If o is a normal Point, do a color-blind comparison
if (!(o instanceof ColorPoint02))
return o.equals(this);
// o is a ColorPoint; do a full comparison
return super.equals(o) && ((ColorPoint02) o).color == color;
}
}
// 简单不可变的二维整数类型 Point
class Point02 {
private final int x;
private final int y;
public Point02(int x, int y) {
this.x = x;
this.y = y;
}
@Override
public boolean equals(Object o) {
if (!(o instanceof Point02))
return false;
Point02 p = (Point02) o;
return p.x == x && p.y == y;
}
}

View File

@ -0,0 +1,65 @@
package com.jueee.item10;
import java.awt.Color;
import java.util.Objects;
// 虽然没有令人满意的方法来继承一个可实例化的类并添加一个值组件但是有一个很好的变通方法优先使用组合而不是继承
public class Item10Example05 {
public static void main(String[] args) {
Point03 p = new Point03(1, 2);
ColorPoint03 cp1 = new ColorPoint03(1, 2, Color.RED);
ColorPoint03 cp2 = new ColorPoint03(1, 2, Color.BLUE);
System.out.println(p.equals(cp1.asPoint())); // true
System.out.println(cp1.asPoint().equals(p)); // true
System.out.println(p.equals(cp2.asPoint())); // true
System.out.println(cp2.asPoint().equals(p)); // true
System.out.println(cp1.asPoint().equals(cp2.asPoint())); // true
System.out.println(cp2.asPoint().equals(cp1.asPoint())); // true
}
}
class ColorPoint03 {
private final Point03 point;
private final Color color;
public ColorPoint03(int x, int y, Color color) {
point = new Point03(x, y);
this.color = Objects.requireNonNull(color);
}
/**
* Returns the point-view of this color point.
*/
public Point03 asPoint() {
return point;
}
@Override
public boolean equals(Object o) {
if (!(o instanceof ColorPoint03))
return false;
ColorPoint03 cp = (ColorPoint03) o;
return cp.point.equals(point) && cp.color.equals(color);
}
}
// 简单不可变的二维整数类型 Point
class Point03 {
public final int x;
public final int y;
public Point03(int x, int y) {
this.x = x;
this.y = y;
}
@Override
public boolean equals(Object o) {
if (!(o instanceof Point03))
return false;
Point03 p = (Point03) o;
return p.x == x && p.y == y;
}
}

View File

@ -0,0 +1,45 @@
package com.jueee.item10;
public class Item10Example06 {
public static void main(String[] args) {
PhoneNumber n1 = new PhoneNumber(3, 5, 8);
PhoneNumber n2 = new PhoneNumber(3, 5, 8);
PhoneNumber n3 = new PhoneNumber(3, 5, 8);
System.out.println(n1.equals(n2)); // true
System.out.println(n2.equals(n1)); // true
System.out.println(n2.equals(n3)); // true
System.out.println(n3.equals(n1)); // true
}
}
final class PhoneNumber {
private final short areaCode, prefix, lineNum;
public PhoneNumber(int areaCode, int prefix, int lineNum) {
this.areaCode = rangeCheck(areaCode, 999, "area code");
this.prefix = rangeCheck(prefix, 999, "prefix");
this.lineNum = rangeCheck(lineNum, 9999, "line num");
}
private static short rangeCheck(int val, int max, String arg) {
if (val < 0 || val > max)
throw new IllegalArgumentException(arg + ": " + val);
return (short)val;
}
@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;
}
}