@@ -562,4 +562,278 @@ equals 方法实现了等价关系(`equivalence relation`):
562
562
563
563
- ** 非空性** 。对于任何非 null 的引用值 x,x.equlas(null) 必须返回 false。
564
564
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