@@ -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