Skip to content

Commit 463cbfc

Browse files
committed
update Article 8 for Effective Java
1 parent ce7b20a commit 463cbfc

File tree

1 file changed

+275
-1
lines changed

1 file changed

+275
-1
lines changed

docs/effective-java.md

Lines changed: 275 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -562,4 +562,278 @@ equals 方法实现了等价关系(`equivalence relation`):
562562

563563
- **非空性**。对于任何非 null 的引用值 x,x.equlas(null) 必须返回 false。
564564

565-
现在我们按照顺序逐一查看以下 5 个要求:
565+
现在我们按照顺序逐一查看以下 5 个要求:
566+
567+
**自反性(`reflexive`** --- 第一个要求仅仅说明对象必须等于其本身。很难想象会无意识地违反这一条。假如违背了这一条,然后把该类的实例添加到集合(`collection`)中,该集合的 contains 方法将会果断地告诉你,该集合不包含你刚刚添加的实例。
568+
569+
**对称性(`symmetry`** --- 第二个要求是说,任何对象对于“它们是否相等”的问题都必须保存一致。与第一个要求不同,若无意中违反这一条,这种情形倒是不难想象。例如,考虑下面的类,它实现了一个区分大小写的字符串。字符串由 toString 保存,但在比较操作中被忽略。
570+
571+
```java
572+
573+
// Broken - violates symmetry
574+
public final class CaseInsensitiveString {
575+
private final String s;
576+
577+
public CaseInsensitiveString(String s) {
578+
if (s == null) {
579+
throw new NullPointerException();
580+
}
581+
this.s = s;
582+
}
583+
584+
@Override
585+
public boolean equals(Object o) {
586+
if (o instanceof CaseInsensitiveString) {
587+
return s.equalsIgnoreCase(((CaseInsensitiveString)o).s)
588+
}
589+
590+
// One-way interoperability
591+
if (o instanceof String) {
592+
return s.equalsIgnoreCase((String)o);
593+
}
594+
595+
...// Remainder ommited
596+
}
597+
}
598+
599+
```
600+
601+
在这个类中,equals 方法的意图非常好,它企图与普通的字符串(String)对象进行互操作。假设我们有一个不区分大小写的字符串和一个普通的字符串:
602+
603+
```java
604+
605+
CaseInsensitiveString cis = new CaseInsensitiveString("TommyYang");
606+
String s = "tommyyang";
607+
608+
```
609+
610+
如我们所想,cis.equals(s) 返回 true。问题在于 CaseInsensitiveString 类中的 equals 方法知道普通的字符(String)对象,而 String 类中的 equals 方法却不知道不区分大小写的字符串。因此,s.equals(cis)返回 false,显然这违反了对称性。假设你把不区分大小写的字符串对象放到一个集合中:
611+
612+
```java
613+
614+
List<CaseInsensitiveString> cisList = new ArrayList<>();
615+
cisList.add(cis);
616+
617+
cisList.contains(s);
618+
619+
```
620+
621+
此时 cisList.contains(s) 会返回什么样的结果?没人知道。在 Sun 的当前实现中,它返回 false,但是这只是这个特定实现得出的结果而已。在其它的实现中,它可能返回 true,或抛出运行时(`Runtime`)异常。**一旦违反了 euqals 约定,当其它对象面对你的对象时,你完成不知道这些对象的行为会怎么样**
622+
623+
为了解决这个问题,你只需要将其与 String 对象互操作的代码移除就可以了。
624+
625+
```java
626+
627+
@Override
628+
public boolean equals(Object o) {
629+
return (o instanceof CaseInsensitiveString)
630+
&& s.equalsIgnoreCase(((CaseInsensitiveString)o).s);
631+
}
632+
633+
```
634+
635+
**传递性(`transitive`** --- euqals 约定的第三个要求是,如果一个对象等于第二个对象,第二个对象等于第三个对象,那个第一个对象一定等于第三个对象。同样地,无意识地违反这条规定的情形也不难想象。考虑子类的情形,它将一个新的值组件(`value component`)添加到超类中。换句话说,子类增加的信息会影响到 equals 的比较结果。我们首先以一个简单的不可变的二维整形 Point 类作为开始:
636+
637+
```java
638+
639+
public class Point {
640+
private final int x;
641+
private final int y;
642+
643+
public Point(int x, int y) {
644+
this.x = x;
645+
this.y = y;
646+
}
647+
648+
@Override
649+
public boolean equals(Object o) {
650+
if (!(o instanceof Point)) {
651+
return false;
652+
}
653+
654+
Point p = (Point)o;
655+
return p.x == this.x && p.y == this.y;
656+
}
657+
658+
...// Remainder ommited
659+
}
660+
661+
```
662+
663+
假设你想要扩展这个类,为一个点添加颜色信息:
664+
665+
```java
666+
667+
public class ColorPoint extends Point {
668+
private final Color color;
669+
670+
public ColorPoint(int x, int y, Color color) {
671+
super(x, y);
672+
this.color = color;
673+
}
674+
675+
...// Remainder ommited
676+
}
677+
678+
```
679+
680+
equals 方法会怎么样呢?如果完全不提供 equals 方法,而是直接从 Point 类继承过来,在 equals 方法做比较的时候颜色信息就会被忽略掉。虽然这么做不会违反 equals 约定,但是很明显这是无法接受的。那么我们应该怎么重写 equals 方法呢?
681+
682+
```java
683+
684+
// Broken - violates symmetry
685+
@Override
686+
public boolean equals(Object o) {
687+
if (!(o instanceof ColorPoint)) {
688+
return false;
689+
}
690+
691+
return super.equals(o) && ((ColorPoint)o).color == this.color;
692+
}
693+
694+
```
695+
696+
这个方法的问题在于,你在比较普通点和有色点,以及相反的情形时,可能会得到不同的结果。前一种比较忽略了颜色信息,而后一种比较则总是返回 false,因为参数的类型不正确。为了直观地说明问题所在,我们创建一个普通点和一个有色点:
697+
698+
```java
699+
700+
Point p = new Point(1, 2);
701+
ColorPoint cp = new ColorPoint(1, 2, Color.Red);
702+
703+
```
704+
705+
然而,p.equals(cp) 返回 true, cp.equals(p) 返回 false。你可以做这样的尝试来修正这个问题,让 ColorPoint.equals 在进行“混合比较”的时候忽略颜色信息:
706+
707+
```java
708+
709+
// Broken - violates transitivity
710+
@Override
711+
public boolean equals(Object o) {
712+
if (!(o instanceof Point)) {
713+
return false;
714+
}
715+
716+
// if o is a normal Point, do a color-blind comparison
717+
if (!o instanceof ColorPoint) {
718+
return o.equals(this);
719+
}
720+
721+
// o is a ColorPoint, do a full comparison
722+
return super.equals(o) && ((ColorPoint)o).color == this.color;
723+
}
724+
725+
```
726+
727+
这种做法确实提供了对称性,却忽略了传递性:
728+
729+
```java
730+
731+
ColorPoint p1 = new ColorPoint(1, 2, Color.RED);
732+
Point p2 = new Point(1, 2);
733+
ColorPoint p3 = new ColorPoint(1, 2, Color.BLUE);
734+
735+
```
736+
737+
此时 p1.equals(p2) 和 p2.equals(p3) 都返回 true,但 p1.equals(p3) 返回 false,很显然违反了传递性。前两者的比较不考虑颜色信息(“色盲”),而第三者的比较则考虑了颜色信息。
738+
739+
那么,怎么解决上述问题呢?事实上,这是面向对象语言中关于等价关系的一个基本问题。我们**无法在扩展可实例化的类的同时,既增加新的值组件,同时又保留 equals 约定**,除非愿意放弃面向对象的抽象带来的优势。
740+
741+
也许你了解到,在 equals 方法中用 getClass 测试代替 instanceof 测试,可以扩展可实例化的类和增加新的值组件,同时保留 equals 约定:
742+
743+
```java
744+
745+
// Broken - violates Liskov substitution principle
746+
@Override
747+
public boolean equals(Object o) {
748+
if (o == null || o.getClass() != this.getClass()) {
749+
return false;
750+
}
751+
752+
Point p = (Point)o;
753+
return p.x == this.x && p.y == this.y;
754+
}
755+
756+
```
757+
758+
这段程序只有当对象具有相同实现时,才能是对象等同。虽然这样也不算太糟糕,但是结果确实无法接受的。
759+
760+
假设我们编写一个方法,已检测某个整值点是否处在单位圆中。下面是可以采用的一种方法:
761+
762+
```java
763+
764+
// Initialize UnitCircle to contain all Points on the unit circle
765+
private static final Set<Point> unitCircle;
766+
static {
767+
unitCircle = new HashSet<>();
768+
unitCircle.add(new Point(1, 0));
769+
unitCircle.add(new Point(0, 1));
770+
unitCircle.add(new Point(-1, 0));
771+
unitCircle.add(new Point(0, -1));
772+
}
773+
774+
public static boolean onUnitCircle(Point p) {
775+
return unitCircle.contains(p);
776+
}
777+
778+
```
779+
780+
虽然这种这可能不是实现这种功能的最快方式,不过它的效果很好。但是,假设你通过某种不添加值组件的方式扩展了 Point,例如让它的构造器记录创建了多少个实例:
781+
782+
```java
783+
784+
public class CounterPoint extends Point {
785+
private static final AtomicInteger counter = new AtomicInteger();
786+
787+
public CounterPoint(int x, int y) {
788+
super(x, y);
789+
counter.incrementAndGet();
790+
}
791+
792+
public int numberCreated() {
793+
return counter.get();
794+
}
795+
}
796+
797+
```
798+
799+
**里氏替换原则(`Liskov substitution principle`**认为,一个类型的任何重要属性也将适用于它的子类型,因此为该类型编写的任何方法,在它的子类型上,也应该运行的很好[Liskov87]。但是假设我们将 CounterPointer 实例传递给了 onUnitCircle 方法。如果 Point 类使用了基于 getClass 的 equals 方法,无论 CounterPoint 实例的 x 和 y 的值是多少,onUnitCircle 方法都会返回 false。这时候基于 instanceof 的 equals 方法就会运行的很好。
800+
801+
虽然没有一种令人满意的方法可以既扩展不可实例化的类,又增加值组件,但是还是有一种不错的权宜之计(workaround)。根据第 16 条的建议:复合优先于继承。我们不再让 ColorPoint 继承 Point,而是在 ColorPoint 中加入一个私有的 Point 域,以及一个共有的视图(view)方法(见第 5 条),此方法返回一个与该有色点处在相同位置的普通 Point 对象:
802+
803+
```java
804+
805+
// Add a value component without violating the equals contract
806+
public class ColorPoint {
807+
private final Point point;
808+
private final Color color;
809+
810+
public ColorPoint(int x, int y, Color color) {
811+
if (color == null) {
812+
throw new NullPointerException();
813+
}
814+
815+
this.point = new Point(x, y);
816+
this.color = color;
817+
}
818+
819+
// return the point-view of this color point.
820+
public Point asPoint() {
821+
return this.point;
822+
}
823+
824+
@Override
825+
public boolean equals(Object o){
826+
if (!(o instanceof ColorPoint)){
827+
return false;
828+
}
829+
830+
ColorPoint cp = (ColorPoint)o;
831+
832+
return cp.point.equals(this.point) && cp.color.equals(this.color);
833+
}
834+
835+
}
836+
837+
```
838+
839+
需要我们记住的是:**复合优先于继承**

0 commit comments

Comments
 (0)