diff --git a/README.md b/README.md index e69de29..9cef404 100644 --- a/README.md +++ b/README.md @@ -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所有对象的通用方法) + diff --git a/ch03所有对象的通用方法/10.重写equals方法时遵守通用约定.md b/ch03所有对象的通用方法/10.重写equals方法时遵守通用约定.md new file mode 100644 index 0000000..a8ff374 --- /dev/null +++ b/ch03所有对象的通用方法/10.重写equals方法时遵守通用约定.md @@ -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 约定以允许实现接口的类进行比较,那么使用接口。 集合接口(如 Set,List,Map 和 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) + diff --git a/ch03所有对象的通用方法/MethodsCommonToAllObjects/.gitignore b/ch03所有对象的通用方法/MethodsCommonToAllObjects/.gitignore new file mode 100644 index 0000000..7caae96 --- /dev/null +++ b/ch03所有对象的通用方法/MethodsCommonToAllObjects/.gitignore @@ -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 diff --git a/ch03所有对象的通用方法/MethodsCommonToAllObjects/pom.xml b/ch03所有对象的通用方法/MethodsCommonToAllObjects/pom.xml new file mode 100644 index 0000000..4360e5a --- /dev/null +++ b/ch03所有对象的通用方法/MethodsCommonToAllObjects/pom.xml @@ -0,0 +1,25 @@ + + 4.0.0 + + com.github.jueee + MethodsCommonToAllObjects + 0.0.1-SNAPSHOT + jar + + MethodsCommonToAllObjects + http://maven.apache.org + + + UTF-8 + + + + + junit + junit + 3.8.1 + test + + + diff --git a/ch03所有对象的通用方法/MethodsCommonToAllObjects/src/main/java/com/jueee/item10/Item10Example01.java b/ch03所有对象的通用方法/MethodsCommonToAllObjects/src/main/java/com/jueee/item10/Item10Example01.java new file mode 100644 index 0000000..c6aa924 --- /dev/null +++ b/ch03所有对象的通用方法/MethodsCommonToAllObjects/src/main/java/com/jueee/item10/Item10Example01.java @@ -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; + } +} \ No newline at end of file diff --git a/ch03所有对象的通用方法/MethodsCommonToAllObjects/src/main/java/com/jueee/item10/Item10Example02.java b/ch03所有对象的通用方法/MethodsCommonToAllObjects/src/main/java/com/jueee/item10/Item10Example02.java new file mode 100644 index 0000000..912b571 --- /dev/null +++ b/ch03所有对象的通用方法/MethodsCommonToAllObjects/src/main/java/com/jueee/item10/Item10Example02.java @@ -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); + } +} \ No newline at end of file diff --git a/ch03所有对象的通用方法/MethodsCommonToAllObjects/src/main/java/com/jueee/item10/Item10Example03.java b/ch03所有对象的通用方法/MethodsCommonToAllObjects/src/main/java/com/jueee/item10/Item10Example03.java new file mode 100644 index 0000000..04ff8bc --- /dev/null +++ b/ch03所有对象的通用方法/MethodsCommonToAllObjects/src/main/java/com/jueee/item10/Item10Example03.java @@ -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; + } +} \ No newline at end of file diff --git a/ch03所有对象的通用方法/MethodsCommonToAllObjects/src/main/java/com/jueee/item10/Item10Example04.java b/ch03所有对象的通用方法/MethodsCommonToAllObjects/src/main/java/com/jueee/item10/Item10Example04.java new file mode 100644 index 0000000..291e718 --- /dev/null +++ b/ch03所有对象的通用方法/MethodsCommonToAllObjects/src/main/java/com/jueee/item10/Item10Example04.java @@ -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; + } +} \ No newline at end of file diff --git a/ch03所有对象的通用方法/MethodsCommonToAllObjects/src/main/java/com/jueee/item10/Item10Example05.java b/ch03所有对象的通用方法/MethodsCommonToAllObjects/src/main/java/com/jueee/item10/Item10Example05.java new file mode 100644 index 0000000..02d70e6 --- /dev/null +++ b/ch03所有对象的通用方法/MethodsCommonToAllObjects/src/main/java/com/jueee/item10/Item10Example05.java @@ -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; + } +} \ No newline at end of file diff --git a/ch03所有对象的通用方法/MethodsCommonToAllObjects/src/main/java/com/jueee/item10/Item10Example06.java b/ch03所有对象的通用方法/MethodsCommonToAllObjects/src/main/java/com/jueee/item10/Item10Example06.java new file mode 100644 index 0000000..19e6bf5 --- /dev/null +++ b/ch03所有对象的通用方法/MethodsCommonToAllObjects/src/main/java/com/jueee/item10/Item10Example06.java @@ -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; + } + +} \ No newline at end of file